#!/bin/bash HSHQ_LIB_SCRIPT_VERSION=249 LOG_LEVEL=info # Copyright (C) 2023 HomeServerHQ # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . set -e function init() { # Version 74 Update # Changing to the home directory fixes issues with docker compose (v2) # since the command 'stat .' (inside of docker compose) results in a # permission denied error when running script from root as another user, # i.e. 'sudo -u bash hshq.sh' # Basically, docker compose is dependent on which directory it is # executed from, regardless if a specified file is in a readable directory. cd ~ IS_STACK_DEBUG=false USERNAME=$(id -u -n) PRIOR_HSHQ_VERSION=0 LAST_RELAYSERVER_VERSION_UPDATE=239 IS_AUTO_INSTALL=false HSHQ_INSTALL_VARIANT=base loadVersionVars IS_RUST_UTILS=false if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26\.04. ]]; then IS_RUST_UTILS=true fi IS_DESKTOP_ENV=false if [ -d $HOME/Desktop ]; then IS_DESKTOP_ENV=true fi IS_CONSOLE_ENV=false # Leave this set to true for a bit, to allow for users to update. # Eventually, we'll turn it back off and only restart as needed. IS_RESTART_SCRIPTSERVER=true LOCK_UTILS_FILENAME=lockUtils.sh LOCKHOLDER_FILENAME=lastCaller ALL_LOCKS="hshqopen networkchecks" CONFIG_FILE_DEFAULT_FILENAME=config.cnf ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME=config.enc ENCRYPTED_CONFIG_FILE_BACKUP_FILENAME=config-enc.bak ENC_PW_FILE=/tmp/encpw.enc TMP_PW_CHECKDIR=/tmp/pwcheckdir IS_CONFIG_FILE_UNENCRYPTED=false RS_INSTALL_SETUP_SCRIPT_NAME=setupRS.sh RS_INSTALL_FRESH_SCRIPT_NAME=installRS.sh RS_INSTALL_TRANSFER_SCRIPT_NAME=transferRS.sh RS_INSTALL_VALIDATION_SCRIPT_NAME=validateRS.sh RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME=validateLib.sh RS_UPDATE_UTILS_SCRIPT_NAME=rsUtilsScript.sh RS_UPDATE_SCRIPT_NAME=rsUpdateScript.sh NUKE_SCRIPT_NAME=nuke.sh RELAYSERVER_HSHQ_FULL_LOG_NAME=hshqRSInstall.log RELAYSERVER_HSHQ_TIMESTAMP_LOG_NAME=hshqRSInstallTS.log RELAYSERVER_NOT_READY_FILE=notready RELAYSERVER_SCRIPTS_UPLOADED_FILE=uploaded RELAYSERVER_INSTALL_COMPLETE_FILE=installed RELAYSERVER_INSTALL_FAILED_FILE=failed RELAYSERVER_SWAP_SIZE=2G HOMESERVER_SWAP_SIZE=16G HSHQ_LOG_FILE=/var/log/hshq.log HSHQ_INSTALL_NOTES_FILENAME='HomeServer_Install_Notes.txt' SCRIPTSERVER_FULL_STACKLIST_FILENAME=fullStackList.txt SCRIPTSERVER_OPTIONAL_STACKLIST_FILENAME=optionalStackList.txt SCRIPTSERVER_UPDATE_STACKLIST_FILENAME=updateStackList.txt SCRIPTSERVER_REDIS_STACKLIST_FILENAME=redisStackList.txt ALLDBS_IMPORT_FILENAME=DBCImport.txt DNS_ZONE_FILENAME_BASE=DNSZoneInfo DOCKER_VERSION_UBUNTU_2204=5:29.2.1-1~ubuntu.22.04~jammy DOCKER_VERSION_UBUNTU_2404=5:29.4.0-1~ubuntu.24.04~noble DOCKER_VERSION_UBUNTU_2604=5:29.4.0-1~ubuntu.24.04~noble DOCKER_VERSION_DEBIAN_12=5:29.1.4-1~debian.12~bookworm DOCKER_VERSION_CHECK_UBUNTU_2204=29.2.1 DOCKER_VERSION_CHECK_UBUNTU_2404=29.4.0 DOCKER_VERSION_CHECK_UBUNTU_2604=29.4.0 DOCKER_VERSION_CHECK_DEBIAN_12=29.1.4 NET_PRIVATEIP_BRIDGE_NAME=brdockpriv NET_PRIVATEIP_SUBNET=172.16.1.0/24 NET_PRIVATEIP_SUBNET_PREFIX=172.16.1 NET_MAILU_EXT_BRIDGE_NAME=brmailuext NET_MAILU_EXT_SUBNET=172.16.2.0/24 NET_MAILU_EXT_SUBNET_PREFIX=172.16.2 NET_MAILU_INT_BRIDGE_NAME=brmailuint NET_MAILU_INT_SUBNET=172.16.3.0/24 NET_MAILU_INT_SUBNET_PREFIX=172.16.3 NET_WEBPROXY_BRIDGE_NAME=brdockproxy NET_WEBPROXY_SUBNET=172.16.4.0/22 NET_WEBPROXY_SUBNET_PREFIX=172.16.4 NET_EXTERNAL_BRIDGE_NAME=brdockext NET_EXTERNAL_SUBNET=172.16.8.0/21 NET_EXTERNAL_SUBNET_PREFIX=172.16.8 NET_INTERNALMAIL_BRIDGE_NAME=brdockmail NET_INTERNALMAIL_SUBNET=172.16.16.0/22 NET_INTERNALMAIL_SUBNET_PREFIX=172.16.16 NET_DBS_BRIDGE_NAME=brdockdbs NET_DBS_SUBNET=172.16.20.0/22 NET_DBS_SUBNET_PREFIX=172.16.20 NET_LDAP_BRIDGE_NAME=brdockldap NET_LDAP_SUBNET=172.16.24.0/22 NET_LDAP_SUBNET_PREFIX=172.16.24 NET_AIPRIVATE_SUBNET=172.16.28.0/22 NET_AIPRIVATE_SUBNET_PREFIX=172.16.28 NET_AIPRIVATE_BRIDGE_NAME=brdockaipriv NET_AILIMITED_SUBNET=172.16.32.0/24 NET_AILIMITED_SUBNET_PREFIX=172.16.32 NET_AILIMITED_BRIDGE_NAME=brdockailim RELAYSERVER_NET_EXTERNAL_BRIDGE_NAME=brdockext RELAYSERVER_NET_EXTERNAL_SUBNET=172.16.1.0/24 RELAYSERVER_NET_EXTERNAL_SUBNET_PREFIX=172.16.1 RELAYSERVER_NET_WEBPROXY_BRIDGE_NAME=brdockproxy RELAYSERVER_NET_WEBPROXY_SUBNET=172.16.2.0/24 RELAYSERVER_NET_WEBPROXY_SUBNET_PREFIX=172.16.2 RELAYSERVER_MAIL_RELAY_BRIDGE_NAME=brdockmrelay RELAYSERVER_MAIL_RELAY_SUBNET=172.16.3.0/24 RELAYSERVER_MAIL_RELAY_SUBNET_PREFIX=172.16.3 RELAYSERVER_CLIENT_DNS_BRIDGE_NAME=brdockcdns RELAYSERVER_CLIENT_DNS_SUBNET=172.16.4.0/24 RELAYSERVER_CLIENT_DNS_SUBNET_PREFIX=172.16.4 ADGUARD_DNS_PORT=53 CADDY_HTTP_PORT=80 CADDY_HTTPS_PORT=443 COTURN_PRIMARY_PORT=3478 COTURN_SECONDARY_PORT=5349 COTURN_COMMS_MIN_PORT=14100 COTURN_COMMS_MAX_PORT=14200 JELLYFIN_PORT=7359 JITSI_COLIBRI_PORT=8020 JITSI_JVB_PORT=4443 JITSI_MEET_PORT=10000 KHOJ_VNC_PORT=55901 MAILU_PORT_1=25 MAILU_PORT_2=110 MAILU_PORT_3=143 MAILU_PORT_4=465 MAILU_PORT_5=587 MAILU_PORT_6=993 MAILU_PORT_7=995 PEERTUBE_RDP_PORT=1935 QBITTORRENT_PORT=6881 SYNCTHING_DISC_PORT=21027 SYNCTHING_SYNC_PORT=22000 SYNCTHING_LOCAL_WEB_PORT=8384 UPNP_PORT=1900 VNC_SERVER_PORT=5901 WAZUH_PORT_1=1514 WAZUH_PORT_2=1515 WAZUH_PORT_3=514 WAZUH_PORT_4=55000 WAZUH_PORT_5=9200 WAZUH_AGENT_VERSION=4.14.7-1 PAPERLESS_ADMIN_ID=2 DEFAULT_UNFOUND_IP_ADDRESS=169.254.84.48 DEFAULT_UNFOUND_IP_SUBNET=169.254.0.0/16 MAX_DOCKER_PULL_TRIES=20 MENU_WIDTH=69 MENU_HEIGHT=24 MENU_INT_HEIGHT=10 ENABLE_STACK_DELETE=false SUDO_NORMAL_TIMEOUT=15 SUDO_LONG_TIMEOUT=1440 SUDO_LONG_TIMEOUT_FILENAME=sudohshqinstall SUDO_MAX_RETRIES=20 MAILX_TIMEOUT=15 NETPLAN_ORIGIN_HINT=88-hshq NETPLAN_APPLY_WAIT=5 MIN_NETWORKCHECK_INTERVAL=5 STACK_VERSION_PREFIX=#HSHQManaged HSHQ_ADMIN_NAME="HSHQ Admin" IS_HSHQ_DEV_FILENAME=hshq.dev IS_HSHQ_TEST_FILENAME=hshq.test IS_HSHQ_DEV_TEST=false if [ -f $HOME/hshq/$IS_HSHQ_DEV_FILENAME ] || [ -f $HOME/hshq/$IS_HSHQ_TEST_FILENAME ]; then IS_HSHQ_DEV_TEST=true fi HSHQ_WRAP_URL=https://homeserverhq.com/hshq.sh HSHQ_WRAP_VER_URL=https://homeserverhq.com/ver/getwrapversion HSHQ_WRAP_DEV_URL=https://homeserverhq.com/hshq-dev.sh HSHQ_WRAP_DEV_VER_URL=https://homeserverhq.com/ver/getwrapversion-dev HSHQ_WRAP_TEST_URL=https://homeserverhq.com/hshq-test.sh HSHQ_WRAP_TEST_VER_URL=https://homeserverhq.com/ver/getwrapversion-test HSHQ_LIB_URL=https://homeserverhq.com/hshqlib.sh HSHQ_LIB_VER_URL=https://homeserverhq.com/ver/getversion HSHQ_LIB_DEV_URL=https://homeserverhq.com/hshqlib-dev.sh HSHQ_LIB_DEV_VER_URL=https://homeserverhq.com/ver/getversion-dev HSHQ_LIB_TEST_URL=https://homeserverhq.com/hshqlib-test.sh HSHQ_LIB_TEST_VER_URL=https://homeserverhq.com/ver/getversion-test HSHQ_RELEASES_URL=https://homeserverhq.com/releases HSHQ_SIG_BASE_URL=https://homeserverhq.com/signatures/ HSHQ_GPG_FINGERPRINT=5B9C33067C71ABCFCE1ACF8A7F46128ABB7C1E42 HSHQ_GPG_FINGERPRINT_SHORT=7F46128ABB7C1E42 HSHQ_LIB_TMP=$HOME/hshqlib.tmp HSHQ_WRAP_TMP=$HOME/hshqwrap.tmp HSHQ_LIB_FILENAME=hshqlib.sh HSHQ_NEW_LIB_FILENAME=hshqlib.new HSHQ_WRAP_FILENAME=hshq.sh HSHQ_ASSETS_GIT_REPO=https://github.com/homeserverhq/assets.git APPLICATION_FIRST_LINE="###################### Application Begin #######################" APPLICATION_LAST_LINE="####################### Application End ########################" INVITATION_FIRST_LINE="####################### Invitation Begin #######################" INVITATION_LAST_LINE="######################## Invitation End ########################" initServiceDefaults loadPinnedDockerImages loadDirectoryStructure CONFIG_FILE_DEFAULT_LOCATION=$HSHQ_CONFIG_DIR HSHQ_DB=$HSHQ_CONFIG_DIR/hshq.db HSHQ_INSTALL_CFG=$HSHQ_BASE_DIR/installConfig.txt HSHQ_CUSTOM_PRE_IPTABLES_SCRIPT=$HSHQ_SCRIPTS_DIR/root/userCustomPreIPTables.sh HSHQ_CUSTOM_POST_IPTABLES_SCRIPT=$HSHQ_SCRIPTS_DIR/root/userCustomPostIPTables.sh HSHQ_PLAINTEXT_ROOT_CONFIG=$HSHQ_CONFIG_DIR/ptRootConfig.conf HSHQ_PLAINTEXT_USER_CONFIG=$HSHQ_CONFIG_DIR/ptUserConfig.conf SYSTEM_STATE_FILENAME=$HSHQ_CONFIG_DIR/systemstate SS_INIT=init SS_INSTALLING=installing SS_RESTORING=restoring SS_BOOTING=booting SS_TRANSFERRING=transferring SS_UPDATING=updating SS_REMOVING=removing SS_RUNNING=running RESTORE_SCREEN_NAME=hshqRestore UTILS_LIST="wget|wget whiptail|whiptail awk|gawk screen|screen pwgen|pwgen argon2|argon2 dig|dnsutils htpasswd|apache2-utils ssh|ssh sshd|openssh-server sshpass|sshpass wg|wireguard-tools qrencode|qrencode openssl|openssl faketime|faketime bc|bc sipcalc|sipcalc jq|jq git|git http|httpie sqlite3|sqlite3 curl|curl sha1sum|coreutils lsb_release|lsb-release nano|nano cron|cron ping|iputils-ping route|net-tools grepcidr|grepcidr networkd-dispatcher|networkd-dispatcher certutil|libnss3-tools update-ca-certificates|ca-certificates gpg|gnupg python3|python3 pip3|python3-pip unzip|unzip hwinfo|hwinfo netplan|netplan.io netplan|openvswitch-switch uuidgen|uuid-runtime aa-enforce|apparmor-utils logrotate|logrotate yq|yq iwlist|wireless-tools sudo|sudo gcc|build-essential gio|libglib2.0-bin ffmpeg|ffmpeg php|php-cli gnupg2|gnupg2 apt-transport-https|apt-transport-https software-properties-common|software-properties-common python3-werkzeug|python3-werkzeug inotifywait|inotify-tools" DESKTOP_APT_LIST="" APT_REMOVE_LIST="xxd needrestart bsd-mailx" RELAYSERVER_UTILS_LIST="curl|curl awk|gawk whiptail|whiptail nano|nano screen|screen htpasswd|apache2-utils pwgen|pwgen git|git http|httpie jq|jq sqlite3|sqlite3 wg|wireguard-tools qrencode|qrencode route|net-tools sipcalc|sipcalc mailx|mailutils ipset|ipset uuidgen|uuid-runtime grepcidr|grepcidr networkd-dispatcher|networkd-dispatcher aa-enforce|apparmor-utils logrotate|logrotate" hshqlogo=$(cat << EOFLG #===============================================================# # ▀█ █ ▄▄ ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄ █ █ █▀▀█ # # █▀▀█ █ █ █ █ █ ▄▄ ▀▀▀▄▄ ▄▄ ▄▄▄▀ ▀▄ ▄▀ ▄▄ ▄▄▄▀ █▀▀█ █ █ # # █ █ ▀▄▄▀ █ █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄ ▀█▀ ▄▄▄ ▄▄▄▄ █ █ █▄▄█▄ # #===============================================================# EOFLG ) hshqwarninglogo=$(cat << EOFLG #===============================================================# # ▀█ █ ▄▄ ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄ █ █ █▀▀█ # # █▀▀█ █ █ █ █ █ ▄▄ ▀▀▀▄▄ ▄▄ ▄▄▄▀ ▀▄ ▄▀ ▄▄ ▄▄▄▀ █▀▀█ █ █ # # █ █ ▀▄▄▀ █ █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄ ▀█▀ ▄▄▄ ▄▄▄▄ █ █ █▄▄█▄ # #===============================================================# ***** WARNING - Config File is UNENCRYPTED! ***** EOFLG ) if [ -d $HSHQ_SCRIPTS_DIR/source ]; then for curSource in $HSHQ_SCRIPTS_DIR/source/*; do if [ -f "$curSource" ]; then source "$curSource" fi done fi } function main() { init checkWrapperVersion IS_CONSOLE_ENV=true IS_PERFORM_INSTALL=false IS_NEW_LIB=false CONNECTING_IP="" IS_PERFORM_RESTORE=false IS_PERFORM_UPDATE=false case "$1" in "install") IS_PERFORM_INSTALL=true CONNECTING_IP=$2 ;; "-a") CONNECTING_IP=$2 ;; "run") IS_NEW_LIB=$2 CONNECTING_IP=$3 ;; "restore") CONNECTING_IP=$2 IS_PERFORM_RESTORE=true ;; "update") IS_PERFORM_UPDATE=true ;; *) ;; esac if [ -z "$CONNECTING_IP" ]; then CONNECTING_IP=$(getConnectingIPAddress) fi if [ -z "$CONNECTING_IP" ] && [ -f ~/hshq/cip.txt ]; then CONNECTING_IP=$(cat ~/hshq/cip.txt) fi if [ "$IS_PERFORM_UPDATE" = "true" ]; then setSystemState $SS_UPDATING checkUpdateVersion moveScriptServerPerformUpdate if [ "$IS_RESTART_SCRIPTSERVER" = "true" ]; then sudo systemctl daemon-reload sudo systemctl restart runScriptServer.service fi setSystemState $SS_RUNNING return fi if ! [ -z "$USER_SUDO_PW" ]; then sudo -k echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ $? -ne 0 ]; then USER_SUDO_PW="" fi fi if [ "$IS_PERFORM_INSTALL" = "true" ]; then mkdir -p $TMP_PW_CHECKDIR read -r -s -p "" sudoPWKey USER_SUDO_PW="$(openssl enc -d -aes256 -pbkdf2 -in $ENC_PW_FILE -pass pass:$sudoPWKey)" rm -f $ENC_PW_FILE refreshSudo rm -fr $TMP_PW_CHECKDIR clear stty echo if [ "$USERNAME" = "root" ]; then strErr="main - Cannot install as root user, exiting..." logHSHQEvent error "$strErr" echo "$strErr" exit 1 fi performBaseInstallation exit 0 fi if [ "$IS_PERFORM_RESTORE" = "true" ]; then mkdir -p $TMP_PW_CHECKDIR read -r -s -p "" sudoPWKey USER_SUDO_PW="$(openssl enc -d -aes256 -pbkdf2 -in $ENC_PW_FILE -pass pass:$sudoPWKey)" rm -f $ENC_PW_FILE refreshSudo rm -fr $TMP_PW_CHECKDIR clear stty echo if [ "$USERNAME" = "root" ]; then echo "Cannot install as root user, exiting..." exit 1 fi performFullRestore exit 0 fi set +e # Mitigate any race conditions after an update sleep 2 tgLock="$(tryGetLock hshqopen User-Console)" # Additional measures against race conditions if ! [ "$tgLock" = "true" ]; then sleep 2 tgLock="$(tryGetLock hshqopen User-Console)" fi if ! [ "$tgLock" = "true" ]; then checkRes="$(getLockOpenMsg hshqopen)" showNoYesMessageBox "Script Running" "It appears that this script is already running in another session ($checkRes). This situation could occur if another scheduled process is currently running or the script exited prematurely with an error. Unknown and damaging consequences can occur if running two different instances at the same time. Are you certain that you wish to continue?" mbres=$? if [ $mbres -eq 0 ]; then exit fi sudo -k USER_SUDO_PW="" refreshSudo if [ $? -ne 0 ]; then exit 3 fi forceGetLock hshqopen "User-Console" fi set -e checkConfigAvailable is_hshq_installed=false if ! [ -z "$CONFIG_FILE" ] && [ -f $CONFIG_FILE ]; then isInstalled=$(getConfigVarFromFile IS_INSTALLED $CONFIG_FILE) if [ "$isInstalled" = "true" ]; then is_hshq_installed=true IS_CONFIG_FILE_UNENCRYPTED=true set +e fixConfigV130 set -e source $CONFIG_FILE else is_hshq_installed=false fi elif ! [ -z "$ENC_CONFIG_FILE" ] && [ -f $ENC_CONFIG_FILE ]; then # Encrypted file found, assume installed. is_hshq_installed=true fi set +e mainMenuResult=1 while [[ $mainMenuResult -ne 0 ]] do if [ "$is_hshq_installed" = "true" ]; then if [ "$IS_NEW_LIB" = "true" ]; then checkLoadConfig fi showInstalledMenu else showNotInstalledMenu fi mainMenuResult=$? done performExitFunctions true } function showNotInstalledMenu() { set +e checkSupportedHostOS chmod 544 ~/hshq.sh refreshSudo setSudoTimeoutInstall if [[ "$(isProgramInstalled sshd)" = "false" ]]; then sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing openssh-server, please wait..." performAptInstall openssh-server > /dev/null 2>&1 fi notinstalledmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$notinstalledmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Perform Base Installation" \ "2" "Edit Configuration" \ "3" "Install Dependencies" \ "4" "Update APT Mirror" \ "5" "Uninstall and Remove Everything" \ "6" "Restore From Backup" \ "7" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi set -e case $menures in 0) return 0 ;; 1) menuPerformBaseInstall ;; 2) checkWorkingInternet checkUpdateAptSources false set -e checkLoadConfig initConfig nano $CONFIG_FILE loadConfigVars true set +e return 1 ;; 3) checkWorkingInternet checkUpdateAptSources false set -e installDependencies pullBaseServicesDockerImages sudo reboot ;; 4) set +e checkWorkingInternet refreshSudo checkUpdateAptSources true return 1 ;; 5) nukeHSHQ set +e return 1 ;; 6) checkWorkingInternet checkUpdateAptSources false showRestoreMenu set +e return 1 ;; 7) return 0 ;; esac } function menuPerformBaseInstall() { checkWorkingInternet checkUpdateAptSources false clear checkDockerPre set -e clear echo "Please wait..." checkLoadConfig initConfig initCertificateAuthority if [ "$IS_DESKTOP_ENV" = "true" ]; then PRIMARY_VPN_SETUP_TYPE=none updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE else setupVPNConnection if [ $? -ne 0 ]; then exit fi fi if [ "$IS_INSTALLED" = "true" ] || [ "$IS_INSTALLING" = "true" ]; then showMessageBox "Error" "Already installed or existing installation is in progress." set +e exit fi initInstallation } function checkDockerPre() { set +e sudo docker ps > /dev/null 2>&1 if [ $? -ne 0 ]; then if [ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]; then # Docker must be installed and system rebooted showMessageBox "WARNING" "It appears that docker is not installed, and the OS Linux version is Debian 12. Due to recent changes, docker must be installed beforehand and the system rebooted in order to continue. After the system has rebooted, please log back in with this same user ($USERNAME) and restart the installation (bash hshq.sh). Press okay to proceed." upgradeHostOS sudo DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' installDocker sudo reboot fi fi } function upgradeHostOS() { if [ "$IS_DISABLE_HOST_UPGRADE" = "true" ]; then return fi sudo DEBIAN_FRONTEND=noninteractive apt update sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' } function checkWorkingInternet() { set +e timeout 10 ping -c 1 1.1.1.1 > /dev/null 2>&1 if [ $? -ne 0 ]; then showMessageBox "ERROR" "You do not appear to have a working internet connection. Please check your network settings and try again." exit 2 fi } function verifyAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then if test -f /etc/apt/sources.list; then echo "true" return fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then if test -f /etc/apt/sources.list.d/ubuntu.sources; then echo "true" return fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then if test -f /etc/apt/sources.list.d/ubuntu.sources; then echo "true" return fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then if test -f /etc/apt/sources.list; then echo "true" return fi elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then if test -f /etc/apt/sources.list.d/official-package-repositories.list; then echo "true" return fi fi echo "false" } function backupOrigAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then if ! test -f /etc/apt/sources.list.orig; then sudo cp /etc/apt/sources.list /etc/apt/sources.list.orig fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then if ! test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.orig fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then if ! test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.orig fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then if ! test -f /etc/apt/sources.list.orig; then sudo cp /etc/apt/sources.list /etc/apt/sources.list.orig fi elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then if ! test -f /etc/apt/sources.list.d/official-package-repositories.list.orig; then sudo cp /etc/apt/sources.list.d/official-package-repositories.list /etc/apt/sources.list.d/official-package-repositories.list.orig fi fi } function checkIfOrigAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then if test -f /etc/apt/sources.list.orig; then echo "true" else echo "false" fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then if test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then echo "true" else echo "false" fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then if test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then echo "true" else echo "false" fi elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then if test -f /etc/apt/sources.list.orig; then echo "true" else echo "false" fi elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then if test -f /etc/apt/sources.list.d/official-package-repositories.list.orig; then echo "true" else echo "false" fi fi } function getDefaultAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then grep "^deb" /etc/apt/sources.list.orig | head -n 1 | cut -d" " -f2 elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then grep "^URIs" /etc/apt/sources.list.d/ubuntu.sources.orig | head -n 1 | cut -d" " -f2 elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then grep "^URIs" /etc/apt/sources.list.d/ubuntu.sources.orig | head -n 1 | cut -d" " -f2 elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then grep "^deb" /etc/apt/sources.list.orig | head -n 1 | cut -d" " -f2 elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then grep "^URIs" /etc/apt/sources.list.d/official-package-repositories.list.orig | head -n 1 | cut -d" " -f2 fi } function backupAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then sudo cp -f /etc/apt/sources.list /etc/apt/sources.list.backup elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then sudo cp -f /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.backup elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then sudo cp -f /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.backup elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then sudo cp -f /etc/apt/sources.list /etc/apt/sources.list.backup elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then sudo cp -f /etc/apt/sources.list.d/official-package-repositories.list /etc/apt/sources.list.d/official-package-repositories.list.backup fi } function removeBackupAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then sudo rm -f /etc/apt/sources.list.backup elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then sudo rm -f /etc/apt/sources.list.d/ubuntu.sources.backup elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then sudo rm -f /etc/apt/sources.list.d/ubuntu.sources.backup elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then sudo rm -f /etc/apt/sources.list.backup elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then sudo rm -f /etc/apt/sources.list.d/official-package-repositories.list.backup fi } function restoreBackupAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then sudo cp -f /etc/apt/sources.list.backup /etc/apt/sources.list elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.backup /etc/apt/sources.list.d/ubuntu.sources elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.backup /etc/apt/sources.list.d/ubuntu.sources elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then sudo cp -f /etc/apt/sources.list.backup /etc/apt/sources.list elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then sudo cp -f /etc/apt/sources.list.d/official-package-repositories.list.backup /etc/apt/sources.list.d/official-package-repositories.list fi } function restoreOrigAptSource() { if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then sudo cp -f /etc/apt/sources.list.orig /etc/apt/sources.list elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.orig /etc/apt/sources.list.d/ubuntu.sources elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.orig /etc/apt/sources.list.d/ubuntu.sources elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then sudo cp -f /etc/apt/sources.list.orig /etc/apt/sources.list elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then sudo cp -f /etc/apt/sources.list.d/official-package-repositories.list.orig /etc/apt/sources.list.d/official-package-repositories.list fi } function replaceAptMirror() { aptUrl="$1" if [ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]; then sudo sed -i "/security/! s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list sudo sed -i "/security/! s|deb-src [a-z]*://[^ ]* |deb-src ${aptUrl} |g" /etc/apt/sources.list elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then sudo sed -i "s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list sudo sed -i "s|deb-src [a-z]*://[^ ]* |deb-src ${aptUrl} |g" /etc/apt/sources.list elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then sudo sed -i "s|^URIs.*|URIs: ${aptUrl} |g" /etc/apt/sources.list.d/ubuntu.sources elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then sudo sed -i "s|^URIs.*|URIs: ${aptUrl} |g" /etc/apt/sources.list.d/ubuntu.sources elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then sudo sed -i "s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list sudo sed -i "s|deb-src [a-z]*://[^ ]* |deb-src ${aptUrl} |g" /etc/apt/sources.list elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then sudo sed -i "/linuxmint/! s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list.d/official-package-repositories.list fi } function checkUpdateAptSources() { isForceCheck="$1" set +e if [ "$IS_AUTO_INSTALL" = "true" ]; then return fi if ! [ "$(verifyAptSource)" = "true" ]; then showMessageBox "ERROR" "There was a problem with your apt sources list. Proceeding with current settings..." return fi if [ "$(checkIfOrigAptSource)" = "true" ] && ! [ "$isForceCheck" = "true" ]; then performAptUpdate false return fi if ! [ "$isForceCheck" = "true" ]; then showYesNoMessageBox "Find Mirrors" "Would you like to check for nearby apt mirrors? This process will take around 1-2 minutes to complete, but it will likely save a lot more time in the future when performing Linux host updates." if [ $? -ne 0 ]; then backupOrigAptSource performAptUpdate false return fi fi backupOrigAptSource default_apt_src="$(getDefaultAptSource)" backupAptSource echo "========================================================================" echo " Calculating the closest apt mirrors." echo " This could take a minute or two, please wait..." echo "========================================================================" mirrorResults="$(getFastestMirrors)" if [ -z "$mirrorResults" ]; then showMessageBox "ERROR" "There was a problem obtaining mirror info, continuing with current settings..." removeBackupAptSource performAptUpdate false return fi OLDIFS=$IFS IFS=$(echo -en "\n\b") mirrorResultsArr=($(echo "$mirrorResults")) IFS=$OLDIFS aptupdatemenu=$(cat << EOF $(getLogo) Select your desired apt sources mirror from the list below. To keep the current settings, select Cancel. EOF ) uas_menu_items=( --title "Select Mirror" --radiolist "$aptupdatemenu" $(($MENU_HEIGHT+2)) $MENU_WIDTH $MENU_INT_HEIGHT ) curNum=1 for curMirror in "${mirrorResultsArr[@]}" do uas_menu_items+=( "$(echo $curMirror | xargs)" ) uas_menu_items+=( "|" ) if [ $curNum -eq 1 ]; then uas_menu_items+=( "on" ) else uas_menu_items+=( "off" ) fi ((curNum++)) done while true; do selItem=$(whiptail "${uas_menu_items[@]}" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then restoreBackupAptSource removeBackupAptSource performAptUpdate false return fi selAptSource=$(echo "$selItem" | xargs | rev | cut -d" " -f1 | rev) replaceAptMirror "$selAptSource" tmpOutput=/tmp/aptOut.txt rm -f $tmpOutput performAptUpdate false 2>&1 | tee $tmpOutput grep -e "^Err:" -e "Failed" $tmpOutput > /dev/null 2>&1 chkRes=$? rm -f $tmpOutput if [ $chkRes -eq 0 ]; then errmenu=$(cat << EOF $(getLogo) There were some errors found with the selected mirror. Select Retry to select a different mirror, or select Revert to restore the original default. EOF ) restoreOrigAptSource if (whiptail --title "Error" --yesno "$errmenu" $MENU_HEIGHT $MENU_WIDTH --yes-button "Retry" --no-button "Revert" ); then continue else performAptUpdate false break fi else echo -e "========================================================================\n" echo -e "The selected mirror appears to be in good working order. (See above)\n" isBreakMainLoop=false while true; do echo "Enter 'confirm': To proceed with this option" echo " 'retry': To select a different mirror from the list" echo " 'revert': To restore the prior settings and continue" echo " 'orig': To restore the original default and continue" echo " 'exit': To restore the prior settings and exit the HSHQ utility" echo read -r -p "Your selection: " selMirrorOption if [ "$selMirrorOption" = "confirm" ]; then isBreakMainLoop=true break elif [ "$selMirrorOption" = "retry" ]; then restoreBackupAptSource isBreakMainLoop=false break elif [ "$selMirrorOption" = "revert" ]; then restoreBackupAptSource isBreakMainLoop=true performAptUpdate false break elif [ "$selMirrorOption" = "orig" ]; then restoreOrigAptSource isBreakMainLoop=true performAptUpdate false break elif [ "$selMirrorOption" = "exit" ]; then restoreBackupAptSource removeBackupAptSource performExitFunctions true exit else echo "Unknown response, please retry." fi done if [ "$isBreakMainLoop" = "true" ]; then break fi fi done removeBackupAptSource } function performAptUpdate() { clear isQuietUpdate="$1" if [ "$isQuietUpdate" = "true" ]; then echo "Performing apt update, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null else sudo DEBIAN_FRONTEND=noninteractive apt update fi } function getFastestMirrors() { # Some of the concepts of this function were adopted from # https://github.com/vegardit/fast-apt-mirror.sh/blob/v1/fast-apt-mirror.sh set +e distID="" last_modified_path="" if [ "$DISTRO_ID" = "debian" ]; then distID="debian" elif [ "$DISTRO_ID" = "ubuntu" ] || [ "$DISTRO_ID" = "linuxmint" ]; then distID="ubuntu" else # Fatal exit 1 fi cd ~ backupOrigAptSource default_mirror="$(getDefaultAptSource)" which netselect > /dev/null 2>&1 if [ $? -ne 0 ]; then netselect_version='0.3.ds1-30.1' wget http://ftp.debian.org/debian/pool/main/n/netselect/netselect_${netselect_version}_${DISTRO_ARCH}.deb > /dev/null 2>&1 sudo apt install ./netselect_${netselect_version}_${DISTRO_ARCH}.deb > /dev/null 2>&1 rm -f ./netselect_${netselect_version}_${DISTRO_ARCH}.deb fi rm -f /tmp/mirrors.txt outputMirrorsList OLDIFS=$IFS IFS=$(echo -en "\n\b") fastestMirrorsArr=($(sudo netselect -s50 -t4 $(grep "^$distID" /tmp/mirrors.txt | cut -d" " -f2) 2>/dev/null)) IFS=$OLDIFS rm -f /tmp/mirrors.txt mirrorsDomainList=$(echo "$default_mirror" | tr -s "/" | cut -d"/" -f2) mirrorsURLsList="$default_mirror" maxMirrors=15 curMirrorCount=1 curDTSeconds=$(date +%s) last_modified_path="ls-lR.gz" for curMirror in "${fastestMirrorsArr[@]}" do curURL=$(echo "$curMirror" | xargs | cut -d" " -f2) curDomain=$(echo "$curURL" | tr -s "/" | cut -d"/" -f2) echo "$mirrorsDomainList" | grep "$curDomain" > /dev/null 2>&1 if [ $? -ne 0 ]; then mirrorsDomainList="$mirrorsDomainList $curDomain" # Check last modified date lastModified=$(curl --max-time 3 -sSfL --head "$curURL${last_modified_path}" 2>/dev/null | grep -i "last-modified" | cut -d" " -f2- | LANG=C date -f- -u +%s) if ! [ -z "$lastModified" ] && [ "$(checkValidNumber $lastModified)" = "true" ] && [ $lastModified -gt 0 ]; then secsDiff=$(($curDTSeconds-$lastModified)) # 2592000 is equivalent to last 30 days if [ $secsDiff -ge 2592000 ]; then continue fi else continue fi mirrorsURLsList="$mirrorsURLsList\n$curURL" ((curMirrorCount++)) if [ $curMirrorCount -ge $maxMirrors ]; then break fi fi done echo -e "$mirrorsURLsList" | xargs -P 5 -i bash -c "curl -r 0-5120000 --max-time 5 -sf -w '%{speed_download} {}\n' -o /dev/null {}ls-lR.gz 2>/dev/null" | awk '$1 > 0' | awk '{printf "%.2f MB/s %s\n",$1/1048576,$2}' | sort -rg | head -n 7 } function outputMirrorsList() { # We'll just manage the list right here # to reduce the potential points of failure cat < /tmp/mirrors.txt debian http://ftp.am.debian.org/debian/ debian http://ftp.au.debian.org/debian/ debian http://ftp.at.debian.org/debian/ debian http://ftp.by.debian.org/debian/ debian http://ftp.be.debian.org/debian/ debian http://ftp.br.debian.org/debian/ debian http://ftp.bg.debian.org/debian/ debian http://ftp.ca.debian.org/debian/ debian http://ftp.cl.debian.org/debian/ debian http://ftp.cn.debian.org/debian/ debian http://ftp.hr.debian.org/debian/ debian http://ftp.cz.debian.org/debian/ debian http://ftp.dk.debian.org/debian/ debian http://ftp.fi.debian.org/debian/ debian http://ftp.fr.debian.org/debian/ debian http://ftp2.de.debian.org/debian/ debian http://ftp.de.debian.org/debian/ debian http://ftp.hk.debian.org/debian/ debian http://ftp.is.debian.org/debian/ debian http://ftp.it.debian.org/debian/ debian http://ftp.jp.debian.org/debian/ debian http://ftp.kr.debian.org/debian/ debian http://ftp.lt.debian.org/debian/ debian http://ftp.md.debian.org/debian/ debian http://ftp.nl.debian.org/debian/ debian http://ftp.nc.debian.org/debian/ debian http://ftp.nz.debian.org/debian/ debian http://ftp.no.debian.org/debian/ debian http://ftp.pl.debian.org/debian/ debian http://ftp.pt.debian.org/debian/ debian http://ftp.sk.debian.org/debian/ debian http://ftp.si.debian.org/debian/ debian http://ftp.es.debian.org/debian/ debian http://ftp.se.debian.org/debian/ debian http://ftp.ch.debian.org/debian/ debian http://ftp.tw.debian.org/debian/ debian http://ftp.tr.debian.org/debian/ debian http://ftp.uk.debian.org/debian/ debian http://ftp.us.debian.org/debian/ debian http://mirror.abmanagement.al/debian/ debian http://debian.unnoba.edu.ar/debian/ debian http://mirror.sitsa.com.ar/debian/ debian http://mirrors.asnet.am/debian/ debian http://debian.mirror.digitalpacific.com.au/debian/ debian http://debian.mirror.serversaustralia.com.au/debian/ debian http://mirror.aarnet.edu.au/debian/ debian http://mirror.amaze.com.au/debian/ debian http://mirror.gsl.icu/debian/ debian http://mirror.linux.org.au/debian/ debian http://mirror.overthewire.com.au/debian/ debian http://mirror.realcompute.io/debian/ debian http://mirrors.xtom.au/debian/ debian http://debian.anexia.at/debian/ debian http://debian.lagis.at/debian/ debian http://debian.mur.at/debian/ debian http://debian.sil.at/debian/ debian http://ftp.tu-graz.ac.at/mirror/debian/ debian http://mirror.alwyzon.net/debian/ debian http://mirror.ourhost.az/debian/ debian http://mirror.limda.net/debian/ debian http://mirror.xeonbd.com/debian/ debian http://ftp.byfly.by/debian/ debian http://mirror.datacenter.by/debian/ debian http://debian-mirror.behostings.net/debian/ debian http://ftp.belnet.be/debian/ debian http://mirror.as35701.net/debian/ debian http://mirror.unix-solutions.be/debian/ debian http://debian.c3sl.ufpr.br/debian/ debian http://debian.pop-sc.rnp.br/debian/ debian http://mirror.blue3.com.br/debian/ debian http://mirrors.ic.unicamp.br/debian/ debian http://mirror.uepg.br/debian/ debian http://mirror.ufscar.br/debian/ debian http://debian.a1.bg/debian/ debian http://debian.ipacct.com/debian/ debian http://debian.ludost.net/debian/ debian http://debian.mnet.bg/debian/ debian http://debian.telecoms.bg/debian/ debian http://ftp.uni-sofia.bg/debian/ debian http://mirrors.netix.net/debian/ debian http://mirror.telepoint.bg/debian/ debian http://mirror.sabay.com.kh/debian/ debian http://debian.linux.n0c.ca/debian/ debian http://debian.mirror.rafal.ca/debian/ debian http://mirror.csclub.uwaterloo.ca/debian/ debian http://mirror.dst.ca/debian/ debian http://mirror.estone.ca/debian/ debian http://mirror.it.ubc.ca/debian/ debian http://debian-mirror.puq.apoapsis.cl/debian/ debian http://debian.redlibre.cl/debian/ debian http://mirror.hnd.cl/debian/ debian http://mirror.insacom.cl/debian/ debian http://mirror.ufro.cl/debian/ debian http://mirror.lzu.edu.cn/debian/ debian http://mirror.nju.edu.cn/debian/ debian http://mirror.nyist.edu.cn/debian/ debian http://mirrors.163.com/debian/ debian http://mirrors.bfsu.edu.cn/debian/ debian http://mirrors.hit.edu.cn/debian/ debian http://mirrors.jlu.edu.cn/debian/ debian http://mirrors.neusoft.edu.cn/debian/ debian http://mirrors.tuna.tsinghua.edu.cn/debian/ debian http://mirrors.ustc.edu.cn/debian/ debian http://mirrors.zju.edu.cn/debian/ debian http://mirrors.ucr.ac.cr/debian/ debian http://debian.carnet.hr/debian/ debian http://debian.iskon.hr/debian/ debian http://debian.superhosting.cz/debian/ debian http://ftp.cvut.cz/debian/ debian http://ftp.debian.cz/debian/ debian http://ftp.sh.cvut.cz/debian/ debian http://ftp.zcu.cz/debian/ debian http://merlin.fit.vutbr.cz/debian/ debian http://mirror.dkm.cz/debian/ debian http://mirror.it4i.cz/debian/ debian http://mirrors.nic.cz/debian/ debian http://mirror.one.com/debian/ debian http://mirrors.dotsrc.org/debian/ debian http://mirrors.rackhosting.com/debian/ debian http://mirrors.xtom.ee/debian/ debian http://debian.web.trex.fi/debian/ debian http://www.nic.funet.fi/debian/ debian http://apt.tetaneutral.net/debian/ debian http://debian.apt-mirror.de/debian/ debian http://debian.mirrors.ovh.net/debian/ debian http://debian.obspm.fr/debian/ debian http://debian.polytech-lille.fr/debian/ debian http://debian.proxad.net/debian/ debian http://debian.univ-tlse2.fr/debian/ debian http://deb-mir1.naitways.net/debian/ debian http://deb.syxpi.fr/debian/ debian http://ftp.ec-m.fr/debian/ debian http://ftp.lip6.fr/pub/linux/distributions/debian/ debian http://ftp.rezopole.net/debian/ debian http://ftp.univ-pau.fr/linux/mirrors/debian/ debian http://ftp.u-picardie.fr/debian/ debian http://ftp.u-strasbg.fr/debian/ debian http://miroir.univ-lorraine.fr/debian/ debian http://mirror.debian.ikoula.com/debian/ debian http://mirror.gitoyen.net/debian/ debian http://mirror.ibcp.fr/debian/ debian http://mirror.johnnybegood.fr/debian/ debian http://mirror.plusserver.com/debian/debian/ debian http://mirrors.ircam.fr/pub/debian/ debian http://debian.grena.ge/debian/ debian http://debian.charite.de/debian/ debian http://debian.inf.tu-dresden.de/debian/ debian http://debian.intergenia.de/debian/ debian http://debian.mirror.iphh.net/debian/ debian http://debian.mirror.lrz.de/debian/ debian http://debian.netcologne.de/debian/ debian http://debian.tu-bs.de/debian/ debian http://de.mirrors.clouvider.net/debian/ debian http://drmirror.physi.uni-heidelberg.de/debian/ debian http://ftp.fau.de/debian/ debian http://ftp.gwdg.de/debian/ debian http://ftp.halifax.rwth-aachen.de/debian/ debian http://ftp.hosteurope.de/mirror/ftp.debian.org/debian/ debian http://ftp-stud.hs-esslingen.de/debian/ debian http://ftp.stw-bonn.de/debian/ debian http://ftp.tu-chemnitz.de/debian/ debian http://ftp.tu-clausthal.de/debian/ debian http://ftp.uni-bayreuth.de/debian/ debian http://ftp.uni-hannover.de/debian/debian/ debian http://ftp.uni-kl.de/debian/ debian http://ftp.uni-mainz.de/debian/ debian http://ftp.uni-stuttgart.de/debian/ debian http://ftp.wrz.de/debian/ debian http://mirror.23m.com/debian/ debian http://mirror.creoline.net/debian/ debian http://mirror.de.leaseweb.net/debian/ debian http://mirror.dogado.de/debian/ debian http://mirror.eu.oneandone.net/debian/ debian http://mirror.informatik.tu-freiberg.de/debian/ debian http://mirror.ipb.de/debian/ debian http://mirror.netzwerge.de/debian/ debian http://mirror.plusline.net/debian/ debian http://mirrors.xtom.de/debian/ debian http://mirror.united-gameserver.de/debian/ debian http://mirror.wtnet.de/debian/ debian http://packages.hs-regensburg.de/debian/ debian http://pubmirror.plutex.de/debian/ debian http://debian.otenet.gr/debian/ debian http://mirror.xtom.com.hk/debian/ debian http://ftp.bme.hu/debian/ debian http://repo.jztkft.hu/debian/ debian http://is.mirror.flokinet.net/debian/ debian http://mirror.cse.iitk.ac.in/debian/ debian http://mirror.nitc.ac.in/debian/ debian http://mirrors.iitd.ac.in/debian/ debian http://kartolo.sby.datautama.net.id/debian/ debian http://kebo.pens.ac.id/debian/ debian http://mirror.poliwangi.ac.id/debian/ debian http://mirror.unair.ac.id/debian/ debian http://mr.heru.id/debian/ debian http://mirror.aminidc.com/debian/ debian http://mirrors.pardisco.co/debian/ debian http://debian.interhost.co.il/debian/ debian http://debian.connesi.it/debian/ debian http://debian.dynamica.it/debian/ debian http://debian.mirror.garr.it/debian/ debian http://ftp.linux.it/debian/ debian http://giano.com.dist.unige.it/debian/ debian http://mirror.units.it/debian/ debian http://debian-mirror.sakura.ne.jp/debian/ debian http://ftp.jaist.ac.jp/debian/ debian http://ftp.kddilabs.jp/pub/debian/ debian http://ftp.nara.wide.ad.jp/debian/ debian http://ftp.riken.jp/Linux/debian/debian/ debian http://ftp.yz.yamagata-u.ac.jp/debian/ debian http://mirrors.xtom.jp/debian/ debian http://repo.jing.rocks/debian/ debian http://mirror.hoster.kz/debian/ debian http://mirror.ps.kz/debian/ debian http://debian.mirror.ac.ke/debian/ debian http://debian.mirror.liquidtelecom.com/debian/ debian http://ftp.kaist.ac.kr/debian/ debian http://ftp.lanet.kr/debian/ debian http://mirror.siwoo.org/debian/ debian http://debian.koyanet.lv/debian/ debian http://mirror.cloudhosting.lv/debian/ debian http://mirror.veesp.com/debian/ debian http://debian.mirror.vu.lt/debian/ debian http://mirror.litnet.lt/debian/ debian http://mirror.vpsnet.com/debian/ debian http://debian.mirror.root.lu/debian/ debian http://mirror.a1.mk/debian/ debian http://mirror-mk.interspace.com/debian/ debian http://lidsol.fi-b.unam.mx/debian/ debian http://mirror.as43289.net/debian/ debian http://mirror.marwan.ma/debian/ debian http://debian.snt.utwente.nl/debian/ debian http://mirror.ams.macarne.com/debian/ debian http://mirror.bgp.rodeo/debian/ debian http://mirror.duocast.net/debian/ debian http://mirror.i3d.net/debian/ debian http://mirror.nforce.com/debian/ debian http://mirror.nl.cdn-perfprod.com/debian/ debian http://mirror.nl.datapacket.com/debian/ debian http://mirror.nl.leaseweb.net/debian/ debian http://mirror.nl.mirhosting.net/debian/ debian http://mirrors.hostiserver.com/debian/ debian http://mirrors.xtom.nl/debian/ debian http://mirror.tngnet.com/debian/ debian http://mirror.vpgrp.io/debian/ debian http://nl.mirror.flokinet.net/debian/ debian http://nl.mirrors.clouvider.net/debian/ debian http://debian.nautile.nc/debian/ debian http://mirror.lagoon.nc/debian/ debian http://linux.purple-cat.net/debian/ debian http://mirror.fsmg.org.nz/debian/ debian http://ftp.uio.no/debian/ debian http://ftp.agh.edu.pl/debian/ debian http://ftp.psnc.pl/debian/ debian http://ftp.task.gda.pl/debian/ debian http://debian.uevora.pt/debian/ debian http://ftp.eq.uc.pt/software/Linux/debian/ debian http://ftp.rnl.tecnico.ulisboa.pt/pub/debian/ debian http://mirror.leitecastro.com/debian/ debian http://mirrors.upr.edu/debian/ debian http://debian.mithril.re/debian/ debian http://mirror.flo.c-f.ro/debian/ debian http://mirror.linux.ro/debian/ debian http://mirror.ro.cdn-perfprod.com/debian/ debian http://mirrors.hosterion.ro/debian/ debian http://mirrors.hostico.ro/debian/ debian http://mirrors.nav.ro/debian/ debian http://mirrors.nxthost.com/debian/ debian http://mirrors.pidginhost.com/debian/ debian http://ro.mirror.flokinet.net/debian/ debian http://ftp.psn.ru/debian/ debian http://mirror.corbina.net/debian/ debian http://mirror.docker.ru/debian/ debian http://mirror.hyperdedic.ru/debian/ debian http://mirror.kpfu.ru/debian/ debian http://mirrors.powernet.com.ru/debian/ debian http://mirror.truenetwork.ru/debian/ debian http://repository.su/debian/ debian http://mirror.pmf.kg.ac.rs/debian/ debian http://mirror.coganng.com/debian/ debian http://mirror.djvg.sg/debian/ debian http://mirror.sg.gs/debian/ debian http://ossmirror.mycloud.services/debian/ debian http://deb.bbxnet.sk/debian/ debian http://ftp.antik.sk/debian/ debian http://ftp.debian.sk/debian/ debian http://debian.mirror.ac.za/debian/ debian http://ftp.is.co.za/debian/ debian http://debian.grn.cat/debian/ debian http://debian.redimadrid.es/debian/ debian http://debian.redparra.com/debian/ debian http://debian.uvigo.es/debian/ debian http://ftp.caliu.cat/debian/ debian http://ftp.cica.es/debian/ debian http://ftp.udc.es/debian/ debian http://mirror.raiolanetworks.com/debian/ debian http://repo.ifca.es/debian/ debian http://softlibre.unizar.es/debian/ debian http://ulises.hostalia.com/debian/ debian http://debian.lth.se/debian/ debian http://debian.mirror.su.se/debian/ debian http://ftp.acc.umu.se/debian/ debian http://ftpmirror1.infania.net/debian/ debian http://mirror.braindrainlan.nu/debian/ debian http://mirrors.glesys.net/debian/ debian http://debian.ethz.ch/debian/ debian http://linuxsoft.cern.ch/debian/ debian http://mirror1.infomaniak.com/debian/ debian http://mirror2.infomaniak.com/debian/ debian http://mirror.init7.net/debian/ debian http://mirror.iway.ch/debian/ debian http://mirror.sinavps.ch/debian/ debian http://pkg.adfinis-on-exoscale.ch/debian/ debian http://debian.ccns.ncku.edu.tw/debian/ debian http://debian.csie.ntu.edu.tw/debian/ debian http://debian.cs.nycu.edu.tw/debian/ debian http://ftp.tku.edu.tw/debian/ debian http://mirror.twds.com.tw/debian/ debian http://opensource.nchc.org.tw/debian/ debian http://tw1.mirror.blendbyte.net/debian/ debian http://ftp.debianclub.org/debian/ debian http://mirror.applebred.net/debian/ debian http://mirror.kku.ac.th/debian/ debian http://mirrors.cloudforest.co.th/debian/ debian http://ftp.linux.org.tr/debian/ debian http://debian.netforce.hosting/debian/ debian http://debian.volia.net/debian/ debian http://distrohub.kyiv.ua/debian/ debian http://mirror.mirohost.net/debian/ debian http://mirror.ukrnames.com/debian/ debian http://debian.mirrors.uk2.net/debian/ debian http://debian.mirror.uk.sargasso.net/debian/ debian http://free.hands.com/debian/ debian http://ftp.ticklers.org/debian/ debian http://mirror.cov.ukservers.com/debian/ debian http://mirror.lchost.net/debian/ debian http://mirror.mythic-beasts.com/debian/ debian http://mirror.ox.ac.uk/debian/ debian http://mirror.positive-internet.com/debian/ debian http://mirrors.coreix.net/debian/ debian http://mirrorservice.org/sites/ftp.debian.org/debian/ debian http://mirror.vinehost.net/debian/ debian http://uk.mirrors.clouvider.net/debian/ debian http://atl.mirrors.clouvider.net/debian/ debian http://debian-archive.trafficmanager.net/debian/ debian http://debian.cc.lehigh.edu/debian/ debian http://debian.csail.mit.edu/debian/ debian http://debian.cs.binghamton.edu/debian/ debian http://debian.mirror.constant.com/debian/ debian http://debian.osuosl.org/debian/ debian http://debian.uchicago.edu/debian/ debian http://la.mirrors.clouvider.net/debian/ debian http://lethe.chinstrap.org/debian/ debian http://mirror.0x626b.com/debian/ debian http://mirror.cogentco.com/debian/ debian http://mirror.dal.nexril.net/debian/ debian http://mirror.keystealth.org/debian/ debian http://mirror.rustytel.net/debian/ debian http://mirrors.accretive-networks.net/debian/ debian http://mirrors.bloomu.edu/debian/ debian http://mirrors.bmcc.edu/debian/ debian http://mirrors.iu13.net/debian/ debian http://mirrors.lug.mtu.edu/debian/ debian http://mirrors.mwcampbell.us/debian/ debian http://mirrors.namecheap.com/debian/ debian http://mirrors.ocf.berkeley.edu/debian/ debian http://mirror.steadfast.net/debian/ debian http://mirrors.vcea.wsu.edu/debian/ debian http://mirrors.wikimedia.org/debian/ debian http://mirrors.xtom.com/debian/ debian http://mirror.timkevin.us/debian/ debian http://mirror.us.leaseweb.net/debian/ debian http://mirror.us.mirhosting.net/debian/ debian http://mirror.us.oneandone.net/debian/ debian http://nyc.mirrors.clouvider.net/debian/ debian http://plug-mirror.rcac.purdue.edu/debian/ debian http://repo.ialab.dsu.edu/debian/ debian http://us.mirror.ahrefs.org/debian/ debian http://debian.repo.cure.edu.uy/debian/ debian http://debian.xtdv.net/debian/ debian http://mirror.bizflycloud.vn/debian/ ubuntu http://mirror.sitsa.com.ar/ubuntu/ ubuntu http://ubuntu.zero.com.ar/ubuntu/ ubuntu http://ubuntu.unc.edu.ar/ubuntu/ ubuntu http://mirrors.asnet.am/ubuntu/ ubuntu http://ubuntu.mirror.gnc.am/ubuntu/ ubuntu http://mirrors.teamcloud.am/mirrors/ubuntu/ ubuntu http://mirror.aarnet.edu.au/pub/ubuntu/archive/ ubuntu http://mirror.internet.asn.au/pub/ubuntu/archive/ ubuntu http://mirror.datamossa.io/ubuntu/archive/ ubuntu http://mirror.realcompute.io/ubuntu/ ubuntu http://ubuntu.mirror.serversaustralia.com.au/ubuntu/ ubuntu http://ftp.iinet.net.au/pub/ubuntu/ ubuntu ftp://ftp.iinet.net.au/pub/ubuntu ubuntu http://mirror.as24220.net/pub/ubuntu-archive/ ubuntu ftp://mirror.as24220.net/pub/ubuntu-archive/ ubuntu http://mirror.as24220.net/pub/ubuntu/ ubuntu ftp://mirror.as24220.net/pub/ubuntu/ ubuntu http://mirror.internode.on.net/pub/ubuntu/ubuntu/ ubuntu ftp://mirror.internode.on.net/pub/ubuntu/ubuntu/ ubuntu http://mirror.netspace.net.au/pub/ubuntu/ ubuntu ftp://mirror.netspace.net.au/pub/ubuntu/ ubuntu http://mirror.overthewire.com.au/ubuntu/ ubuntu ftp://mirror.overthewire.com.au/ubuntu/ ubuntu http://mirror.alwyzon.net/ubuntu/ ubuntu http://mirror.easyname.at/ubuntu-archive/ ubuntu ftp://mirror.easyname.at/ubuntu-archive/ ubuntu http://ubuntu.anexia.at/ubuntu/ ubuntu http://mirror.kumi.systems/ubuntu/ ubuntu ftp://mirror.kumi.systems/ubuntu/ ubuntu http://mirror.kumi.systems/ubuntu-ports/ ubuntu ftp://mirror.kumi.systems/ubuntu-ports/ ubuntu http://ubuntu.lagis.at/ubuntu/ ubuntu ftp://ubuntu.lagis.at/ubuntu/ ubuntu http://mirror.datacenter.az/ubuntu/ ubuntu http://mirror.ourhost.az/ubuntu/ ubuntu http://aze.archive.ubuntu.com/ubuntu/ ubuntu ftp://aze.archive.ubuntu.com/ubuntu/ ubuntu http://mirror.yer.az/ubuntu/archive/ ubuntu http://mirror.gotmyhost.com/ubuntu-archive/ ubuntu http://mirror.limda.net/ubuntu-archive/ ubuntu http://mirror.xeonbd.com/ubuntu-archive/ ubuntu http://ftp.byfly.by/ubuntu/ ubuntu ftp://ftp.byfly.by/ubuntu/ ubuntu http://mirror.datacenter.by/ubuntu/ ubuntu ftp://mirror.datacenter.by/ubuntu/ ubuntu http://ftp.belnet.be/ubuntu/ ubuntu ftp://ftp.belnet.be/ubuntu/ ubuntu http://mirror.unix-solutions.be/ubuntu/ ubuntu http://mirrors-ubuntu.behostings.com/ubuntu/ ubuntu http://archive.ubuntu.mirror.ba/ubuntu/ ubuntu http://ubuntu-mirror.bhtelecom.ba/ubuntu/ ubuntu http://mirror.uepg.br/ubuntu/ ubuntu http://ubuntu.c3sl.ufpr.br/ubuntu/ ubuntu ftp://ubuntu.c3sl.ufpr.br/ubuntu/ ubuntu http://ubuntu.mirror.letscloud.io/ ubuntu http://mirror.ufam.edu.br/ubuntu/ ubuntu http://mirror.ufscar.br/ubuntu/ ubuntu http://mirror.unesp.br/ubuntu/ ubuntu http://sft.if.usp.br/ubuntu/ ubuntu ftp://sft.if.usp.br/ubuntu/ ubuntu http://ubuntu-archive.locaweb.com.br/ubuntu/ ubuntu http://ubuntu.letscloud.io/ubuntu/ ubuntu http://ubuntu.mti.mt.gov.br/ ubuntu http://mirror.telepoint.bg/ubuntu/ ubuntu ftp://mirror.telepoint.bg/ubuntu/ ubuntu http://mirrors.neterra.net/ubuntu/archive/ ubuntu ftp://mirrors.neterra.net/ubuntu/archive/ ubuntu http://mirrors.netix.net/ubuntu/archive/ ubuntu ftp://mirrors.netix.net/ubuntu/archive/ ubuntu http://ubuntu.ipacct.com/ubuntu/ ubuntu ftp://ubuntu.ipacct.com/ubuntu/ ubuntu http://mirrors.storpool.com/ubuntu/archive/ ubuntu http://ubuntu.linux-bg.org/ubuntu/ ubuntu ftp://ubuntu.linux-bg.org/ubuntu/ ubuntu http://mirror.bg.host.ag/ubuntu/ ubuntu ftp://mirror.bg.host.ag/ubuntu/ ubuntu http://mirror.sabay.com.kh/ubuntu/ ubuntu http://mirrors.layeronline.com/ubuntu/ ubuntu http://ubuntu.linux.n0c.ca/ubuntuarchive/ ubuntu http://mirror.csclub.uwaterloo.ca/ubuntu/ ubuntu ftp://mirror.csclub.uwaterloo.ca/ubuntu/ ubuntu http://mirror.it.ubc.ca/ubuntu/ ubuntu http://mirror.reenigne.net/ubuntu/ ubuntu http://muug.ca/mirror/ubuntu/ ubuntu ftp://ftp.muug.ca/mirror/ubuntu/ ubuntu http://mirror.0xem.ma/ubuntu/ ubuntu http://mirror.hep.gg/ubuntu/ ubuntu http://archive.ubuntu.mirror.rafal.ca/ubuntu/ ubuntu ftp://archive.ubuntu.mirror.rafal.ca/ubuntu/ ubuntu http://gpl.savoirfairelinux.net/pub/mirrors/ubuntu/ ubuntu http://mirror.ca-tr.kamatera.com/ubuntu/ ubuntu http://mirror.rcg.sfu.ca/mirror/ubuntu/ ubuntu http://ubuntu.mirror.globo.tech/ ubuntu ftp://ubuntu.mirror.globo.tech/ ubuntu http://mirror.its.dal.ca/ubuntu/ ubuntu ftp://mirror.its.dal.ca/ubuntu/ ubuntu http://ubuntu.mirror.rafal.ca/ubuntu/ ubuntu http://mirror.hnd.cl/ubuntu/ ubuntu http://ubuntu-mirror.puq.apoapsis.cl/ubuntu/ ubuntu http://mirror.uchile.cl/ubuntu/ ubuntu http://elmirror.cl/ubuntu/ ubuntu http://mirror.ufro.cl/ubuntu/ ubuntu http://mirror1.grupocg.cl/ubuntu/ ubuntu http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ubuntu http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ ubuntu http://mirror.nju.edu.cn/ubuntu/ ubuntu http://mirror.nyist.edu.cn/ubuntu/ ubuntu http://mirror.nyist.edu.cn/ubuntu-ports/ ubuntu http://mirrors.ustc.edu.cn/ubuntu/ ubuntu http://mirrors.sdu.edu.cn/ubuntu/ ubuntu http://repo.huaweicloud.com/ubuntu/ ubuntu http://ftp.sjtu.edu.cn/ubuntu/ ubuntu ftp://ftp.sjtu.edu.cn/ubuntu/ ubuntu http://mirror.bjtu.edu.cn/ubuntu/ ubuntu http://mirrors.cn99.com/ubuntu/ ubuntu http://mirrors.huaweicloud.com/repository/ubuntu/ ubuntu http://mirrors.hust.edu.cn/ubuntu/ ubuntu http://mirrors.jcut.edu.cn/ubuntu/ ubuntu http://mirrors.jlu.edu.cn/ubuntu/ ubuntu http://mirrors.jxust.edu.cn/ubuntu/ ubuntu http://mirrors.njupt.edu.cn/ubuntu/ ubuntu http://mirrors.sjtug.sjtu.edu.cn/ubuntu/ ubuntu http://mirrors.sohu.com/ubuntu/ ubuntu ftp://mirrors.sohu.com/ubuntu/ ubuntu http://mirrors.xjtu.edu.cn/ubuntu/ ubuntu http://mirrors.yun-idc.com/ubuntu/ ubuntu ftp://mirrors.yun-idc.com/ubuntu/ ubuntu http://mirrors.zju.edu.cn/ubuntu/ ubuntu http://mirrors.cqu.edu.cn/ubuntu/ ubuntu http://edgeuno-bog2.mm.fcix.net/ubuntu/ ubuntu http://mirror.unimagdalena.edu.co/ubuntu/ ubuntu http://mirrors.atlas.net.co/ubuntu/ ubuntu http://mirrors.united.cd/ubuntu/ ubuntu http://ubuntu.ucr.ac.cr/ubuntu/ ubuntu ftp://ubuntu.ucr.ac.cr/ubuntu/ ubuntu http://ubuntu.grad.hr/ubuntu/ ubuntu ftp://ubuntu.grad.hr/ubuntu/ ubuntu http://mirror.library.ucy.ac.cy/linux/ubuntu/archive/ ubuntu http://cz.archive.ubuntu.com/ubuntu/ ubuntu http://ftp.linux.cz/pub/linux/ubuntu/ ubuntu ftp://ftp.linux.cz/pub/linux/ubuntu/ ubuntu http://ftp.sh.cvut.cz/ubuntu/ ubuntu ftp://ftp.sh.cvut.cz/ubuntu/ ubuntu http://mirror.it4i.cz/ubuntu/ ubuntu http://ftp.cvut.cz/ubuntu/ ubuntu ftp://ftp.cvut.cz/ubuntu/ ubuntu http://ucho.ignum.cz/ubuntu/ ubuntu ftp://ucho.ignum.cz/ubuntu/ ubuntu http://mirrors.dotsrc.org/ubuntu/ ubuntu ftp://mirrors.dotsrc.org/ubuntu/ ubuntu http://mirrors.fibianet.dk/ubuntu/ ubuntu http://mirror.group.one/ubuntu/ ubuntu http://mirror.one.com/ubuntu/ ubuntu ftp://mirror.one.com/ubuntu/ ubuntu http://ftp.klid.dk/ftp/ubuntu/ ubuntu ftp://ftp.klid.dk/ubuntu/ ubuntu http://mirror.cedia.org.ec/ubuntu/ ubuntu ftp://mirror.cedia.org.ec/ubuntu/ ubuntu http://mirrors.xtom.ee/ubuntu/ ubuntu http://mirror.5i.fi/ubuntu/ ubuntu http://ubuntu.web.trex.fi/ubuntu/ ubuntu ftp://mirror.clientvps.com/ubuntu/ ubuntu http://mirror.fi.ossplanet.net/ubuntu/ ubuntu http://mirror.ubuntu.ikoula.com/ ubuntu http://ubuntu.lafibre.info/ubuntu/ ubuntu http://ubuntu.univ-reims.fr/ubuntu/ ubuntu ftp://ubuntu.univ-reims.fr/ubuntu/ ubuntu http://ubuntu.mirrors.ovh.net/ubuntu/ ubuntu http://mirror.plusserver.com/ubuntu/ubuntu/ ubuntu http://miroir.univ-lorraine.fr/ubuntu/ ubuntu http://mirror.its-tps.fr/ubuntu/ ubuntu http://mirror.johnnybegood.fr/ubuntu/ ubuntu http://mirrors.ircam.fr/pub/ubuntu/archive/ ubuntu ftp://mirrors.ircam.fr/pub/ubuntu/archive/ ubuntu http://ubuntu.syxpi.fr/ubuntu/ ubuntu http://ubuntu.univ-nantes.fr/ubuntu/ ubuntu ftp://ubuntu.univ-nantes.fr/ubuntu/ ubuntu http://ftp.u-picardie.fr/mirror/ubuntu/ubuntu/ ubuntu http://distrib-coffee.ipsl.jussieu.fr/pub/linux/ubuntu/ ubuntu ftp://distrib-coffee.ipsl.jussieu.fr/pub/linux/ubuntu/ ubuntu http://ubuntu.grena.ge/ubuntu/ ubuntu http://ftp.uni-stuttgart.de/ubuntu/ ubuntu http://ftp.halifax.rwth-aachen.de/ubuntu/ ubuntu ftp://ftp.halifax.rwth-aachen.de/ubuntu/ ubuntu http://mirror.dogado.de/ubuntu/ ubuntu http://mirror.netcologne.de/ubuntu/ ubuntu ftp://mirror.netcologne.de/ubuntu/ ubuntu http://ubuntu.mirror.lrz.de/ubuntu/ ubuntu http://de.mirrors.clouvider.net/ubuntu/ ubuntu http://debian.charite.de/ubuntu/ ubuntu http://ftp.stw-bonn.de/ubuntu/ ubuntu ftp://ftp.stw-bonn.de/ubuntu/ ubuntu http://mirror.creoline.net/ubuntu/ ubuntu http://mirror.de.leaseweb.net/ubuntu/ ubuntu ftp://mirror.de.leaseweb.net/ubuntu/ ubuntu http://mirror.ipb.de/ubuntu/ ubuntu http://mirror.wtnet.de/ubuntu/ ubuntu http://mirrors.xtom.de/ubuntu/ ubuntu http://ubuntu.mirror.tudos.de/ubuntu/ ubuntu http://ftp.fau.de/ubuntu/ ubuntu ftp://ftp.fau.de/ubuntu/ ubuntu http://drmirror.physi.uni-heidelberg.de/ubuntu/ ubuntu http://ftp-stud.hs-esslingen.de/ubuntu/ ubuntu ftp://ftp-stud.hs-esslingen.de/ubuntu/ ubuntu http://mirror.netzwerge.de/ubuntu/ ubuntu http://mirror.serverloft.eu/ubuntu/ubuntu/ ubuntu http://artfiles.org/ubuntu.com/ ubuntu ftp://artfiles.org/ubuntu.com/ ubuntu ftp://ftp.fu-berlin.de/linux/ubuntu/ ubuntu http://ftp.rz.tu-bs.de/pub/mirror/ubuntu-packages/ ubuntu ftp://ftp.rz.tu-bs.de/pub/mirror/ubuntu-packages/ ubuntu http://ftp.tu-chemnitz.de/pub/linux/ubuntu-ports/ ubuntu ftp://ftp.tu-chemnitz.de/pub/linux/ubuntu-ports/ ubuntu http://ftp.tu-ilmenau.de/mirror/ubuntu/ ubuntu ftp://ftp.tu-ilmenau.de/mirror/ubuntu/ ubuntu http://ftp.uni-bayreuth.de/linux/ubuntu/ubuntu/ ubuntu ftp://ftp.uni-bayreuth.de/pub/linux/ubuntu/ubuntu/ ubuntu http://ftp.uni-mainz.de/ubuntu/ ubuntu ftp://ftp.uni-mainz.de/ubuntu/ ubuntu http://ftp5.gwdg.de/pub/linux/debian/ubuntu/ ubuntu ftp://ftp5.gwdg.de/pub/linux/debian/ubuntu/ ubuntu http://mirror.23m.com/ubuntu/ ubuntu http://mirror.daniel-jost.net/ubuntu/ ubuntu ftp://mirror.daniel-jost.net/ubuntu/ ubuntu http://mirror.eu-fr.kamatera.com/ubuntu/ ubuntu http://mirror.informatik.tu-freiberg.de/ubuntu/ ubuntu http://mirror.kamp.de/ubuntu/ ubuntu http://mirror.kkg.berlin/ubuntu/ ubuntu http://mirror.liquidrp.de/ubuntu/ ubuntu http://mirror.scaleuptech.com/ubuntu/ ubuntu ftp://mirror.scaleuptech.com/ubuntu/ ubuntu http://mirror.thunderhosting.cloud/ubuntu/ ubuntu http://mirror.united-gameserver.de/ubuntu/ ubuntu http://packages.oth-regensburg.de/ubuntu/ ubuntu http://suse.uni-leipzig.de/pub/releases.ubuntu.com/ubuntu/ ubuntu ftp://suse.uni-leipzig.de/pub/releases.ubuntu.com/ubuntu/ ubuntu http://ftp.hosteurope.de/mirror/archive.ubuntu.com/ ubuntu ftp://ftp.hosteurope.de/mirror/archive.ubuntu.com/ ubuntu http://ftp.tu-chemnitz.de/pub/linux/ubuntu/ ubuntu http://ftp.cc.uoc.gr/mirrors/linux/ubuntu/packages/ ubuntu ftp://ftp.cc.uoc.gr/mirrors/linux/ubuntu/packages/ ubuntu http://ftp.ntua.gr/ubuntu/ ubuntu ftp://ftp.ntua.gr/ubuntu/ ubuntu http://ubuntu.otenet.gr/ ubuntu ftp://ftp.otenet.gr/ubuntu/ ubuntu http://mirror.greennet.gl/ubuntu/ ubuntu http://hk.mirrors.thegigabit.com/ubuntu/ ubuntu http://mirror-hk.koddos.net/ubuntu/ ubuntu http://mirror.01link.hk/ubuntu/ ubuntu http://mirror.as.kamatera.com/ubuntu/ ubuntu http://repo.jztkft.hu/ubuntu/ ubuntu http://mirror.niif.hu/ubuntu/ ubuntu http://quantum-mirror.hu/mirrors/pub/ubuntu/ ubuntu http://mirrors.sth.sze.hu/ubuntu/ ubuntu http://ubuntu.hysing.is/ubuntu/ ubuntu http://is.mirror.flokinet.net/ubuntu/ ubuntu http://mirrors.opensource.is/ubuntu/ ubuntu http://repos.del.extreme-ix.org/ubuntu/ ubuntu http://in.mirror.coganng.com/ubuntu/ ubuntu ftp://in.mirror.coganng.com/ubuntu/ ubuntu http://in.mirror.coganng.com/ubuntu-ports/ ubuntu ftp://in.mirror.coganng.com/ubuntu-ports/ ubuntu http://mirror.bharatdatacenter.com/ubuntu/ ubuntu ftp://mirror.bharatdatacenter.com/ubuntu/ ubuntu http://mirror.cse.iitk.ac.in/ubuntu/ ubuntu ftp://mirror.cse.iitk.ac.in/ubuntu/ ubuntu http://mirror.ietlucknow.ac.in/ubuntu/ ubuntu http://mirror.nitc.ac.in/ubuntu/ ubuntu http://mirrors.nxtgen.com/ubuntu-mirror/ubuntu/ ubuntu http://repo.extreme-ix.org/ubuntu/ ubuntu http://ftp.iitm.ac.in/ubuntu/ ubuntu ftp://ftp.iitm.ac.in/ubuntu ubuntu http://cdn.repo.cloudeka.id/ubuntu/ ubuntu http://cdn.ubuntu.repo.cloudeka.id/ubuntu/ ubuntu http://linux.domainesia.com/ubuntu/ubuntu-archive/ ubuntu http://mirror.faizuladib.com/ubuntu/ ubuntu http://mirror.repository.id/ubuntu/ ubuntu http://mr.heru.id/ubuntu/ ubuntu http://mirror.deace.id/ubuntu/ ubuntu http://mirror-gw.elastis.id/ubuntu/ ubuntu http://mirror.biznetgio.com/ubuntu/ ubuntu http://mirror.cepatcloud.id/ubuntu/ ubuntu http://mirror.citrahost.com/ubuntu/ ubuntu http://mirror.dewabiz.com/ubuntu/ ubuntu http://mirror.gi.co.id/ubuntu/ ubuntu http://mirror.papua.go.id/ubuntu/ ubuntu http://mirror.poliwangi.ac.id/ubuntu/ ubuntu http://mirror.unair.ac.id/ubuntu/ ubuntu http://repo.ugm.ac.id/ubuntu/ ubuntu http://repo.usk.ac.id/ubuntu/ ubuntu http://suro.ubaya.ac.id/ubuntu/ ubuntu ftp://suro.ubaya.ac.id/mirror/ubuntu/ ubuntu http://archive.ubuntu.mirrors.zagrio.net/ ubuntu ftp://mirrors.zagrio.net/ubuntu/archive/ ubuntu http://mirror.iranserver.com/ubuntu/ ubuntu http://ir.archive.ubuntu.com/ubuntu/ ubuntu ftp://ir.archive.ubuntu.com/ubuntu/ ubuntu http://ubuntu.mobinhost.com/ubuntu/ ubuntu ftp://ubuntu.mobinhost.com/ubuntu/ ubuntu http://ubuntu.pishgaman.net/ubuntu/ ubuntu http://mirror.aminidc.com/ubuntu/ ubuntu ftp://mirror.aminidc.com/ubuntu/ ubuntu http://mirrors.pardisco.co/ubuntu/ ubuntu http://archive.ubuntu.petiak.ir/ubuntu/ ubuntu ftp://archive.ubuntu.petiak.ir/ubuntu/ ubuntu http://ubuntu.pars.host/ ubuntu http://ubuntu.parsvds.com/ubuntu/ ubuntu http://mirror.faraso.org/ubuntu/ ubuntu http://mirrors.ubuntu.dimit.cloud/ ubuntu http://repo.iut.ac.ir/repo/Ubuntu/ ubuntu http://mirror.webworld.ie/ubuntu/ ubuntu http://mirror.il-ha.kamatera.com/ubuntu/ ubuntu http://ubuntu-archive.interhost.co.il/ubuntu/ ubuntu http://mirror.il-pt.kamatera.com/ubuntu/ ubuntu http://mirror.il-rh.kamatera.com/ubuntu/ ubuntu http://mirror.il-ta.kamatera.com/ubuntu/ ubuntu http://mirror.il.kamatera.com/ubuntu/ ubuntu http://rep-ubuntu-il.upress.io/ubuntu/ ubuntu http://mirror.isoc.org.il/pub/ubuntu/ ubuntu ftp://mirror.isoc.org.il/ubuntu/ ubuntu http://it1.mirror.vhosting-it.com/ubuntu/ ubuntu http://it2.mirror.vhosting-it.com/ubuntu/ ubuntu http://giano.com.dist.unige.it/ubuntu/ ubuntu http://ftp.udx.icscoe.jp/Linux/ubuntu/ ubuntu http://mirror.hashy0917.net/ubuntu/ ubuntu http://mirror.hashy0917.net/ubuntu-ports/ ubuntu http://archive.g4t1.pro/ubuntu/ ubuntu http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu/ ubuntu http://linux.yz.yamagata-u.ac.jp/ubuntu/ ubuntu http://mirror.nishi.network/ubuntu/ ubuntu http://mirror.nishi.network/ubuntu-ports/ ubuntu http://repo.jing.rocks/ubuntu/ ubuntu http://repo.jing.rocks/ubuntu-ports/ ubuntu http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ ubuntu ftp://ftp.jaist.ac.jp/pub/Linux/ubuntu/ ubuntu http://ftp.riken.jp/Linux/ubuntu/ ubuntu ftp://ftp.riken.jp/Linux/ubuntu/ ubuntu http://ubuntutym.u-toyama.ac.jp/ubuntu/ ubuntu http://ftp.sakura.ad.jp/ubuntu/ ubuntu http://jp.mirror.coganng.com/ubuntu/ ubuntu ftp://jp.mirror.coganng.com/ubuntu/ ubuntu http://jp.mirror.coganng.com/ubuntu-ports/ ubuntu ftp://jp.mirror.coganng.com/ubuntu-ports/ ubuntu http://mirror.hoster.kz/ubuntu/ ubuntu ftp://mirror.hoster.kz/ubuntu/ ubuntu http://mirror.ps.kz/ubuntu/ ubuntu ftp://mirror.ps.kz/ubuntu/ ubuntu http://mirror.amuksa.com/ubuntu/ ubuntu http://mirror.kakao.com/ubuntu/ ubuntu http://mirror.krfoss.org/ubuntu/ ubuntu http://mirror.siwoo.org/ubuntu/ ubuntu http://ftp.daum.net/ubuntu/ ubuntu ftp://ftp.daum.net/ubuntu/ ubuntu http://ftp.lanet.kr/ubuntu/ ubuntu ftp://ftp.lanet.kr/ubuntu/ ubuntu http://ftp.lanet.kr/ubuntu-ports/ ubuntu ftp://ftp.lanet.kr/ubuntu-ports/ ubuntu http://mirror.jeonnam.school/ubuntu/ ubuntu http://mirror.zzunipark.com/ubuntu/ ubuntu http://mirror.zzunipark.com/ubuntu-ports/ ubuntu http://mirror.yuki.net.uk/ubuntu-ports/ ubuntu http://mir.linux.kg/ubuntu/ ubuntu http://mirror.cloudhosting.lv/ubuntu/ ubuntu http://ubuntu-arch.linux.edu.lv/ubuntu/ ubuntu ftp://ubuntu-arch.linux.edu.lv/lv.archive.ubuntu.com/ubuntu/ ubuntu http://ubuntu.koyanet.lv/ubuntu/ ubuntu http://mirror.bacloud.com/ubuntu-mirror/archive/ ubuntu http://mirror.vpsnet.com/ubuntu/ ubuntu http://ubuntu.mirror.vu.lt/ubuntu/ ubuntu ftp://ubuntu.mirror.vu.lt/ubuntu/ ubuntu http://ftp.litnet.lt/ubuntu/ ubuntu ftp://ftp.litnet.lt/ubuntu/ ubuntu http://ubuntu.mirror.root.lu/ubuntu/ ubuntu ftp://mirror.root.lu/ubuntu/ubuntu/ ubuntu http://mirror.a1.mk/ubuntu/ ubuntu http://mirror.t-home.mk/ubuntu/ ubuntu http://ubuntu.dts.mg/ubuntu/ ubuntu http://mirrors.gbnetwork.com/ubuntu/ ubuntu http://mirrors.ipserverone.com/ubuntu/ ubuntu http://ubuntu.mirror.thegigabit.com/ ubuntu http://ubuntu.tuxuri.com/ubuntu/ ubuntu http://ubuntu-mirror.cloud.mu/ubuntu/ ubuntu http://ubuntu-mirror.cloud.mu/ubuntu-ports/ ubuntu http://mirror.as43289.net/ubuntu/ ubuntu ftp://mirror.as43289.net/ubuntu/ ubuntu http://mirror.ihost.md/ubuntu/ ubuntu http://mirrors.mivocloud.com/ubuntu/ ubuntu ftp://mirrors.mivocloud.com/ubuntu/ ubuntu http://mirror.hosthink.net/ubuntu/ ubuntu http://mirror.datacenter.mn/ubuntu/ ubuntu http://mirror.marwan.ma/ubuntu/ ubuntu http://mirrors.ygn.mmix.net.mm/ubuntu/ ubuntu http://download.nust.na/pub/ubuntu/ubuntu/ ubuntu http://ubuntu.ntc.net.np/ubuntu/ ubuntu http://mirror.ams.macarne.com/ubuntu/ ubuntu http://mirror.i3d.net/pub/ubuntu/ ubuntu http://mirror.nl.datapacket.com/ubuntu/ ubuntu http://nl.archive.ubuntu.com/ubuntu/ ubuntu http://ftp.nluug.nl/os/Linux/distr/ubuntu/ ubuntu ftp://ftp.nluug.nl/pub/os/Linux/distr/ubuntu/ ubuntu http://ftp.snt.utwente.nl/pub/os/linux/ubuntu/ ubuntu ftp://ftp.snt.utwente.nl/pub/os/linux/ubuntu/ ubuntu http://mirror.lyrahosting.com/ubuntuarchive/ ubuntu ftp://mirror.lyrahosting.com/ubuntuarchive/ ubuntu http://mirror.nforce.com/pub/linux/ubuntu/ ubuntu ftp://mirror.nforce.com/pub/linux/ubuntu/ ubuntu http://mirror.nl.altushost.com/ubuntu/ ubuntu http://mirror.nl.leaseweb.net/ubuntu/ ubuntu ftp://mirror.nl.leaseweb.net/ubuntu/ ubuntu http://mirror.serverion.com/ubuntu/ ubuntu ftp://mirror.serverion.com/ubuntu/ ubuntu http://mirror.vpgrp.io/ubuntu/ ubuntu http://mirrors.xtom.nl/ubuntu/ ubuntu http://nl.mirrors.clouvider.net/ubuntu/ ubuntu http://ubuntu.mirror.intermax.nl/ ubuntu http://ubuntu.mirror.true.nl/ubuntu/ ubuntu http://ubuntu.mirror.wearetriple.com/archive/ ubuntu http://mirror.transip.net/ubuntu/ubuntu/ ubuntu ftp://mirror.transip.net/ubuntu/ubuntu/ ubuntu http://mirrors.as215248.net/ubuntu/ ubuntu http://nl3.archive.ubuntu.com/ubuntu/ ubuntu http://ftp.tudelft.nl/archive.ubuntu.com/ ubuntu ftp://ftp.tudelft.nl/pub/Linux/archive.ubuntu.com/ ubuntu http://mirror.eu.kamatera.com/ubuntu/ ubuntu http://mirror.hostnet.nl/ubuntu/archive/ ubuntu http://mirror.nl.as215296.net/ubuntu/ ubuntu http://mirror.previder.nl/ubuntu/ ubuntu http://mirrors.evoluso.com/ubuntu/ ubuntu http://nl.mirror.flokinet.net/ubuntu/ ubuntu http://osmirror.rug.nl/ubuntu/ ubuntu ftp://osmirror.rug.nl/ubuntu/ ubuntu ftp://ftpserv.tudelft.nl/pub/Linux/archive.ubuntu.com/ ubuntu http://ubuntu.lagoon.nc/ubuntu/ ubuntu ftp://ubuntu.lagoon.nc/ubuntu/ ubuntu http://archive.ubuntu.nautile.nc/ubuntu/ ubuntu http://mirror.fsmg.org.nz/ubuntu/ ubuntu http://ubuntu.btit.nz/ubuntu/ ubuntu ftp://ubuntu.btit.nz/ubuntu/ ubuntu http://no.mirrors.blix.com/ubuntu/ ubuntu http://no.archive.ubuntu.com/ubuntu/ ubuntu http://ubuntu.hi.no/archive/ ubuntu http://ftp.uninett.no/ubuntu/ ubuntu ftp://ftp.uninett.no/ubuntu/ ubuntu http://ubuntu.uib.no/archive/ ubuntu ftp://ubuntu.uib.no/pub/Linux/Distributions/ubuntu/archive/ ubuntu http://mirror.virtury.com/ubuntu/ ubuntu http://mirror.rise.ph/ubuntu/ ubuntu ftp://mirror.rise.ph/ubuntu/ ubuntu http://ftp.icm.edu.pl/pub/Linux/ubuntu/ ubuntu ftp://ftp.icm.edu.pl/pub/Linux/ubuntu/ ubuntu http://ftp.psnc.pl/linux/ubuntu/ ubuntu http://ubuntu.task.gda.pl/ubuntu/ ubuntu ftp://ubuntu.task.gda.pl/ubuntu/ ubuntu http://ftp.agh.edu.pl/ubuntu/ ubuntu ftp://ftp.agh.edu.pl/ubuntu/ ubuntu http://mirror.chmuri.net/ubuntu/ ubuntu http://ubuntu.man.lodz.pl/ubuntu/ ubuntu ftp://ubuntu.man.lodz.pl/ubuntu/ ubuntu http://mirror.leitecastro.com/ubuntu/ ubuntu http://mirrors.ptisp.pt/ubuntu/ ubuntu http://mirrors.up.pt/ubuntu/ ubuntu ftp://mirrors.up.pt/pub/ubuntu/ ubuntu http://cesium.di.uminho.pt/pub/ubuntu-archive/ ubuntu ftp://cesium.di.uminho.pt/pub/ubuntu-archive/ ubuntu http://archive.ubuntumirror.dei.uc.pt/ubuntu/ ubuntu ftp://archive.ubuntumirror.dei.uc.pt/ubuntu/ ubuntu http://ftp.rnl.tecnico.ulisboa.pt/pub/ubuntu/archive/ ubuntu ftp://ftp.rnl.tecnico.ulisboa.pt/pub/ubuntu/archive/ ubuntu http://glua.ua.pt/pub/ubuntu/ ubuntu ftp://glua.ua.pt/pub/ubuntu/ ubuntu http://mirrors.webhs.pt/ubuntu/ ubuntu http://mirrors.hosterion.ro/ubuntu/ ubuntu http://mirror.efect.ro/ubuntu/archive/ ubuntu http://mirrors.nav.ro/ubuntu/ ubuntu ftp://mirrors.nav.ro/ubuntu/ ubuntu http://mirror.flo.c-f.ro/ubuntu/ ubuntu http://mirror.flokinet.net/ubuntu/ ubuntu http://mirrors.chroot.ro/ubuntu/ ubuntu ftp://mirrors.chroot.ro/ubuntu/ ubuntu http://mirrors.hostico.ro/ubuntu/archive/ ubuntu http://mirrors.pidginhost.com/ubuntu/ ubuntu ftp://mirrors.pidginhost.com/ubuntu/ ubuntu http://ubuntu.mirrors.linux.ro/archive/ ubuntu ftp://ftp.linux.ro/ubuntu/archive/ ubuntu http://mirror.linux-ia64.org/ubuntu/ ubuntu http://mirror.neftm.ru/ubuntu/ ubuntu http://mirror.timeweb.ru/ubuntu/ ubuntu http://mirror.truenetwork.ru/ubuntu/ ubuntu ftp://mirror.truenetwork.ru/ubuntu/ ubuntu http://mirror.docker.ru/ubuntu/ ubuntu http://mirror.corbina.net/ubuntu/ ubuntu ftp://mirror.corbina.net/ubuntu/ ubuntu http://mirror.hyperdedic.ru/ubuntu/ ubuntu http://mirror.yandex.ru/ubuntu/ ubuntu ftp://mirror.yandex.ru/ubuntu/ ubuntu http://mirrors.powernet.com.ru/ubuntu/ ubuntu ftp://mirrors.powernet.com.ru/pub/ubuntu/archive/ ubuntu http://mirror.logol.ru/ubuntu/ ubuntu ftp://mirror.logol.ru/ubuntu/ ubuntu http://mirror.maeen.sa/apt-mirror/ ubuntu http://mirror1.sox.rs/ubuntu/ubuntu/ ubuntu http://ossmirror.mycloud.services/os/linux/ubuntu/ ubuntu http://mirror.sg.gs/ubuntu/ ubuntu http://kambing.uu.sg/ubuntu/ ubuntu http://mirror.0x.sg/ubuntu/ ubuntu ftp://mirror.0x.sg/ubuntu/ ubuntu http://mirror.soonkeat.sg/ubuntu/ ubuntu http://mirror.aktkn.sg/ubuntu/ ubuntu http://mirror.coganng.com/ubuntu/ ubuntu ftp://mirror.coganng.com/ubuntu/ ubuntu http://mirror.coganng.com/ubuntu-ports/ ubuntu ftp://mirror.coganng.com/ubuntu-ports/ ubuntu http://sg-mirrors.vhost.vn/ubuntu/ ubuntu http://mirror.vnet.sk/ubuntu/ ubuntu http://ftp.energotel.sk/pub/linux/ubuntu/ ubuntu ftp://ftp.energotel.sk/pub/linux/ubuntu/ ubuntu http://tux.rainside.sk/ubuntu/ ubuntu ftp://tux.rainside.sk/ubuntu/ ubuntu http://ftp.arnes.si/pub/mirrors/ubuntu/ ubuntu ftp://ftp.arnes.si/mirrors/ubuntu/ ubuntu http://ubuntu.mirror.ac.za/ubuntu/ ubuntu ftp://ubuntu.mirror.ac.za/ubuntu/ ubuntu http://mirror.hostafrica.co.za/ubuntu/ ubuntu http://ubuntu.mirror.rain.co.za/ubuntu/ ubuntu http://mirror.raiolanetworks.com/ubuntu/ ubuntu http://ftp.udc.es/ubuntu/ ubuntu ftp://ftp.udc.es/ubuntu/ ubuntu http://ubuntu.cica.es/ubuntu/ ubuntu ftp://ubuntu.cica.es/ubuntu/ ubuntu http://ftp.caliu.cat/pub/distribucions/ubuntu/archive/ ubuntu http://ftp.csuc.cat/ubuntu/archive/ ubuntu ftp://ftp.csuc.cat/ubuntu/archive/ ubuntu http://ubuntu.uvigo.es/ ubuntu ftp://ftp.uvigo.es/ubuntu/ ubuntu http://dafi.inf.um.es/ubuntu/ ubuntu ftp://dafi.inf.um.es/ubuntu/ ubuntu http://ubuntu.grn.cat/ubuntu/ ubuntu ftp://ubuntu.grn.cat/ubuntu/ ubuntu http://ftp.acc.umu.se/ubuntu/ ubuntu ftp://ftp.acc.umu.se/ubuntu/ ubuntu http://ftp.ludd.ltu.se/mirrors/ubuntu/ ubuntu http://mirror.bahnhof.net/ubuntu/ ubuntu ftp://mirror.bahnhof.net/ubuntu/ ubuntu http://mirror.zetup.net/ubuntu/ ubuntu http://ftp.lysator.liu.se/ubuntu/ ubuntu ftp://ftp.lysator.liu.se/ubuntu/ ubuntu http://ftpmirror1.infania.net/ubuntu/ ubuntu ftp://ftpmirror1.infania.net/ubuntu/ ubuntu http://mirror.siati.ch/ubuntu-archive/ ubuntu http://mirror.infomaniak.ch/ubuntu/ ubuntu ftp://mirror.infomaniak.ch/ubuntu/ ubuntu http://mirror.init7.net/ubuntu/ ubuntu http://mirror.solnet.ch/ubuntu/ ubuntu http://pkg.adfinis-on-exoscale.ch/ubuntu/ ubuntu http://ubuntu.ethz.ch/ubuntu/ ubuntu ftp://ubuntu.ethz.ch/ubuntu/ ubuntu http://mirror.gofoss.xyz/ubuntu/ ubuntu http://mirror.gofoss.xyz/ubuntu-ports/ ubuntu http://pkg.adfinis.com/ubuntu/ ubuntu http://mirror.twds.com.tw/ubuntu/ ubuntu http://mirror.twds.com.tw/ubuntu-ports/ ubuntu http://free.nchc.org.tw/ubuntu/ ubuntu ftp://free.nchc.org.tw/ubuntu ubuntu http://ftp.tw.debian.org/ubuntu/ ubuntu ftp://ftp.tw.debian.org/ubuntu/ ubuntu http://ubuntu.ccns.ncku.edu.tw/ubuntu/ ubuntu http://ftp.ubuntu-tw.net/ubuntu/ ubuntu ftp://ftp.ubuntu-tw.net/ubuntu/ ubuntu http://mirror.ossplanet.net/ubuntu/ ubuntu ftp://mirror.ossplanet.net/ubuntu/ ubuntu http://ftp.tku.edu.tw/ubuntu/ ubuntu ftp://ftp.tku.edu.tw/ubuntu/ ubuntu http://tw1.mirror.blendbyte.net/ubuntu/ ubuntu http://ubuntu.cs.nctu.edu.tw/ubuntu/ ubuntu ftp://ubuntu.cs.nctu.edu.tw/ubuntu/ ubuntu http://ftp.mirror.tw/pub/ubuntu/ubuntu/ ubuntu ftp://ftp.mirror.tw/pub/ubuntu/ubuntu/ ubuntu http://mirror.aptus.co.tz/pub/ubuntuarchive/ ubuntu http://deb-mirror.habari.co.tz/ubuntu/ ubuntu http://mirror.kku.ac.th/ubuntu/ ubuntu ftp://mirror.kku.ac.th/ubuntu/ ubuntu http://mirrors.cloudforest.co.th/ubuntu/ ubuntu http://mirror1.totbb.net/ubuntu/ ubuntu http://mirrors.bangmod.cloud/ubuntu/ ubuntu http://mirror1.ku.ac.th/ubuntu/ ubuntu http://mirror.ati.tn/ubuntu/ ubuntu http://mirror.ekiphost.com/ubuntu/ ubuntu http://mirror.sh.com.tr/ubuntu/ ubuntu http://mirror.domainhizmetleri.com/ubuntu/ ubuntu http://mirror.verinomi.com/ubuntu/ubuntu-archive/ ubuntu http://mirror.kapteyan.com.tr/ubuntu/ ubuntu http://mirror.rabisu.com/ubuntu/ubuntu-archive/ ubuntu http://mirror.renu.ac.ug/ubuntu/ ubuntu http://ubuntu.ip-connect.vn.ua/ ubuntu ftp://ubuntu.ip-connect.vn.ua/mirror/ubuntu/ ubuntu http://ubuntu.mirrors.omnilance.com/ubuntu/ ubuntu http://distrohub.kyiv.ua/ubuntu/ ubuntu http://mirror.mirohost.net/ubuntu/ ubuntu ftp://mirror.mirohost.net/ubuntu/ ubuntu http://ubuntu.netforce.hosting/ubuntu/ ubuntu ftp://ubuntu.netforce.hosting/ubuntu/ ubuntu http://ubuntu.org.ua/ubuntu/ ubuntu ftp://ubuntu.org.ua/ubuntu/ ubuntu http://mirror.cov.ukservers.com/ubuntu/ ubuntu ftp://mirror.cov.ukservers.com/ubuntu/ ubuntu http://mirror.katapult.io/ubuntu/ ubuntu http://mirror.server.net/ubuntu/ ubuntu http://mirror.vorboss.net/ubuntu-archive/ ubuntu http://mirrors.ukfast.co.uk/sites/archive.ubuntu.com/ ubuntu ftp://mirrors.ukfast.co.uk/archive.ubuntu.com/ ubuntu http://www.mirrorservice.org/sites/archive.ubuntu.com/ubuntu/ ubuntu ftp://ftp.mirrorservice.org/sites/archive.ubuntu.com/ubuntu/ ubuntu http://uk.mirrors.clouvider.net/ubuntu/ ubuntu http://archive.ubuntu.moon127.net/ ubuntu http://mirror.as29550.net/archive.ubuntu.com/ ubuntu ftp://mirror.as29550.net/archive.ubuntu.com/ ubuntu http://mirror.bytemark.co.uk/ubuntu/ ubuntu ftp://mirror.bytemark.co.uk/ubuntu/ ubuntu http://mirror.eu-lo.kamatera.com/ubuntu/ ubuntu http://mirror.freethought-internet.co.uk/ubuntu/ ubuntu ftp://mirror.freethought-internet.co.uk/ubuntu/ ubuntu http://mirror.ox.ac.uk/sites/archive.ubuntu.com/ubuntu/ ubuntu ftp://mirror.ox.ac.uk/sites/archive.ubuntu.com/ubuntu/ ubuntu http://mirror.vinehost.net/ubuntu/ ubuntu ftp://mirror.vinehost.net/ubuntu/ ubuntu http://mirrors.coreix.net/ubuntu/ ubuntu http://mirrors.gethosted.online/ubuntu/ ubuntu ftp://mirrors.gethosted.online/ubuntu/ ubuntu http://mirrors.melbourne.co.uk/ubuntu/ ubuntu ftp://mirrors.melbourne.co.uk/ubuntu/ ubuntu http://ports.ubuntu.moon127.net/ ubuntu http://ubuntu.mirrors.uk2.net/ubuntu/ ubuntu ftp://ubuntu.mirrors.uk2.net/ubuntu/ ubuntu http://archive.ubuntu.com/ubuntu/ ubuntu ftp://archive.ubuntu.com/ubuntu/ ubuntu http://mirror.enzu.com/ubuntu/ ubuntu ftp://mirror.enzu.com/ubuntu/ ubuntu http://mirror.pilotfiber.com/ubuntu/ ubuntu http://dal.mirrors.serverside.com/ubuntu/ ubuntu http://mirror.math.princeton.edu/pub/ubuntu/ ubuntu http://mirror.pit.teraswitch.com/ubuntu/ ubuntu http://mirror.us.mirhosting.net/ubuntu/ ubuntu ftp://mirror.us.mirhosting.net/ubuntu/ ubuntu http://mirrors.iu13.net/ubuntu/ ubuntu http://atl.mirrors.clouvider.net/ubuntu/ ubuntu http://dal.mirrors.clouvider.net/ubuntu/ ubuntu http://la.mirrors.clouvider.net/ubuntu/ ubuntu http://mirror.arizona.edu/ubuntu/ ubuntu http://mirror.brightridge.com/ubuntuarchive/ ubuntu http://mirror.hoobly.com/ubuntu/ ubuntu http://mirror.its.umich.edu/ubuntu/ ubuntu http://mirror.linux.ncsu.edu/ubuntu/ ubuntu http://mirror.lstn.net/ubuntu/ ubuntu http://mirror.nodesdirect.com/ubuntu/ ubuntu ftp://mirror.nodesdirect.com/ubuntu/ ubuntu http://mirror.servaxnet.com/ubuntu/ ubuntu http://mirror.timkevin.us/ubuntu/ ubuntu http://mirror.ubuntu.serverforge.org/ ubuntu http://mirror.umd.edu/ubuntu/ ubuntu http://mirror.us.leaseweb.net/ubuntu/ ubuntu ftp://mirror.us.leaseweb.net/ubuntu/ ubuntu http://mirrors.bloomu.edu/ubuntu/ ubuntu ftp://mirrors.bloomu.edu/ubuntu/ ubuntu http://mirrors.egr.msu.edu/ubuntu/ ubuntu http://mirrors.rit.edu/ubuntu/ ubuntu ftp://mirrors.rit.edu/ubuntu/ ubuntu http://mirrors.usinternet.com/ubuntu/archive/ ubuntu http://mirrors.xtom.com/ubuntu/ ubuntu http://nyc.mirrors.clouvider.net/ubuntu/ ubuntu http://plug-mirror.rcac.purdue.edu/ubuntu/ ubuntu ftp://plug-mirror.rcac.purdue.edu/ubuntu/ ubuntu http://ubuntu.cs.utah.edu/ubuntu/ ubuntu ftp://ubuntu.cs.utah.edu/ubuntu/ubuntu/ ubuntu http://ubuntu.mirror.constant.com/ ubuntu http://ubuntu.mirror.shastacoe.net/ubuntu/ ubuntu http://mirrors.wikimedia.org/ubuntu/ ubuntu http://mirrors.gigenet.com/ubuntu/ ubuntu http://mirror.rustytel.net/ubuntu/ ubuntu http://mirror.siena.edu/ubuntu/ ubuntu http://mirrors.cmich.edu/ubuntu/ ubuntu http://mirrors.us.kernel.org/ubuntu/ ubuntu ftp://mirrors.us.kernel.org/ubuntu/ ubuntu http://repo.ialab.dsu.edu/ubuntu/ ubuntu http://ubuntu.osuosl.org/ubuntu/ ubuntu ftp://ubuntu.osuosl.org/pub/ubuntu/ ubuntu http://ftp.usf.edu/pub/ubuntu/ ubuntu ftp://ftp.usf.edu/pub/ubuntu/ ubuntu http://mirror.cc.vt.edu/pub2/ubuntu/ ubuntu ftp://mirror.cc.vt.edu/pub2/ubuntu/ ubuntu http://mirror.clarkson.edu/ubuntu/ ubuntu http://mirror.cogentco.com/pub/linux/ubuntu/ ubuntu http://mirror.cs.jmu.edu/pub/ubuntu/ ubuntu http://mirror.dal.nexril.net/ubuntu/ ubuntu http://mirror.math.ucdavis.edu/ubuntu/ ubuntu http://mirror.metrocast.net/ubuntu/ ubuntu http://mirror.mia.velocihost.net/ubuntu/ ubuntu http://mirror.mrjester.net/ubuntu/archive/ ubuntu http://mirror.nerdmilio.com/ubuntu/ ubuntu http://mirror.steadfastnet.com/ubuntu/ ubuntu http://mirror.team-cymru.com/ubuntu/ ubuntu ftp://mirror.team-cymru.com/ubuntu/ ubuntu http://mirror.team-cymru.org/ubuntu/ ubuntu ftp://mirror.team-cymru.org/ubuntu/ ubuntu http://mirror.uoregon.edu/ubuntu/ ubuntu ftp://mirror.uoregon.edu/ubuntu/ ubuntu http://mirror.us-midwest-1.nexcess.net/ubuntu/ ubuntu http://mirror.us-ny2.kamatera.com/ubuntu/ ubuntu http://mirror.us-sc.kamatera.com/ubuntu/ ubuntu http://mirror.us-tx.kamatera.com/ubuntu/ ubuntu http://mirrors.accretive-networks.net/ubuntu/ ubuntu http://mirrors.arpnetworks.com/Ubuntu/ ubuntu http://mirrors.cat.pdx.edu/ubuntu/ ubuntu http://mirrors.liquidweb.com/ubuntu/ ubuntu http://mirrors.lug.mtu.edu/ubuntu/ ubuntu ftp://mirrors.lug.mtu.edu/ubuntu/ ubuntu http://mirrors.maine.edu/ubuntu/ ubuntu http://mirrors.mit.edu/ubuntu/ ubuntu http://mirrors.namecheap.com/ubuntu/ ubuntu ftp://mirrors.namecheap.com/ubuntu/ ubuntu http://mirrors.ocf.berkeley.edu/ubuntu/ ubuntu ftp://mirrors.ocf.berkeley.edu/ubuntu/ ubuntu http://mirrors.ocf.berkeley.edu/ubuntu-ports/ ubuntu http://mirrors.sarak.as/ubuntu/ ubuntu ftp://mirrors.sarak.as/ubuntu/ ubuntu http://mirrors.sonic.net/ubuntu/ ubuntu ftp://mirrors.sonic.net/ubuntu/ ubuntu http://mirrors.xmission.com/ubuntu/ ubuntu ftp://mirrors.xmission.com/ubuntu/ ubuntu http://pubmirrors.dal.corespace.com/ubuntu/ ubuntu ftp://pubmirrors.dal.corespace.com/ubuntu/ ubuntu http://ubuntu.mirror.frontiernet.net/ubuntu/ ubuntu ftp://ubuntu.mirror.frontiernet.net/ubuntu/ ubuntu http://ubuntu.mirrors.pair.com/archive/ ubuntu ftp://ubuntu.mirrors.pair.com/archive/ ubuntu http://ubuntu.phoenixnap.com/ubuntu/ ubuntu http://www.gtlib.gatech.edu/pub/ubuntu/ ubuntu ftp://ftp.gtlib.gatech.edu/pub/ubuntu ubuntu http://mirror.d.umn.edu/ubuntu/ ubuntu http://mirrors.vcea.wsu.edu/ubuntu/ ubuntu http://ubuntu.securedservers.com/ ubuntu http://ftp.ussg.iu.edu/linux/ubuntu/ ubuntu http://mirror.vcu.edu/pub/gnu+linux/ubuntu/ ubuntu ftp://mirror.vcu.edu/pub/gnu+linux/ubuntu/ ubuntu http://ubuntu.repo.cure.edu.uy/ubuntu/ ubuntu http://repos.interior.edu.uy/ubuntu/ ubuntu http://mirror.dc.uz/ubuntu/ ubuntu ftp://mirror.dc.uz/ubuntu/ ubuntu http://mirror.azvps.vn/ubuntu/ ubuntu http://ubuntu.vpsttt.com/ubuntu/ ubuntu http://opensource.xtdv.net/ubuntu/ ubuntu http://mirror.bizflycloud.vn/ubuntu/ ubuntu http://mirror.clearsky.vn/ubuntu/ ubuntu http://mirror.vietnix.vn/ubuntu/ ubuntu http://mirror.viettelcloud.vn/ubuntu/ ubuntu http://mirrors.tino.org/ubuntu/ ubuntu http://vn-mirrors.techhost.vn/ubuntu/ ubuntu http://vn-mirrors.vhost.vn/ubuntu/ ubuntu http://mirrors.nhanhoa.com/ubuntu/ EOFML } function getLogo() { if [ "$IS_INSTALLED" = "true" ] && [ "$IS_CONFIG_FILE_UNENCRYPTED" = "true" ]; then echo "$hshqwarninglogo" else echo "$hshqlogo" fi } function showInstalledMenu() { installedmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$installedmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Services" \ "2" "System Utils" \ "3" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 0) return 0 ;; 1) checkLoadConfig showStacksMenu set +e return 1 ;; 2) set +e showAllUtilsMenu set +e return 1 ;; # 2) # checkLoadConfig # if ! [ "$IS_INSTALLED" = "true" ]; then # showMessageBox "System Not Installed" "You must perform a system installation first before using this utility." # exit # fi # set +e # showNetworkMenu # set +e # return 1 ;; # 2) # showHSHQUtilsMenu # set +e # return 1 ;; # 3) # showSysUtilsMenu # set +e # return 1 ;; # 4) # return 0 ;; esac } function showRestoreMenu() { set +e precheckRestoreResult="$(performRestorePrecheck)" if ! [ -z "$precheckRestoreResult" ]; then showMessageBox "ERROR" "$precheckRestoreResult" return fi checkPerformSystemPrep if [ $? -ne 0 ]; then return fi restoremenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$restoremenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Restore From Encrypted Duplicati Backup" \ "2" "Restore From Unencrypted Backup" \ "3" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 0) return 0 ;; 1) showRestoreEncryptedMenu ;; 2) showRestoreUnencryptedMenu ;; 3) return 0 ;; esac } function showRestoreEncryptedMenu() { restoremenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$restoremenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Mount Drive" \ "2" "Select Directory" \ "3" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 0) return 0 ;; 1) showRestoreMountDriveMenu ;; 2) showRestoreSelectEncryptedDirectoryMenu ;; 3) return 0 ;; esac } function showRestoreUnencryptedMenu() { set +e performRestorePostcheck if [ $? -ne 0 ]; then return fi confirmRestore=$(promptUserInputMenu "" "Confirm" "The main restore process is ready to start. This will take awhile to complete, as all of the docker images must be re-downloaded. You will be prompted shortly for your config decrypt password. Enter the word 'restore' below to continue:") if [ $? -ne 0 ] || [ -z "$confirmRestore" ] || ! [ "$confirmRestore" = "restore" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi cp $HSHQ_LIB_SCRIPT $HOME/$HSHQ_LIB_FILENAME rm -fr $HSHQ_DATA_DIR/* mv $HSHQ_RESTORE_DIR/* $HSHQ_DATA_DIR/ rm -fr $HSHQ_RESTORE_DIR # Only restore hshqlib script if version is less than 85 hshqlib_orig_version=$(sed -n 2p $HSHQ_LIB_SCRIPT | cut -d"=" -f2) if [ $hshqlib_orig_version -lt 85 ]; then mv $HOME/$HSHQ_LIB_FILENAME $HSHQ_LIB_SCRIPT else rm -f $HOME/$HSHQ_LIB_FILENAME fi scName=$RESTORE_SCREEN_NAME initScreen $scName "bash $HSHQ_LIB_SCRIPT restore $CONNECTING_IP" if [ $? -ne 0 ]; then showMessageBox "ERROR" "There was a problem starting the restore process, exiting..." exit 1 fi screen -r $scName exit 0 } function performFullRestore() { refreshSudo createHSHQLog loadConfigConsole forceGetLock networkchecks performFullRestore setSystemState $SS_RESTORING IS_INSTALLED=false IS_INSTALLING=true is_draw_pb=false set +e origUsername=$(echo $HSHQ_BASE_DIR | cut -d"/" -f3) curUID=$(id -u) curGID=$(id -g) if ! [ "$USERNAME" = "$origUsername" ] || ! [ "$curUID" = "$USERID" ] || ! [ "$curGID" = "$GROUPID" ]; then msgmenu=$(cat << EOF $(getLogo) The current username(UID:GID) does not match the original. Current: $USERNAME ($curUID:$curGID) Original: $origUsername ($USERID:$GROUPID) The restore cannot be performed. Please ensure to have the exact same username and UID:GID as the Original. EOF ) whiptail --title "ERROR" --msgbox "$msgmenu" $MENU_HEIGHT $MENU_WIDTH return fi setSudoTimeoutInstall mkdir -p $HSHQ_NONBACKUP_DIR mkdir -p $HSHQ_BUILD_DIR # Set hostname new_hostname="HomeServer-$(echo $HOMESERVER_DOMAIN | sed 's/\./-/g')" if [ -z "$(cat /etc/hosts | grep $new_hostname)" ]; then echo "127.0.1.1 $new_hostname" | sudo tee -a /etc/hosts fi sudo hostnamectl set-hostname $new_hostname rm -f $HOME/dead.letter installDependencies echo "Pulling docker images..." restorePullDockerImages rm -f $HOME/script-server.zip # Set timezone sudo timedatectl set-timezone "$TZ" set +e # Setup primary network interface prevPrimaryIface=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ConnectionType = 'primary' and NetworkType = 'home_network';") isFound=false availInterfacesArr=($(echo "$(getAllNetworkInterfacesCSV)" | tr "," "\n")) for curInterface in "${availInterfacesArr[@]}" do if [ "$prevPrimaryIface" = "$curInterface" ]; then isFound=true break fi done if [ "$isFound" = "false" ]; then default_iface=$(getDefaultIface) checkDB=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where InterfaceName = '$default_iface';") if [ -z "$checkDB" ]; then addHSInterface $default_iface true else sudo sqlite3 $HSHQ_DB "update connections set ConnectionType = 'secondary' where NetworkType = 'home_network';" sudo sqlite3 $HSHQ_DB "update connections set ConnectionType = 'primary' where InterfaceName = '$default_iface';" fi fi set -e # This should cover all possibilities curPrimaryIfaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ConnectionType = 'primary' and NetworkType = 'home_network';") curPrimaryIfaceIP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ConnectionType = 'primary' and NetworkType = 'home_network';") HOMESERVER_HOST_PRIMARY_INTERFACE_NAME=$curPrimaryIfaceName updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_NAME $HOMESERVER_HOST_PRIMARY_INTERFACE_NAME HOMESERVER_HOST_PRIMARY_INTERFACE_IP=$curPrimaryIfaceIP updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_IP $HOMESERVER_HOST_PRIMARY_INTERFACE_IP find $HSHQ_SSL_DIR -name "*-ca.crt" -print0 | xargs -0 -I {} sudo cp {} /usr/local/share/ca-certificates/ sudo update-ca-certificates CURRENT_SSH_PORT=$(sudo grep "^Port\|^#Port" /etc/ssh/sshd_config | cut -d" " -f2) if [ -z "$CURRENT_SSH_PORT" ]; then CURRENT_SSH_PORT=22 fi # Change SSH port on host sudo sed -i "s|^#*Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config sudo sed -i "s|^Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config updateMOTD performSuggestedSecUpdates installMailUtils performClearIPTables true checkUpdateAllIPTables performFullRestore outputScripts sudo cp $HSHQ_WIREGUARD_DIR/vpn/*.conf /etc/wireguard/ > /dev/null 2>&1 performAptInstall python3-tornado > /dev/null 2>&1 sudo rm -f /etc/systemd/system/runScriptServer.service sudo ln -s $HSHQ_SCRIPTS_DIR/root/runScriptServer.service /etc/systemd/system/runScriptServer.service sudo systemctl daemon-reload sudo systemctl enable runScriptServer sudo systemctl start runScriptServer set -e if [ -d $HSHQ_STACKS_DIR/wazuh ]; then installWazuhAgent fi set +e sudo docker ps -q | xargs sudo docker stop > /dev/null 2>&1 sudo docker container prune -f > /dev/null 2>&1 createDockerNetworks enableAllWGVPNInterfaces createWGDockerNetworks outputEnvPortainer startPortainer docker volume create --driver local -o device=$HSHQ_STACKS_DIR/caddy-common/primary-certs -o o=bind -o type=none caddy-primary-certs createClientDNSNetworksOnRestore prepAdguardInstallation restartAllStacks "duplicati,syncthing" true doNothing false doNothing sudo docker container prune -f addDomainAndWildcardAdguardHS $HOMESERVER_DOMAIN $HOMESERVER_HOST_PRIMARY_INTERFACE_IP # Add RelayServer fingerprint if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "echo \"Successfully connected to RelayServer! \"" -f fi wgDockInternetUpAll postInstallation restore setSystemState $SS_RUNNING performExitFunctions releaseLock networkchecks performFullRestore false set +e sudo systemctl daemon-reload sudo systemctl restart ssh sudo systemctl restart sshd clear echo -e "\n\n\n\n################################################################\n" echo -e " HomeServer Restore Process Complete! \n" echo -e " The Duplicati and Syncthing stacks have been intentionally" echo -e " skipped from the stack restart process, as you may still" echo -e " need to re-configure your backup drives, etc. You may also" echo -e " have to manually restart some stacks due to any issues in" echo -e " the docker startup process. Double check everything is" echo -e " working correctly in Portainer." echo -e " --------------------- " echo -e " If the ip address of the primary network interface is" echo -e " different, then the caddy-home-xxx stack likely had" echo -e " issues when restarting the stack. However, the networking" echo -e " checks background process will automatically resolve that" echo -e " issue within a minute or two.\n" echo -e " ***** IMPORTANT ***** " echo -e " If you use Duplicati as your primary backup method, then" echo -e " the first thing you need to do after starting the Duplicati" echo -e " stack is to run the 'Recreate (delete and repair)' function." echo -e " This can be found by going to your HSHQ Backup, then go to" echo -e " Advanced -> Database. DO NOT run the 'Repair' function," echo -e " even if prompted. It will cause permanent data loss to your" echo -e " backup history, and you will have to either recover the" echo -e " missing files or start a fresh new backup profile." echo -e "\n################################################################\n\n" read -r -p " Press enter to continue." screen -S $RESTORE_SCREEN_NAME -X quit } function restorePullDockerImages() { set +e OLDIFS=$IFS IFS=$(echo -en "\n\b") imgList=($(sudo find $HSHQ_STACKS_DIR/portainer/compose -name docker-compose.yml -print0 | xargs -0 sudo grep -hr "^[[:blank:]]*image:")) imgOnlyList=() for curImgItem in "${imgList[@]}" do imgOnlyList+=($(echo "$curImgItem" | xargs | cut -d" " -f2)) done IFS=$OLDIFS uniqs_arr=($(for tmpImg in "${imgOnlyList[@]}"; do echo "${tmpImg}"; done | sort -u)) for curImg in "${uniqs_arr[@]}" do echo "Pulling $curImg..." buildOrPullImage "$curImg" 5 > /dev/null 2>&1 done # Create nonbackup directories stackList=($(sudo find $HSHQ_STACKS_DIR/portainer/compose -name docker-compose.yml -print0 | xargs -0 sudo grep -hr "^#HSHQManaged" | cut -d" " -f2)) for curStackItem in "${stackList[@]}" do checkCreateNonbackupDirByStack "$curStackItem" done restoreRemainingNonBackupDirectories } function restoreRemainingNonBackupDirectories() { set +e OLDIFS=$IFS IFS=$(echo -en "\n\b") dirList=($(sudo find $HSHQ_STACKS_DIR/portainer/compose -name docker-compose.yml -print0 | xargs -0 sudo grep -hr "\${PORTAINER_HSHQ_NONBACKUP_DIR}" | cut -d"}" -f2 | cut -d":" -f1 | cut -d" " -f1)) for curDir in "${dirList[@]}" do mkdir -p ${HSHQ_NONBACKUP_DIR}$curDir done IFS=$OLDIFS } function showRestoreMountDriveMenu() { findmnt | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1 if [ $? -eq 0 ]; then showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR. Please unmount this filesystem first, returning..." return fi dbackmenu=$(cat << EOF $(getLogo) EOF ) OLDIFS=$IFS IFS=$(echo -en "\n\b") diskarr=($(sudo hwinfo --disk --short | tail +2)) curListNum=1 scsbm_menu_items=( --title "Select Disk/Partition" --radiolist "$dbackmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT ) for curDisk in "${diskarr[@]}" do curDiskID=$(echo "$curDisk" | xargs | cut -d" " -f1) curDiskName=$(echo "$curDisk" | xargs | cut -d" " -f2-) findmnt | grep $curDiskID > /dev/null 2>&1 if [ $? -eq 0 ]; then continue fi curDiskPartitions=($(lsblk --noheadings --raw -o NAME,SIZE,TYPE $curDiskID)) for curDiskPart in "${curDiskPartitions[@]}" do curDiskPartName=$(echo $curDiskPart | cut -d" " -f1) curDiskPartSize=$(echo $curDiskPart | cut -d" " -f2) curDiskPartType=$(echo $curDiskPart | cut -d" " -f3) if [ "$curDiskPartType" = "part" ]; then scsbm_menu_items+=( "$curDiskPartName $curDiskPartSize ($curDiskName)" ) scsbm_menu_items+=( "|" ) scsbm_menu_items+=( "off" ) ((curListNum++)) fi done done IFS=$OLDIFS if [ $curListNum -le 1 ]; then showMessageBox "ERROR" "There are no available disks for this operation, returning..." return fi while true; do selDiskItem=$(whiptail "${scsbm_menu_items[@]}" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then return fi if [ -z "$selDiskItem" ]; then showMessageBox "ERROR" "You did not make a selection, please try again." else break fi done selDisk=$(echo "$selDiskItem" | cut -d" " -f1) if [ -z "$selDisk" ]; then showMessageBox "ERROR" "No partition was selected, returning..." return fi mountDir=$(promptUserInputMenu "$HSHQ_BACKUP_DIR" "Select Directory" "Enter the directory where you would like to mount this partition:") if [ "$mountDir" = "$HSHQ_BACKUP_DIR" ]; then mkdir -p $HSHQ_BACKUP_DIR fi if [ $(checkValidDirectory "$mountDir") = "false" ]; then showMessageBox "ERROR" "Invalid directory name, returning..." return fi findmnt | grep "$mountDir" > /dev/null 2>&1 if [ $? -eq 0 ]; then showMessageBox "ERROR" "There is already a disk mounted to this directory, returning..." return fi if [ -d "$mountDir" ]; then if ! [ -z "$(ls -A $mountDir)" ]; then showMessageBox "ERROR" "The selected directory is not empty, returning..." return fi fi if ! [ -d "$mountDir" ]; then showYesNoMessageBox "ERROR" "This directory does not exist, would you like to create it now?" if [ $? -ne 0 ]; then return fi mkdir -p $mountDir if [ $? -ne 0 ]; then showMessageBox "ERROR" "There was an error creating this directory, likely a permission error, returning..." return fi fi sudo mount /dev/$selDisk $mountDir if [ $? -ne 0 ]; then showMessageBox "ERROR" "There was an error mounting this partition, returning..." return fi showRestoreDuplicatiRestoreMenu "$mountDir" "/dev/$selDisk" } function showRestoreSelectEncryptedDirectoryMenu() { backupDir=$(promptUserInputMenu "$HSHQ_BACKUP_DIR" "Select Directory" "Enter the top-level directory of the encrypted data:") showRestoreDuplicatiRestoreMenu "$backupDir" } function showRestoreDuplicatiRestoreMenu() { set +e backupDirTL="$1" umountDisk="$2" curListNum=0 dbackupmenu=$(cat << EOF $(getLogo) EOF ) OLDIFS=$IFS IFS=$(echo -en "\n\b") dupback_menu_items=( --title "Select Directory" --radiolist "$dbackupmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT ) dirarr=($(sudo ls $backupDirTL)) cd ~ for curDir in "${dirarr[@]}" do if [ "$curDir" = "lost+found" ]; then continue fi if sudo test -d "$backupDirTL/$curDir"; then dupback_menu_items+=( "$curDir" ) dupback_menu_items+=( "|" ) dupback_menu_items+=( "off" ) ((curListNum++)) fi done IFS=$OLDIFS if [ $curListNum -lt 1 ]; then showMessageBox "ERROR" "There are no subdirectories for this operation, returning..." return fi selBackupDir=$(whiptail "${dupback_menu_items[@]}" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then return fi if [ -z "$selBackupDir" ]; then showMessageBox "ERROR" "No directory was selected, returning..." return fi if [ -d "$HSHQ_RESTORE_DIR" ]; then if ! [ -z "$(ls -A $HSHQ_RESTORE_DIR)" ]; then showMessageBox "ERROR" "The restore directory ($HSHQ_RESTORE_DIR) is not empty, returning..." return 1 fi else mkdir -p $HSHQ_RESTORE_DIR fi dup_pw=$(promptPasswordMenu "Enter Passphrase" "Enter the passphrase that you used to encrypt your Duplicati backup data: ") confirmRestore=$(promptUserInputMenu "" "Confirm" "The decryption process is ready to start. It could take anywhere from 15 minutes to many hours to decrypt the data from the encrypted state, depending on how much data. After this step, you will be prompted for additional information. So check back in an hour or so to complete the full server restore process. Enter the word 'decrypt' below to continue:") if [ $? -ne 0 ] || [ -z "$confirmRestore" ] || ! [ "$confirmRestore" = "decrypt" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi sudo rm -fr /tmp/dupconfig mkdir /tmp/dupconfig docker container stop duplicati-restore > /dev/null 2>&1 docker container rm duplicati-restore > /dev/null 2>&1 docker run -d --name=duplicati-restore -v /tmp/dupconfig:/config -v $backupDirTL/$selBackupDir:/backup -v $HSHQ_RESTORE_DIR:/restore $(getScriptImageByContainerName duplicati) > /dev/null 2>&1 echo "Recreating duplicati database..." #docker exec -it duplicati-restore bash -c "mono /app/duplicati/Duplicati.CommandLine.exe repair /backup --dbpath=/config/hshqrestore.sqlite --passphrase=$dup_pw" docker exec -it duplicati-restore bash -c "/app/duplicati/duplicati-cli repair /backup --dbpath=/config/hshqrestore.sqlite --passphrase=$dup_pw" is_repair_dup_error=$? if [ $is_repair_dup_error -ne 0 ]; then docker container stop duplicati-restore > /dev/null 2>&1 docker container rm duplicati-restore > /dev/null 2>&1 sudo rm -fr /tmp/dupconfig if ! [ -z "$umountDisk" ]; then echo "Unmounting disk ($umountDisk)..." sudo umount $umountDisk fi showMessageBox "ERROR" "There was a problem recreating the database, the password may be incorrect. Returning..." return 2 fi echo "Database recreated, restoring files..." #docker exec -it duplicati-restore bash -c "mono /app/duplicati/Duplicati.CommandLine.exe restore /backup --dbpath=/config/hshqrestore.sqlite --restore-path=/restore --overwrite=true --restore-permissions --passphrase=$dup_pw" docker exec -it duplicati-restore bash -c "/app/duplicati/duplicati-cli restore /backup --dbpath=/config/hshqrestore.sqlite --restore-path=/restore --overwrite=true --restore-permissions --passphrase=$dup_pw" docker container stop duplicati-restore > /dev/null 2>&1 docker container rm duplicati-restore > /dev/null 2>&1 sudo rm -fr /tmp/dupconfig echo "Data Successfully Restored!" if ! [ -z "$umountDisk" ]; then echo "Unmounting disk ($umountDisk)..." sudo umount $umountDisk fi showRestoreUnencryptedMenu } function performRestorePrecheck() { if [ -d $HSHQ_ASSETS_DIR ]; then echo "The HSHQ assets directory ($HSHQ_ASSETS_DIR) already exists. Cannot perform restore." fi if [ -d $HSHQ_CONFIG_DIR ]; then echo "The HSHQ config directory ($HSHQ_CONFIG_DIR) already exists. Cannot perform restore." fi if [ -d $HSHQ_RELAYSERVER_DIR ]; then echo "The HSHQ relayserver directory ($HSHQ_RELAYSERVER_DIR) already exists. Cannot perform restore." fi if [ -d $HSHQ_SCRIPTS_DIR ]; then echo "The HSHQ scripts directory ($HSHQ_SCRIPTS_DIR) already exists. Cannot perform restore." fi if [ -d $HSHQ_SECRETS_DIR ]; then echo "The HSHQ secrets directory ($HSHQ_SECRETS_DIR) already exists. Cannot perform restore." fi if [ -d $HSHQ_SSL_DIR ]; then echo "The HSHQ ssl directory ($HSHQ_SSL_DIR) already exists. Cannot perform restore." fi if [ -d $HSHQ_STACKS_DIR ]; then echo "The HSHQ stacks directory ($HSHQ_STACKS_DIR) already exists. Cannot perform restore." fi if [ -d $HSHQ_WIREGUARD_DIR ]; then echo "The HSHQ wireguard directory ($HSHQ_WIREGUARD_DIR) already exists. Cannot perform restore." fi } function performRestorePostcheck() { if ! [ -d "$HSHQ_RESTORE_DIR" ]; then showMessageBox "ERROR" "The restore directory ($HSHQ_RESTORE_DIR) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/assets" ]; then showMessageBox "ERROR" "The restore (assets) directory ($HSHQ_RESTORE_DIR/assets) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/config" ]; then showMessageBox "ERROR" "The restore (config) directory ($HSHQ_RESTORE_DIR/config) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/lib" ]; then showMessageBox "ERROR" "The restore (lib) directory ($HSHQ_RESTORE_DIR/lib) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/scripts" ]; then showMessageBox "ERROR" "The restore (scripts) directory ($HSHQ_RESTORE_DIR/scripts) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/secrets" ]; then showMessageBox "ERROR" "The restore (secrets) directory ($HSHQ_RESTORE_DIR/secrets) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/ssl" ]; then showMessageBox "ERROR" "The restore (ssl) directory ($HSHQ_RESTORE_DIR/ssl) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/stacks" ]; then showMessageBox "ERROR" "The restore (stacks) directory ($HSHQ_RESTORE_DIR/stacks) does not exist, returning..." return 1 fi if ! [ -d "$HSHQ_RESTORE_DIR/wireguard" ]; then showMessageBox "ERROR" "The restore (wireguard) directory ($HSHQ_RESTORE_DIR/wireguard) does not exist, returning..." return 1 fi } function createClientDNSNetworksOnRestore() { echo "Creating ClientDNS networks..." if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi rsi_numTries=1 rsi_totalTries=5 rsi_retVal=1 rstackIDsQry="" while [ $rsi_numTries -le $rsi_totalTries ] do rstackIDsQry=$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN") rsi_retVal=$? if [ $rsi_retVal -eq 0 ]; then break fi ((rsi_numTries++)) done if [ $rsi_retVal -ne 0 ]; then echo "ERROR: Could not get list of stack IDs in Portainer..." 1>&2 return fi rstackIDs=($(echo $rstackIDsQry | jq -r '.[] | select(.Status == 1) | .Id')) rstackNames=($(echo $rstackIDsQry | jq -r '.[] | select(.Status == 1) | .Name')) numItems=$((${#rstackIDs[@]} - 1)) for curID in $(seq 0 $numItems); do echo ${rstackNames[$curID]} | grep "^clientdns-" > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "Found ClientDNS stack: (${rstackIDs[$curID]}) ${rstackNames[$curID]}" cdns_stack_name=$(echo echo ${rstackNames[$curID]} | cut -d"-" -f2) clientdns_subnet=$(getConfigVarFromFile CLIENTDNS_SUBNET_PREFIX $HSHQ_STACKS_DIR/portainer/compose/${rstackIDs[$curID]}/stack.env root).0/24 docker network create -o com.docker.network.bridge.name=brcd-${cdns_stack_name} --driver=bridge --subnet $clientdns_subnet cdns-${cdns_stack_name} > /dev/null fi done } function showAllUtilsMenu() { set +e utilmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Edit Encrypted Config File" \ "2" "Edit Plaintext Root Config File" \ "3" "Edit Plaintext User Config File" \ "4" "Update APT Mirror" \ "5" "Release All Locks" \ "6" "Update IP and Refresh Firewall" \ "7" "Configure Simple Backup" \ "8" "Uninstall and Remove Everything" \ "9" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi set -e case $menures in 0) set +e return 1 ;; 1) set +e showYesNoMessageBox "WARNING" "Be VERY careful editing this file. You could very easily break things. The configuration file will be opened with the nano utility, so ensure you know how to use this before proceeding.\n\nDo you wish to continue?" if [ $? -ne 0 ]; then return 1 fi checkLoadConfig nano $CONFIG_FILE loadConfigVars true set +e return 1 ;; 2) set +e showYesNoMessageBox "WARNING" "Be VERY careful editing this file. You could very easily break things. The configuration file will be opened with the nano utility, so ensure you know how to use this before proceeding.\n\nDo you wish to continue?" if [ $? -ne 0 ]; then return 1 fi checkLoadConfig sudo nano $HSHQ_PLAINTEXT_ROOT_CONFIG loadConfigVars true set +e return 1 ;; 3) set +e showYesNoMessageBox "WARNING" "Be VERY careful editing this file. You could very easily break things. The configuration file will be opened with the nano utility, so ensure you know how to use this before proceeding.\n\nDo you wish to continue?" if [ $? -ne 0 ]; then return 1 fi checkLoadConfig nano $HSHQ_PLAINTEXT_USER_CONFIG loadConfigVars true set +e return 1 ;; 4) set +e refreshSudo checkUpdateAptSources true return 1 ;; 5) set +e sudo -k USER_SUDO_PW="" refreshSudo releaseAllLocks true return 1 ;; 6) checkLoadConfig checkHostAllInterfaceIPChanges true false User-Console checkUpdateAllIPTables User-Console set +e return 1 ;; 7) checkLoadConfig showSimpleBackupMenu set +e return 1 ;; 8) nukeHSHQ ;; 9) return 0 ;; esac } function showHSHQUtilsMenu() { set +e utilmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Edit Encrypted Config File" \ "2" "Edit Plaintext Root Config File" \ "3" "Edit Plaintext User Config File" \ "5" "Reset Caddy Data" \ "6" "Restart All Stacks" \ "7" "Email Vaultwarden Credentials" \ "8" "Email Root CA" \ "9" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi set -e case $menures in 0) set +e return 1 ;; 1) checkLoadConfig nano $CONFIG_FILE loadConfigVars true set +e return 1 ;; 2) checkLoadConfig sudo nano $HSHQ_PLAINTEXT_ROOT_CONFIG loadConfigVars true set +e return 1 ;; 3) checkLoadConfig nano $HSHQ_PLAINTEXT_USER_CONFIG loadConfigVars true set +e return 1 ;; 4) checkLoadConfig if ! [ "$IS_INSTALLED" = "true" ]; then showMessageBox "System Not Installed" "You must perform a system installation first before using this utility." exit fi generateCertDialog set +e return 1 ;; 5) checkLoadConfig if ! [ "$IS_INSTALLED" = "true" ]; then showMessageBox "System Not Installed" "You must perform a system installation first before using this utility." exit fi showResetCaddyMenu set +e return 1 ;; 6) checkLoadConfig restartAllStacksDialog ;; 7) checkLoadConfig emailVaultwardenCredentials ;; 8) checkLoadConfig sendRootCAEmail false ;; 9) return 0 ;; esac } function showSysUtilsMenu() { set +e utilmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Update Linux OS and Reboot" \ "2" "Download All Docker Images" \ "3" "Release All Locks" \ "4" "Check For IP Address Changes" \ "5" "Refresh Firewall" \ "6" "Configure Simple Backup" \ "7" "Uninstall and Remove Everything" \ "8" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi set -e case $menures in 0) set +e return 1 ;; 1) performExitFunctions true set +e sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y && sudo reboot ;; 2) pullDockerImages set +e return 1 ;; 3) sudo -k set +e USER_SUDO_PW="" refreshSudo releaseAllLocks return 1 ;; 4) checkLoadConfig checkHostAllInterfaceIPChanges true false User-Console set +e return 1 ;; 5) checkLoadConfig checkUpdateAllIPTables User-Console set +e return 1 ;; 6) checkLoadConfig showSimpleBackupMenu set +e return 1 ;; 7) nukeHSHQ ;; 8) return 0 ;; esac } function showResetCaddyMenu() { set +e caddymenu=$(cat << EOF $(getLogo) Select the Caddy instances that you wish to reset: EOF ) caddy_arr=($(docker ps -a --filter name=caddy- --format "{{.Names}}")) menu_items="" for curcaddy in "${caddy_arr[@]}" do menu_items=${menu_items}"$curcaddy | OFF " done if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then menu_items=${menu_items}"RelayServer | OFF " fi sel_caddy=($(whiptail --title "Select Instances" --checklist "$caddymenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) showYesNoMessageBox "Confirm Reset" "Perform this action on the selected Caddy server(s), Continue?" if [ $? -ne 0 ]; then set -e return 0 fi set -e for curcaddy in "${sel_caddy[@]}" do curcaddy=$(echo $curcaddy | tr -d \") if [ "$curcaddy" = "RelayServer" ]; then resetCaddyDataRelayServer true else sudo $HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh $curcaddy fi done } function checkPerformSystemPrep() { isPrepped=true for util in $UTILS_LIST; do if [[ "$(isProgramInstalled $util)" = "false" ]]; then isPrepped=false break fi done if [ "$isPrepped" = "false" ]; then showYesNoMessageBox "Prep System" "The system must be prepped with updates and required utilities. The system will automatically reboot upon completion and you will need to re-run this utility. Do you wish to continue?" if [ $? -ne 0 ]; then return 1 fi sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y installDependencies pullBaseServicesDockerImages sudo reboot fi } function checkWrapperVersion() { set +e curWrapV=$(sed -n 2p $HSHQ_WRAP_SCRIPT | cut -d"=" -f2) if ! [ -z "$curWrapV" ]; then if [ $curWrapV -lt 5 ]; then showMessageBox "Update Wrapper" "It appears you are using a deprecated version of the wrapper script. Please run the command: 'wget -q4N https://homeserverhq.com/hshq.sh' to obtain the latest version as soon as possible." fi fi set -e } function loadVersionVars() { DISTRO_ID=$(cat /etc/*-release 2> /dev/null | grep "^ID=" | cut -d"=" -f2 | xargs | tr '[:upper:]' '[:lower:]') DISTRO_NAME=$(cat /etc/*-release 2> /dev/null | grep "^NAME=" | cut -d"=" -f2 | xargs) DISTRO_VERSION=$(cat /etc/*-release 2> /dev/null | grep "^VERSION=" | cut -d"=" -f2 | xargs) DISTRO_CODENAME=$(cat /etc/*-release 2> /dev/null | grep "^VERSION_CODENAME=" | cut -d"=" -f2 | xargs) DISTRO_ARCH=$(dpkg --print-architecture) } function checkSupportedHostOS() { IS_DISTRO_EXP=false if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22\.04. ]]; then return fi if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24\.04. ]]; then return fi if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26\.04. ]]; then return fi if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]] && [ "$DISTRO_NAME" = "AnduinOS" ]; then return fi if [ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]; then return fi if [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then return fi if [ "$IS_DISTRO_EXP" = "true" ]; then showYesNoMessageBox "Confirm Experimental" "This distribution of Linux is on the experimental list. It has not yet been thoroughly tested. Do you wish to continue?" if [ $? -eq 0 ]; then touch $HOME/hshq/$IS_HSHQ_TEST_FILENAME IS_HSHQ_DEV_TEST=true return fi else echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" echo "@ Unsupported Host Operating System @" echo "@ @" echo "@ This installation only supports the following Linux distribution(s): @" echo "@ @" echo "@ - Ubuntu 22.04 (Jammy Jellyfish) @" echo "@ - Ubuntu 24.04 (Noble Numbat) @" echo "@ - AnduinOS 1.1 (Noble Numbat) @" echo "@ - Debian 12 (Bookworm) @" echo "@ - Mint 22 (Wilma) @" echo "@ @" echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" fi releaseLock hshqopen "checkSupportedHostOS" false exit 2 } function updateSysctl() { isPerfUpdate=$1 set +e sudo rm -f /etc/sysctl.d/88-hshq.conf sudo tee /etc/sysctl.d/88-hshq.conf >/dev/null < /dev/null 2>&1 fi } function createSwapfile() { set +e sudo grep -q swap /etc/fstab > /dev/null 2>&1 if [ $? -ne 0 ] && ! [ -f /swapfile ]; then sudo fallocate -l $HOMESERVER_SWAP_SIZE /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo "# Swap file" | sudo tee -a /etc/fstab > /dev/null 2>&1 echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1 fi } function updateLogrotateConfig() { ulrc_curE=${-//[^e]/} set +e # Set max size of syslog to 5G grep maxsize /etc/logrotate.d/rsyslog > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo sed -i "/weekly$/a\ maxsize 5G" /etc/logrotate.d/rsyslog fi # Move logrotate into hourly if [ -f /etc/cron.daily/logrotate ]; then sudo mv /etc/cron.daily/logrotate /etc/cron.hourly fi # Set conditions for docker container logs sudo tee /etc/logrotate.d/docker >/dev/null < $HOME/88-hshq #!/bin/bash echo echo " #===============================================================#" echo " # ▀█ █ ▄▄ ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄ █ █ █▀▀█ #" echo " # █▀▀█ █ █ █ █ █ ▄▄ ▀▀▀▄▄ ▄▄ ▄▄▄▀ ▀▄ ▄▀ ▄▄ ▄▄▄▀ █▀▀█ █ █ #" echo " # █ █ ▀▄▄▀ █ █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄ ▀█▀ ▄▄▄ ▄▄▄▄ █ █ █▄▄█▄ #" echo " #===============================================================#" echo echo " Linux OS: \$(lsb_release -d | cut -d":" -f2 | cut -d"(" -f1 | xargs)" if [ -f $HSHQ_LIB_SCRIPT ]; then echo " HSHQ Version: \$(sed -n 2p $HSHQ_LIB_SCRIPT | cut -d'=' -f2)" fi printf " Memory Usage: %.1f%% of \$(free -h | awk '/Mem:/{print \$2}')\n" \$((10000 - \$((10**4 * \$(grep "MemAvailable" /proc/meminfo | xargs | cut -d" " -f2) / \$(grep "MemTotal" /proc/meminfo | xargs | cut -d" " -f2)))))e-2 totSwap=\$(grep "SwapTotal" /proc/meminfo | xargs | cut -d" " -f2) if [ \$totSwap -gt 0 ]; then printf " Swap Usage: %.1f%% of \$(free -h | awk '/Swap:/{print \$2}')\n" \$((10000 - \$((10**4 * \$(grep "SwapFree" /proc/meminfo | xargs | cut -d" " -f2) / \$totSwap))))e-2 fi echo echo " Disks: " echo " -----------------------------------------------------------------" echo " Filesystem Size Used Avail Use% Mounted on" echo " -----------------------------------------------------------------" df -hP | grep "^/dev" | awk '{printf " %-21s %-7s %-7s %-7s %-7s %-8s\n", \$1, \$2, \$3, \$4, \$5, \$6}' echo # Let's only show zombie count if greater than 10 zombie_count=\$(ps aux | grep "defunct" | wc -l) if [ \$zombie_count -gt 10 ]; then echo " => There are \$zombie_count zombie processes." echo fi echo " Enter 'bash $HSHQ_WRAP_FILENAME' to run the HomeServerHQ script." echo EOFMD chmod 755 $HOME/88-hshq sudo chown root:root $HOME/88-hshq sudo chmod -x /etc/update-motd.d/* sudo mv $HOME/88-hshq /etc/update-motd.d/ if [ -f /etc/motd ]; then sudo mv /etc/motd /etc/motd.old fi } function performSuggestedSecUpdates() { disa_curE=${-//[^e]/} set +e # This function continues the process of reducing the failed checks in the Wazuh SCA scan # It's mostly low hanging fruit, but reducing the list of exceptions will help to focus # on the more important ones. grep "enabled=1" /etc/default/apport > /dev/null 2>&1 if [ $? -eq 0 ]; then sudo sed -i "s/enabled=1/enabled=0/" /etc/default/apport sudo systemctl stop apport sudo systemctl --now disable apport sudo systemctl daemon-reload #sudo systemctl mask apport fi echo "Authorized uses only." | sudo tee /etc/issue > /dev/null 2>&1 echo "Authorized uses only." | sudo tee /etc/issue.net > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y telnet > /dev/null 2>&1 # Assume the caller will restart sshd - don't want to risk breaking the installation process sudo sed -i "s/^#MaxAuthTries.*/MaxAuthTries 4/" /etc/ssh/sshd_config sudo sed -i "s/^#Banner none.*/Banner \/etc\/issue.net/" /etc/ssh/sshd_config sudo sed -i "s/^#MaxStartups.*/MaxStartups 10:30:60/" /etc/ssh/sshd_config sudo sed -i "s/^#LoginGraceTime.*/LoginGraceTime 60/" /etc/ssh/sshd_config sudo sed -i "s/^#AllowTcpForwarding yes.*/AllowTcpForwarding no/" /etc/ssh/sshd_config sudo chown root:root /etc/crontab sudo chmod og-rwx /etc/crontab sudo chown root:root /etc/cron.hourly/ sudo chmod og-rwx /etc/cron.hourly/ sudo chown root:root /etc/cron.daily/ sudo chmod og-rwx /etc/cron.daily/ sudo chown root:root /etc/cron.weekly/ sudo chmod og-rwx /etc/cron.weekly/ sudo chown root:root /etc/cron.monthly/ sudo chmod og-rwx /etc/cron.monthly/ sudo chown root:root /etc/cron.d/ sudo chmod og-rwx /etc/cron.d/ if ! [ -z "$disa_curE" ]; then set -e fi } function performAptInstall() { if [ "$1" = "yq" ]; then installYQ return fi sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' $1 } function installYQ() { sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 sudo chmod a+x /usr/local/bin/yq } function installDependencies() { set +e createHSHQLog sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1 upgradeHostOS sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1 # Install utils installHostNTPServer for util in $UTILS_LIST; do if [[ "$(isProgramInstalled $util)" = "false" ]]; then lib_name=$(echo $util | cut -d"|" -f2) echo "Installing $lib_name, please wait..." performAptInstall $lib_name fi done if ! [ -f /etc/rsyslog.d/docker-logs.conf ]; then # Install Rsyslog sudo systemctl status rsyslog > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "Installing rsyslog, please wait..." performAptInstall rsyslog sudo systemctl enable rsyslog sudo systemctl start rsyslog fi fi # Some host tuning updateSysctl true updateLogrotateConfig sudo sed -i "s|^ClientAliveInterval .*$|ClientAliveInterval 15|g" /etc/ssh/sshd_config sudo sed -i "s|^ClientAliveCountMax .*$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config sudo sed -i "s|^PermitEmptyPasswords .*$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config sudo sed -i "s|^#*ClientAliveInterval .*$|ClientAliveInterval 15|g" /etc/ssh/sshd_config sudo sed -i "s|^#*ClientAliveCountMax .*$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config sudo sed -i "s|^#*PermitEmptyPasswords .*$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config sudo sed -i "s/^#DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/system.conf sudo sed -i "s/^#DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/user.conf sudo sed -i "s/^DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/system.conf sudo sed -i "s/^DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/user.conf sudo grep "* - nofile" /etc/security/limits.conf >/dev/null if [ $? -ne 0 ]; then echo "* - nofile 65536" | sudo tee -a /etc/security/limits.conf >/dev/null fi sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target > /dev/null 2>&1 if [ "$IS_DESKTOP_ENV" = "true" ]; then for util in $DESKTOP_APT_LIST; do if [[ "$(isProgramInstalled $util)" = "false" ]]; then lib_name=$(echo $util | cut -d"|" -f2) echo "Installing $lib_name, please wait..." performAptInstall $lib_name fi done fi installDocker curl -fsSL https://gvisor.dev/archive.key | sudo gpg --yes --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null 2>&1 sudo apt-get update > /dev/null 2>&1 performAptInstall runsc > /dev/null 2>&1 for rem_util in $APT_REMOVE_LIST; do sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y $rem_util done sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y set -e } function removeBSDMailx() { sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y bsd-mailx sudo tee /etc/apt/preferences.d/bsd-mailx >/dev/null < /dev/null 2>&1 } function installMailUtils() { set +e echo "Removing bsd-mailx, please wait..." removeBSDMailx echo "Installing ssmtp, please wait..." performAptInstall ssmtp echo "Installing mailx, please wait..." performAptInstall mailutils sudo tee /etc/ssmtp/ssmtp.conf >/dev/null </dev/null </dev/null || sudo groupadd mailsenders sudo usermod -aG mailsenders $USERNAME sudo chown root:mailsenders /usr/bin/mail.mailutils sudo update-alternatives --set mailx /usr/bin/mail.mailutils } function installSQLite344() { if [ -d $HSHQ_NONBACKUP_DIR/sqlite344 ]; then return fi echo "Installing sqlite (v3.44) from source, this will take a few minutes..." cd $HSHQ_NONBACKUP_DIR wget https://www.sqlite.org/2023/sqlite-autoconf-3440000.tar.gz > /dev/null 2>&1 tar xvfz sqlite-autoconf-3440000.tar.gz > /dev/null 2>&1 rm -f sqlite-autoconf-3440000.tar.gz sudo mv sqlite-autoconf-3440000 sqlite344 cd sqlite344 ./configure > /dev/null 2>&1 make -j $(nproc) > /dev/null 2>&1 sudo make install > /dev/null 2>&1 cd ~ $HSHQ_NONBACKUP_DIR/sqlite344/sqlite3 --version > /dev/null 2>&1 rtv=$? if [ $rtv -ne 0 ]; then return $rtv else echo "Sqlite (v3.44) installation complete!" fi } function checkLoadConfig() { set -e if ! [ -z "$ENC_CONFIG_FILE" ] && ! [ -f "$ENC_CONFIG_FILE" ]; then ENC_CONFIG_FILE="" fi if [ -z "$CONFIG_FILE" ] || ! [ -f "$CONFIG_FILE" ]; then DEF_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME if [ -f $DEF_CONFIG_FILE ]; then CONFIG_FILE=$DEF_CONFIG_FILE ENC_CONFIG_FILE="" else CONFIG_FILE="" fi fi if [ -z "$ENC_CONFIG_FILE" ] && [ -z "$CONFIG_FILE" ]; then if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME fi fi set +e if ! [ -z "$ENC_CONFIG_FILE" ]; then showYesNoMessageBox "Encrypted Config Found" "Encrypted configuration file found. Do you wish to decrypt it?" if [ $? -ne 0 ]; then releaseLock hshqopen "checkLoadConfig" false exit 1 fi # Prompt for config password, test it. # Check if sudo, if not, get it and test it. # If either fails, exit. confirmUnlockCreds if [ $? -ne 0 ]; then releaseLock hshqopen "checkLoadConfig" false exit 1 fi decryptConfigFile "$ENC_CONFIG_FILE" ENC_CONFIG_FILE="" fi confirmUnlockCreds if [ $? -ne 0 ]; then releaseLock hshqopen "checkLoadConfig" false exit 1 fi set -e if ! [ -z "$CONFIG_FILE" ] && [ -f $CONFIG_FILE ]; then loadConfigVars true fi } function loadConfigConsole() { ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME if ! [ -f $ENC_CONFIG_FILE ]; then echo "Config file does not exist, exiting..." exit 1 fi if ! [ -z "$USER_CONFIG_PW" ]; then checkDecryptConfigFile > /dev/null 2>&1 if [ $? -ne 0 ]; then USER_CONFIG_PW="" fi fi while [ -z "$USER_CONFIG_PW" ] do read -r -s -p "Enter your config decrypt password: " USER_CONFIG_PW retVal=$? if [ $retVal -ne 0 ]; then return 3 fi checkDecryptConfigFile > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "The password is incorrect, please re-enter it." USER_CONFIG_PW="" fi done decryptAndLoad false } function confirmUnlockCreds() { set +e refreshSudo if [ $? -ne 0 ]; then return 3 fi if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME IS_CONFIG_FILE_UNENCRYPTED=true return fi if ! [ -f $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then # Assume this is pre-install, so just return return fi promptTestDecryptConfigFile if [ $? -ne 0 ]; then return 3 fi } function refreshSudo() { rsu_curE=${-//[^e]/} set +e sudo -k if ! [ -z "$USER_SUDO_PW" ]; then echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ $? -ne 0 ]; then USER_SUDO_PW="" fi fi while [ -z "$USER_SUDO_PW" ] do if [ "$IS_CONSOLE_ENV" = "true" ]; then USER_SUDO_PW=$(promptPasswordMenu "Enter Password" "Enter the sudo password for $USERNAME: ") retVal=$? if [ $retVal -ne 0 ]; then exit 3 fi echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ $? -ne 0 ]; then showMessageBox "Incorrect Password" "The password is incorrect, please re-enter it." USER_SUDO_PW="" fi else echo read -r -s -p "Enter the sudo password for $USERNAME: " USER_SUDO_PW echo echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "The password is incorrect, please re-enter it." USER_SUDO_PW="" fi fi done if ! [ -z "$rsu_curE" ]; then set -e fi } function checkConfigAvailable() { if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME IS_CONFIG_FILE_UNENCRYPTED=true return fi if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME IS_CONFIG_FILE_UNENCRYPTED=false return fi } function initConfig() { if [ "$IS_INSTALLING" = "true" ]; then showMessageBox "Active Installation" "There is an active installation in progress, exiting..." exit 1 fi if [ "$IS_INSTALLED" = "true" ]; then return 0 fi if [[ "$(isProgramInstalled sqlite3)" = "false" ]]; then echo "Installing sqlite3, please wait..." performAptInstall sqlite3 > /dev/null 2>&1 fi if [[ "$(isProgramInstalled sipcalc)" = "false" ]]; then echo "Installing sipcalc, please wait..." performAptInstall sipcalc > /dev/null 2>&1 fi if [[ "$(isProgramInstalled jq)" = "false" ]]; then echo "Installing jq, please wait..." performAptInstall jq > /dev/null 2>&1 fi if [ -z "$CONFIG_FILE" ]; then # Create new directories and config file createInitialEnv loadConfigVars true fi set +e if [ -z "$IS_ACCEPT_DEFAULTS" ]; then showYesNoMessageBox "Accept Defaults?" "Do you wish to use defaults where applicable?" mbres=$? if [ $mbres -eq 0 ]; then IS_ACCEPT_DEFAULTS=yes else IS_ACCEPT_DEFAULTS=no fi fi total_ram=$(free --giga | grep "Mem:" | xargs | cut -d" " -f2) if [ $total_ram -lt 4 ]; then showMessageBox "Insufficient Memory" "Total memory is $total_ram GB. You must have at least 4 GB to perform the installation, exiting..." exit 1 elif [ $total_ram -lt 8 ]; then showYesNoMessageBox "Insufficient Memory" "Total memory is $total_ram GB. You should have at least 8 GB to perform the installation. The process could fail or your server could crash. Are you sure you want to continue?" mbres=$? if [ $mbres -ne 0 ]; then exit 1 fi fi # Need to allow for docker images already downloaded, so look at total disk space. # Obviously 120 != 150, but this allows a little leeway. # The partitioning structure could be different than expected, so user can bypass this warning. total_disk_space=$(($(df / | tail -1 | xargs | cut -d" " -f2) / 1048576)) if [ $total_disk_space -lt 11 ]; then showMessageBox "Insufficient Disk Space" "You must have at least 16GB of disk space to perform the base installation, exiting..." exit 1 elif [ $total_disk_space -lt 120 ]; then showYesNoMessageBox "Insufficient Disk Space" "Total size of disk is $total_disk_space GB. You should have at least 150 GB of available space to perform the installation. The process could fail or your server could crash. Are you sure you want to continue?" mbres=$? if [ $mbres -ne 0 ]; then exit 1 fi fi if [[ "$(isProgramInstalled grepcidr)" = "false" ]]; then echo "Installing grepcidr, please wait..." performAptInstall grepcidr > /dev/null 2>&1 fi is_add_error=false default_iface=$(getDefaultIface) add_interface="" while [ -z "$add_interface" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ] && [ "$is_add_error" = "false" ]; then add_interface=$default_iface else add_interface=$(promptUserInputMenu "$default_iface" "Enter Primary Interface" "Enter the primary network interface. The interface for the default route has been entered for you.") if [ $? -ne 0 ]; then exit 5 fi if [ -z "$add_interface" ]; then showMessageBox "Interface Error" "The interface cannot be empty." add_interface="" continue elif [ $(checkValidStringUpperLowerNumbers "$add_interface" "-") = "false" ]; then showMessageBox "Interface Error" "The interface contains invalid characters. It must consist of a-z (upper or lowercase), 0-9, and/or -" add_interface="" continue elif [ $(getStringLength "$add_interface") -gt 15 ]; then showMessageBox "Interface Error" "The interface name is too long, it must be 15 characters or less." add_interface="" continue fi fi set +e addHSInterface $add_interface true retVal=$? if [ $retVal -ne 0 ] && [ $retVal -ne 1 ]; then ip_addr=$(getIPAddressOfInterface $add_interface) is_private=$(checkIsIPPrivate "$ip_addr") chk_subnet=$(getSubnetOfInterface "$add_interface" "$ip_addr" "$is_private") if ! [ -z "$chk_subnet" ] && [ "$(checkNetworkIntersect $chk_subnet 10.0.0.0/8)" = "true" ] && [ $(echo "$chk_subnet" | cut -d"/" -f2) -eq 8 ]; then showMessageBox "Default Interface Error" "You are using the ENTIRE 10.0.0.0/8 network for your home network - a terribly poor design choice. You will not be able to host a VPN or network with anyone. If you want to bypass these safeguards and proceed, then change your settings in /etc/netplan to a static IP and a smaller network and restart the installation." exit 6 else showMessageBox "Default Interface Error" "There was an error with the default interface" fi add_interface="" is_add_error=true fi set -e done if [ "$(checkIsIPPrivate $HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" = "false" ]; then if [ -z "$CONNECTING_IP" ]; then showMessageBox "Error with Connecting IP" "Could not determine connecting IP, exiting..." exit 1 else addHomeNetIP ${CONNECTING_IP}/32 false fi fi if [ -z "$USERID" ]; then USERID=$(id -u) updatePlaintextRootConfigVar USERID $USERID fi if [ -z "$GROUPID" ]; then GROUPID=$(id -g) updatePlaintextRootConfigVar GROUPID $GROUPID fi if [ -z "$XDG_RUNTIME_DIR" ]; then XDG_RUNTIME_DIR="/run/user/$USERID" updateConfigVar XDG_RUNTIME_DIR $XDG_RUNTIME_DIR fi if ! [ "$IS_AUTO_INSTALL" = "true" ] && [ "$HSHQ_APP_TYPE" = "home" ]; then selecttypemenu=$(cat << EOF $(getLogo) Is this server for your home or business? EOF ) menures=$(whiptail --title "Select an option" --menu "$selecttypemenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Home" \ "2" "Business" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=1 fi case $menures in 1) HSHQ_APP_TYPE=home ;; 2) HSHQ_APP_TYPE=business ;; esac fi updatePlaintextRootConfigVar HSHQ_APP_TYPE $HSHQ_APP_TYPE if [ -z "$DISABLED_SERVICES" ]; then if [ "$HSHQ_APP_TYPE" = "business" ]; then if [ $total_ram -lt 8 ]; then DISABLED_SERVICES=$DS_MEM_LOW elif [ $total_ram -lt 12 ]; then DISABLED_SERVICES=$BDS_MEM_12 elif [ $total_ram -lt 16 ]; then DISABLED_SERVICES=$BDS_MEM_16 elif [ $total_ram -lt 22 ]; then DISABLED_SERVICES=$BDS_MEM_22 elif [ $total_ram -lt 28 ]; then DISABLED_SERVICES=$BDS_MEM_28 else DISABLED_SERVICES=$BDS_MEM_HIGH fi else if [ $total_ram -lt 8 ]; then DISABLED_SERVICES=$DS_MEM_LOW elif [ $total_ram -lt 12 ]; then DISABLED_SERVICES=$DS_MEM_12 elif [ $total_ram -lt 16 ]; then DISABLED_SERVICES=$DS_MEM_16 elif [ $total_ram -lt 22 ]; then DISABLED_SERVICES=$DS_MEM_22 elif [ $total_ram -lt 28 ]; then DISABLED_SERVICES=$DS_MEM_28 else DISABLED_SERVICES=$DS_MEM_HIGH fi fi updatePlaintextRootConfigVar DISABLED_SERVICES $DISABLED_SERVICES fi replaceType="HomeServer" if [ "$HSHQ_APP_TYPE" = "business" ]; then replaceType="company" fi while [ -z "$HOMESERVER_DOMAIN" ] do HOMESERVER_DOMAIN=$(promptUserInputMenu "" "Enter Domain" "Enter your $replaceType domain name, i.e. example.com. You must own this domain to route external emails and/or post services on the public internet: ") if [ -z "$HOMESERVER_DOMAIN" ]; then showMessageBox "Domain Empty" "The domain cannot be empty" elif [ $(checkValidString "$HOMESERVER_DOMAIN" ".-") = "false" ]; then showMessageBox "Invalid Character(s)" "The domain contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or -." HOMESERVER_DOMAIN="" elif [ $(checkValidBaseDomain "$HOMESERVER_DOMAIN") = "false" ]; then showMessageBox "Invalid Domain Name" "Invalid domain name. The base domain must be of the format 'example.com'. It cannot start or end with a period." HOMESERVER_DOMAIN="" elif [ $(getDomainTLD "$HOMESERVER_DOMAIN") = "test" ]; then showMessageBox "Invalid Domain" "The specific TLD .test actually causes some issues. Change it to .tester or something else." HOMESERVER_DOMAIN="" fi done updatePlaintextRootConfigVar HOMESERVER_DOMAIN $HOMESERVER_DOMAIN new_hostname="HomeServer-$(echo $HOMESERVER_DOMAIN | sed 's/\./-/g')" if [ -z "$(cat /etc/hosts | grep $new_hostname)" ]; then echo "127.0.1.1 $new_hostname" | sudo tee -a /etc/hosts fi sudo hostnamectl set-hostname $new_hostname rm -f $HOME/dead.letter EXT_DOMAIN_PREFIX="" while [ -z "$EXT_DOMAIN_PREFIX" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then EXT_DOMAIN_PREFIX="external" else EXT_DOMAIN_PREFIX=$(promptUserInputMenu "external" "External Domain Prefix" "Enter the prefix for your domain to be reached externally:") fi if [ -z "$EXT_DOMAIN_PREFIX" ]; then showMessageBox "Prefix Empty" "The Prefix cannot be empty" elif [ $(checkValidString "$EXT_DOMAIN_PREFIX" "-") = "false" ]; then showMessageBox "Invalid Character(s)" "The prefix contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or -" EXT_DOMAIN_PREFIX="" else updatePlaintextRootConfigVar EXT_DOMAIN_PREFIX $EXT_DOMAIN_PREFIX fi done INT_DOMAIN_PREFIX="" while [ -z "$INT_DOMAIN_PREFIX" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then INT_DOMAIN_PREFIX="internal" else INT_DOMAIN_PREFIX=$(promptUserInputMenu "internal" "Internal Domain Prefix" "Enter the prefix for your RelayServer to be reached internally (if applicable):") fi if [ -z "$INT_DOMAIN_PREFIX" ]; then showMessageBox "Prefix Empty" "The Prefix cannot be empty" elif [ $(checkValidString "$INT_DOMAIN_PREFIX" "-") = "false" ]; then showMessageBox "Invalid Character(s)" "The prefix contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or -" INT_DOMAIN_PREFIX="" else updatePlaintextRootConfigVar INT_DOMAIN_PREFIX $INT_DOMAIN_PREFIX fi done while [ -z "$HOMESERVER_NAME" ] do HOMESERVER_NAME=$(promptUserInputMenu "" "Enter Name" "Enter your $replaceType name with desired formatting, i.e. capitalization, spaces, etc. No special characters except for commas, hyphens, or periods. This will appear in window titles, certificates, among other things: ") if [ -z "$HOMESERVER_NAME" ]; then showMessageBox "Name Empty" "The name cannot be empty" elif [ $(checkValidStringUpperLowerNumbers "$HOMESERVER_NAME" "[:space:],.-") = "false" ]; then showMessageBox "Invalid Character(s)" "The name contains invalid character(s)." HOMESERVER_NAME="" fi done updatePlaintextRootConfigVar HOMESERVER_NAME "$HOMESERVER_NAME" while [ -z "$HOMESERVER_ABBREV" ] do HOMESERVER_ABBREV=$(promptUserInputMenu "" "Enter Abbrev" "Enter an abbreviation for your $replaceType, 3 to 10 lowercase alpha-numeric characters. This will be used as a default prefix for admin usernames as well as some other uses: ") if [ -z "$HOMESERVER_ABBREV" ]; then showMessageBox "Abbrev Empty" "The abbreviation cannot be empty." elif [ $(checkValidString "$HOMESERVER_ABBREV") = "false" ]; then showMessageBox "Invalid Character(s)" "The abbreviation contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9" HOMESERVER_ABBREV="" elif [ ${#HOMESERVER_ABBREV} -lt 3 ] || [ ${#HOMESERVER_ABBREV} -gt 10 ]; then showMessageBox "Invalid Length" "The abbreviation must be 3 to 10 characters" HOMESERVER_ABBREV="" fi done updatePlaintextRootConfigVar HOMESERVER_ABBREV $HOMESERVER_ABBREV while [ -z "$HOMESERVER_CURRENCY_CODE" ] do if [ "$IS_AUTO_INSTALL" = "true" ]; then HOMESERVER_CURRENCY_CODE="USD" break fi HOMESERVER_CURRENCY_CODE=$(promptUserInputMenu "USD" "Enter Currency Code" "Enter your currency code: ") if [ -z "$HOMESERVER_CURRENCY_CODE" ]; then showMessageBox "Code Empty" "The code cannot be empty" elif [ $(checkValidCurrencyCode "$HOMESERVER_CURRENCY_CODE") = "false" ]; then showMessageBox "Invalid Code" "The code is invalid." HOMESERVER_CURRENCY_CODE="" fi done updatePlaintextRootConfigVar HOMESERVER_CURRENCY_CODE "$HOMESERVER_CURRENCY_CODE" priorTZ=$(readlink /etc/localtime | sed 's|.*/zoneinfo/||') while [ -z "$TZ" ] do TZ=$(promptUserInputMenu "$priorTZ" "Enter Time Zone" "Enter your time zone. For a list of all time zones, visit https://en.wikipedia.org/wiki/List_of_tz_database_time_zones and enter the value in the [TZ database name] column: ") if [ $? -ne 0 ]; then exit 2 fi if [ -z "$TZ" ]; then showMessageBox "Time Zone Empty" "The time zone cannot be empty" elif [ $(checkValidStringUpperLowerNumbers "$TZ" "/_+-") = "false" ]; then showMessageBox "Invalid Character(s)" "The timezone contains invalid character(s)." TZ="" else set +e sudo timedatectl set-timezone "$TZ" > /dev/null 2>&1 if [ $? -ne 0 ]; then showMessageBox "Invalid Time Zone" "The time zone is invalid. Please enter a valid time zone." TZ="" sudo timedatectl set-timezone "$priorTZ" > /dev/null 2>&1 fi set -e fi done updatePlaintextRootConfigVar TZ $TZ echo "$TZ" | sudo tee /etc/timezone > /dev/null 2>&1 sudo chmod 0644 /etc/timezone set +e CURRENT_SSH_PORT=$(grep "^Port" /etc/ssh/sshd_config) if [ $? -eq 0 ]; then CURRENT_SSH_PORT=$(echo $CURRENT_SSH_PORT | cut -d " " -f2) if ! [ "$CURRENT_SSH_PORT" = "22" ]; then SSH_PORT=$CURRENT_SSH_PORT fi else CURRENT_SSH_PORT=22 fi updatePlaintextRootConfigVar CURRENT_SSH_PORT $CURRENT_SSH_PORT while [ -z "$SSH_PORT" ] do SSH_PORT=$(promptUserInputMenu $((9000 + $RANDOM % 999)) "Enter SSH Port" "It is highly advised to change your default SSH port (22), since bots will constantly probe port 22. A random port between 9000-9999 has been generated for you. Ensure you remember this port in order to log back in.") if [ -z "$SSH_PORT" ]; then showMessageBox "SSH Port Empty" "The SSH port cannot be empty" elif [ "$(checkValidNumber $SSH_PORT)" = "false" ]; then showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number." SSH_PORT="" elif [ $SSH_PORT -eq 22 ]; then # No change, we'll allow it showMessageBox "Huh" "Okay, whatever...you do you." SSH_PORT="22" elif [ $SSH_PORT -lt 1024 ] || [ $SSH_PORT -gt 65536 ]; then showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536." SSH_PORT="" else checkAvailableHomeServerPort $SSH_PORT if [ $? -ne 0 ]; then showMessageBox "Invalid Port" "The port is invalid. It either conflicts with an existing used port or is less than 1024." SSH_PORT="" fi fi done updatePlaintextRootConfigVar SSH_PORT $SSH_PORT # Change SSH port on host sudo sed -i "s|^#*Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config sudo sed -i "s|^Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config while [ -z "$PORTAINER_LOCAL_HTTPS_PORT" ] || [ "$PORTAINER_LOCAL_HTTPS_PORT" = "$SSH_PORT" ] do tmp_port=$((9000 + $RANDOM % 999)) while [ "$tmp_port" = "$SSH_PORT" ] do tmp_port=$((9000 + $RANDOM % 999)) done if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then PORTAINER_LOCAL_HTTPS_PORT=$tmp_port else PORTAINER_LOCAL_HTTPS_PORT=$(promptUserInputMenu $tmp_port "Enter Local Portainer Port" "Enter a local port to use for portainer. A random port between 9000-9999 has been generated for you.") if [ -z "$PORTAINER_LOCAL_HTTPS_PORT" ] || [ "$PORTAINER_LOCAL_HTTPS_PORT" = "$SSH_PORT" ]; then showMessageBox "Portainer Port Error" "The port cannot be empty or the same as the SSH port" elif [ "$(checkValidNumber $PORTAINER_LOCAL_HTTPS_PORT)" = "false" ]; then showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number." PORTAINER_LOCAL_HTTPS_PORT="" elif [ $PORTAINER_LOCAL_HTTPS_PORT -lt 1024 ] || [ $PORTAINER_LOCAL_HTTPS_PORT -gt 65536 ]; then showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536." PORTAINER_LOCAL_HTTPS_PORT="" fi if ! [ -z "$PORTAINER_LOCAL_HTTPS_PORT" ]; then checkAvailableHomeServerPort $PORTAINER_LOCAL_HTTPS_PORT if [ $? -ne 0 ]; then showMessageBox "Invalid Port" "The port is invalid. It either conflicts with an existing used port or is less than 1024." PORTAINER_LOCAL_HTTPS_PORT="" fi fi fi updatePlaintextRootConfigVar PORTAINER_LOCAL_HTTPS_PORT $PORTAINER_LOCAL_HTTPS_PORT DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT=$DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT,$PORTAINER_LOCAL_HTTPS_PORT updatePlaintextUserConfigVar DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT $DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT done set -e # Do the boring things that must be done while ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_USERNAME" ] do FIRST_USER_USERNAME=$(promptUserInputMenu "" "Enter First User" "Enter the username for your first LDAP/email account: ") if [ -z "$FIRST_USER_USERNAME" ]; then showMessageBox "Username Empty" "The username cannot be empty" elif [ $(checkValidString "$FIRST_USER_USERNAME") = "false" ]; then showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9" FIRST_USER_USERNAME="" fi done curFullName="$(echo $(getent passwd $USERNAME) | cut -d":" -f5 | cut -d"," -f1)" if [ -z "$curFullName" ]; then curFullName="${FIRST_USER_USERNAME^}" fi while ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_FIRSTNAME" ] do FIRST_USER_FIRSTNAME=$(promptUserInputMenu "$curFullName" "Enter First Name" "Enter the first name for your first LDAP/email user account: ") if [ -z "$FIRST_USER_FIRSTNAME" ]; then showMessageBox "Name Empty" "The name cannot be empty" elif [ $(checkValidStringUpperLowerNumbers "$FIRST_USER_FIRSTNAME" "[:space:].,-") = "false" ]; then showMessageBox "Invalid Character(s)" "The name contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, periods, commas, and/or hyphens." FIRST_USER_FIRSTNAME="" fi done while ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_LASTNAME" ] do FIRST_USER_LASTNAME=$(promptUserInputMenu "" "Enter Last Name" "Enter the last name for your first LDAP/email user account: ") if [ -z "$FIRST_USER_LASTNAME" ]; then showMessageBox "Name Empty" "The name cannot be empty" elif [ $(checkValidStringUpperLowerNumbers "$FIRST_USER_LASTNAME" "[:space:].,-") = "false" ]; then showMessageBox "Invalid Character(s)" "The name contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, periods, commas, and/or hyphens." FIRST_USER_LASTNAME="" fi done if ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_PASSWORD" ]; then tmp_pw1="" tmp_pw2="" set +e while [ -z "$tmp_pw1" ] || ! [ "$tmp_pw1" = "$tmp_pw2" ] do tmp_pw1=$(promptPasswordMenu "Create Password" "Create a password for your LDAP/email user ($FIRST_USER_USERNAME) account: ") if [ $? -ne 0 ]; then exit; fi if [ -z "$tmp_pw1" ]; then showMessageBox "Password Empty" "The password cannot be empty, please try again." tmp_pw1=abc tmp_pw2=def continue fi if [ $(checkValidPassword "$tmp_pw1" 8) = "false" ]; then showMessageBox "Password Invalid" "The password is too short or contains invalid characters. It must contain at least 8 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.," tmp_pw1=abc tmp_pw2=def continue fi tmp_pw2=$(promptPasswordMenu "Confirm Password" "Enter the password again to confirm: ") if [ $? -ne 0 ]; then exit; fi if ! [ "$tmp_pw1" = "$tmp_pw2" ]; then showMessageBox "Password Mismatch" "The passwords do not match, please try again." fi done FIRST_USER_PASSWORD="$tmp_pw1" tmp_pw1="" tmp_pw2="" set -e fi while [ -z "$ADMIN_USERNAME_BASE" ] || [ "$ADMIN_USERNAME_BASE" = "admin" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then ADMIN_USERNAME_BASE=$HOMESERVER_ABBREV"admin" else ADMIN_USERNAME_BASE=$(promptUserInputMenu $HOMESERVER_ABBREV"admin" "Enter Admin Base" "Enter the base admin username (it cannot be \"admin\"): ") fi if [ $(checkValidString "$ADMIN_USERNAME_BASE") = "false" ]; then showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9" ADMIN_USERNAME_BASE="" fi updatePlaintextRootConfigVar ADMIN_USERNAME_BASE $ADMIN_USERNAME_BASE done if [[ "$(isProgramInstalled pwgen)" = "false" ]]; then echo "Installing pwgen, please wait..." performAptInstall pwgen > /dev/null 2>&1 fi if [[ "$(isProgramInstalled uuidgen)" = "false" ]]; then echo "Installing uuidgen, please wait..." performAptInstall uuid-runtime > /dev/null 2>&1 fi while [ -z "$EMAIL_ADMIN_USERNAME" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then EMAIL_ADMIN_USERNAME="mailmanager" else EMAIL_ADMIN_USERNAME=$(promptUserInputMenu "mailmanager" "Enter Admin Email" "Enter the admin email username: ") fi if [ $(checkValidString "$EMAIL_ADMIN_USERNAME") = "false" ]; then showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9" EMAIL_ADMIN_USERNAME="" fi updateConfigVar EMAIL_ADMIN_USERNAME $EMAIL_ADMIN_USERNAME done while [ -z "$EMAIL_JOINT_USERNAME" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then EMAIL_JOINT_USERNAME="info" else EMAIL_JOINT_USERNAME=$(promptUserInputMenu "info" "Enter Shared Email" "Enter the shared email username: ") fi if [ $(checkValidString "$EMAIL_JOINT_USERNAME") = "false" ]; then showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9" EMAIL_JOINT_USERNAME="" fi updateConfigVar EMAIL_JOINT_USERNAME $EMAIL_JOINT_USERNAME done initServicesCredentials addUserShareDirectories $NEXTCLOUD_ADMIN_USERNAME addUserShareDirectories $SPEAKR_ADMIN_USERNAME } function initInstallation() { if [[ "$(isProgramInstalled screen)" = "false" ]]; then echo "Installing screen, please wait..." performAptInstall screen > /dev/null 2>&1 fi if [[ "$(isProgramInstalled nano)" = "false" ]]; then echo "Installing nano, please wait..." performAptInstall nano > /dev/null 2>&1 fi # Show info to user and prompt for confirmation if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then rs_wg_user_conf=$(sudo cat $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf) fi clear strInstallConfig="" strInstallConfig="${strInstallConfig}################################################################\n" strInstallConfig="${strInstallConfig}========================= General Info =========================\n" strInstallConfig="${strInstallConfig}- Root CA URL: http://$SUB_FILES.$HOMESERVER_DOMAIN/ca.crt\n" strInstallConfig="${strInstallConfig}- Home Page: https://$SUB_HSHQHOME.$HOMESERVER_DOMAIN\n" strInstallConfig="${strInstallConfig}- Email Admin Username: $EMAIL_ADMIN_EMAIL_ADDRESS\n" strInstallConfig="${strInstallConfig}- Email Admin Password: $EMAIL_ADMIN_PASSWORD\n" strInstallConfig="${strInstallConfig}- HomeServer Current SSH Port: $CURRENT_SSH_PORT\n" strInstallConfig="${strInstallConfig}- HomeServer New SSH Port: $SSH_PORT\n" strInstallConfig="${strInstallConfig}- Portainer IP URL: https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$PORTAINER_LOCAL_HTTPS_PORT\n" strInstallConfig="${strInstallConfig}- Portainer Username: $PORTAINER_ADMIN_USERNAME\n" strInstallConfig="${strInstallConfig}- Portainer Password: $PORTAINER_ADMIN_PASSWORD\n" strInstallConfig="${strInstallConfig}- Script-server IP URL: https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$SCRIPTSERVER_LOCALHOST_PORT\n" strInstallConfig="${strInstallConfig}- Script-server Username: $SCRIPTSERVER_ADMIN_USERNAME\n" strInstallConfig="${strInstallConfig}- Script-server Password: $SCRIPTSERVER_ADMIN_PASSWORD\n" if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then strInstallConfig="${strInstallConfig}- RelayServer IP Address: $RELAYSERVER_SERVER_IP\n" strInstallConfig="${strInstallConfig}- RelayServer Current SSH Port: $RELAYSERVER_CURRENT_SSH_PORT\n" strInstallConfig="${strInstallConfig}- RelayServer New SSH Port: $RELAYSERVER_SSH_PORT\n" strInstallConfig="${strInstallConfig}- RelayServer Linux Username: $RELAYSERVER_REMOTE_USERNAME\n" fi strInstallConfig="${strInstallConfig}================================================================\n\n" if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] || [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then strInstallConfig="${strInstallConfig}$(getDNSRecordsInfo $HOMESERVER_DOMAIN)\n\n" fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then strInstallConfig="${strInstallConfig}================= User WireGuard Configuration =================\n" strInstallConfig="${strInstallConfig}""""$rs_wg_user_conf""""\n" strInstallConfig="${strInstallConfig}================================================================\n\n" fi #strInstallConfig="${strInstallConfig}=========================== Root CA ============================\n" #strInstallConfig="${strInstallConfig}""""$(cat "$HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt")""""\n" #strInstallConfig="${strInstallConfig}================================================================\n" strInstallConfig="${strInstallConfig} INSTRUCTIONS \n" strInstallConfig="${strInstallConfig}The system is prepped for installation. It will take around\n" strInstallConfig="${strInstallConfig}10-15 minutes to perform the base installation, depending on\n" strInstallConfig="${strInstallConfig}hardware specs and download speeds. Copy all of this info\n" strInstallConfig="${strInstallConfig}before proceeding. Ensure to scroll to the top and retain\n" strInstallConfig="${strInstallConfig}everything between the ##### borders. Simply select the\n" strInstallConfig="${strInstallConfig}information with your mouse, and it will automatically be\n" strInstallConfig="${strInstallConfig}copied to the clipboard.\n\n" strInstallConfig="${strInstallConfig}The process will run continuously uninterrupted in a 'screen'\n" strInstallConfig="${strInstallConfig}If you lose connection, then enter 'screen -r hshqInstall' to\n" strInstallConfig="${strInstallConfig}resume the screen if you want to continue to view the process.\n" strInstallConfig="${strInstallConfig}To exit the screen, press CTRL-a, then release both keys,\n" strInstallConfig="${strInstallConfig}then press d to detach.\n\n" strInstallConfig="${strInstallConfig}The system will automatically reboot upon completion.\n\n" strInstallConfig="${strInstallConfig}If you want to completely remove everything, run the utility\n" strInstallConfig="${strInstallConfig}inside this script (System Utils -> Uninstall and Remove\n" strInstallConfig="${strInstallConfig}Everything) and follow the prompts. To do the same on the\n" strInstallConfig="${strInstallConfig}RelayServer, enter the command 'bash nuke.sh', and follow\n" strInstallConfig="${strInstallConfig}the prompts.\n\n" if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then strInstallConfig="${strInstallConfig}The installation on the RelayServer will also automatically\n" strInstallConfig="${strInstallConfig}be initiated as well. Ensure to log in to the RelayServer\n" strInstallConfig="${strInstallConfig}as user ($RELAYSERVER_REMOTE_USERNAME) to monitor the progress of the installation,\n" strInstallConfig="${strInstallConfig}by entering the command 'screen -r hshqInstall'.\n\n" fi strInstallConfig="${strInstallConfig}################################################################\n\n" echo -e "${strInstallConfig}" while true; do echo -e "________________________________________________________________________\n" if [ "$IS_DESKTOP_ENV" = "true" ]; then hdir=/home/$USERNAME/Desktop is_keep_config=y echo -e "The above information has been saved to your Desktop," echo -e "i.e. $hdir/$HSHQ_INSTALL_NOTES_FILENAME." else hdir=/home/$USERNAME echo -e "Do you want to retain the above information in a file in the home directory," echo -e "i.e. ($hdir/$HSHQ_INSTALL_NOTES_FILENAME)? Note that this" echo -e "information is very sensitive and you should delete the file as soon" read -r -p "as you are finished with it. Enter 'y' or 'n': " is_keep_config fi if [ "$is_keep_config" = "y" ]; then final_prompt="After reading the above section, enter 'install' or 'exit': " rm -f "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" echo -e "\n" > "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" echo -e "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" echo -e "@@@@@@@@ You should permanently delete this file @@@@@@@@" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" echo -e "@@@@@@@@ as soon as you are finished with it @@@@@@@@" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" echo -e "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n\n" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" echo -e "${strInstallConfig}" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" chmod 0400 "$hdir/$HSHQ_INSTALL_NOTES_FILENAME" break elif [ "$is_keep_config" = "n" ]; then final_prompt="After reading/copying the above section, enter 'install' or 'exit': " break else echo "Unknown response, please try again." fi done while true; do if [ "$IS_AUTO_INSTALL" = "true" ]; then break fi echo -e "\n________________________________________________________________________\n" read -r -p "$final_prompt" is_install if [ "$is_install" = "install" ]; then break elif [ "$is_install" = "exit" ]; then echo "Exiting..." releaseLock hshqopen "initConfig" false exit 1 else echo "Unknown response, please try again." fi done if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "Starting installation on RelayServer..." sleep 1 isRSReady=false set +e countRSPreRetries=0 maxRSPreRetries=30 while true; do perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "sleep 1;if [ -f ~/$RELAYSERVER_NOT_READY_FILE ]; then exit 1; fi" -r 1 -b 1 if [ $? -eq 0 ]; then break fi if [ $countRSPreRetries -ge $maxRSPreRetries ]; then echo "Max retries reached. Something has likely gone wrong with the RelayServer pre-installation script, exiting..." exit 2 fi ((countRSPreRetries++)) echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for installation, sleeping 30 seconds then will retry..." sleep 30 done sleep 1 perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "bash $RS_INSTALL_FRESH_SCRIPT_NAME -s" -f -i "$USER_RELAY_SUDO_PW" fi set -e refreshSudo echo "Starting installation on HomeServer..." sleep 1 releaseLock hshqopen "preInstallation" false truncate -s 0 $HSHQ_LOG_FILE scName=hshqInstall initScreen $scName "bash $HSHQ_LIB_SCRIPT install $CONNECTING_IP" if [ $? -ne 0 ]; then showMessageBox "ERROR" "There was a problem starting the installation, exiting..." exit 1 fi screen -r $scName exit 0 } function setSudoTimeoutInstall() { sudo tee $HOME/$SUDO_LONG_TIMEOUT_FILENAME >/dev/null </dev/null updateHomeServerDNS $dns_file rm -f $dns_file docker container start heimdall >/dev/null fi fi draw_progress_bar 81 logHSHQEvent info "performBaseInstallation - Connect Services To VPN" connectPrimaryVPN true draw_progress_bar 83 echo "Waiting 30s for services to initialize..." sleep 30 fi draw_progress_bar 85 clearQueryLogAndStatsAdguardHS set +e if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then curl https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN > /dev/null 2>&1 if [ $? -eq 0 ]; then clearQueryLogAndStatsAdguardRS fi fi set -e logHSHQEvent info "performBaseInstallation - Post Installation" draw_progress_bar 86 checkUpdateAllIPTables performBaseInstallation-Late set -e draw_progress_bar 90 postInstallation install } function trustDesktopIcon() { set +e dtIcon=$1 which gio > /dev/null 2>&1 if [ $? -eq 0 ]; then gio set $HOME/Desktop/$dtIcon metadata::trusted true > /dev/null 2>&1 gio set -t string $HOME/Desktop/$dtIcon metadata::xfce-exe-checksum "$(sha256sum $HOME/Desktop/$dtIcon | awk '{print $1}')" > /dev/null 2>&1 dbus-launch --exit-with-session gio set $HOME/Desktop/$dtIcon metadata::trusted true > /dev/null 2>&1 dbus-launch --exit-with-session gio set -t string $HOME/Desktop/$dtIcon metadata::xfce-exe-checksum "$(sha256sum $HOME/Desktop/$dtIcon | awk '{print $1}')" > /dev/null 2>&1 nautilus -q > /dev/null 2>&1 fi } function postInstallation() { isInstallOrRestore="$1" refreshSudo echo "Performing post-installation tasks..." sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y avahi-daemon > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y avahi-autoipd > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt autoremove --purge -y if [ "$isInstallOrRestore" = "install" ]; then echo "Emailing Root CA..." sendRootCAEmail true draw_progress_bar 91 if ! [ "$IS_AUTO_INSTALL" = "true" ]; then emailFormattedCredentials true fi draw_progress_bar 95 fi # Need to wait until emails have been sent before changing permissions. sudo chmod 750 /usr/bin/mail.mailutils sudo rm -f /etc/skel/hshq.sh if [ "$IS_DESKTOP_ENV" = "true" ]; then echo "Configuring Desktop environment..." set +e if ! [ -f /usr/share/icons/HSHQ/Homepage.png ]; then sudo cp /usr/share/icons/HSHQ/HomepageHSHQ.png /usr/share/icons/HSHQ/Homepage.png fi rm -f ~/Desktop/InstallHSHQ.desktop rm -f ~/Desktop/HomePage.desktop cat < ~/Desktop/HomePage.desktop [Desktop Entry] Name=$HOMESERVER_NAME Exec=firefox $SUB_HSHQHOME.$HOMESERVER_DOMAIN Comment=Opens your HomeServer home page Terminal=false Icon=/usr/share/icons/HSHQ/Homepage.png Type=Application Categories=HomeServerHQ EOFHP chmod 755 ~/Desktop/HomePage.desktop rm -f ~/Desktop/HSHQConsole.desktop which terminator > /dev/null 2>&1 if [ $? -eq 0 ]; then # Specify terminator as console if possible cat < ~/Desktop/HSHQConsole.desktop [Desktop Entry] Name=HSHQ Console Util Exec=/usr/bin/terminator -x bash -ic "~/hshq.sh" Comment=Runs the HSHQ console-based management utility Icon=/usr/share/icons/HSHQ/ConsoleHSHQ.png Type=Application Categories=HomeServerHQ EOFHP else cat < ~/Desktop/HSHQConsole.desktop [Desktop Entry] Name=HSHQ Console Util Exec=bash -ic "~/hshq.sh" Terminal=true Comment=Runs the HSHQ console-based management utility Icon=/usr/share/icons/HSHQ/ConsoleHSHQ.png Type=Application Categories=HomeServerHQ EOFHP fi chmod 755 ~/Desktop/HSHQConsole.desktop rm -f ~/Desktop/HSHQScriptServer.desktop cat < ~/Desktop/HSHQScriptServer.desktop [Desktop Entry] Name=HSHQ Web Util Exec=firefox https://127.0.0.1:$SCRIPTSERVER_LOCALHOST_PORT Comment=Runs the HSHQ web-based management utility Terminal=false Icon=/usr/share/icons/HSHQ/ScriptServerHSHQ.png Type=Application Categories=HomeServerHQ EOFHP chmod 755 ~/Desktop/HSHQScriptServer.desktop rm -f ~/Desktop/HSHQHelp.desktop cat < ~/Desktop/HSHQHelp.desktop [Desktop Entry] Name=Help! Exec=firefox forum.homeserverhq.com Comment=Ask for help on the HomeServerHQ Forum Terminal=false Icon=/usr/share/icons/HSHQ/HelpHSHQ.png Type=Application Categories=HomeServerHQ EOFHP chmod 755 ~/Desktop/HSHQHelp.desktop trustDesktopIcon "HomePage.desktop" trustDesktopIcon "HSHQConsole.desktop" trustDesktopIcon "HSHQScriptServer.desktop" trustDesktopIcon "HSHQHelp.desktop" sudo mkdir -p /usr/share/applications sudo cp -f ~/Desktop/HomePage.desktop /usr/share/applications/ sudo cp -f ~/Desktop/HSHQConsole.desktop /usr/share/applications/ sudo cp -f ~/Desktop/HSHQScriptServer.desktop /usr/share/applications/ sudo cp -f ~/Desktop/HSHQHelp.desktop /usr/share/applications/ draw_progress_bar 96 which gsettings > /dev/null 2>&1 if [ $? -eq 0 ]; then # Spell check doesn't belong in a basic text editor, # especially if it's insanely intrusive. gsettings set org.gnome.TextEditor spellcheck false > /dev/null 2>&1 # Just because gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' > /dev/null 2>&1 fi replaceHomePageDesktopIcon $(pwgen -c -n 8 1) draw_progress_bar 97 addRootCertificateToApps draw_progress_bar 98 configureFirefox set -e fi draw_progress_bar 99 initCronJobs removeSudoTimeoutInstall rm -f ~/dead.letter rm -f $HSHQ_BASE_DIR/cip.txt if [ "$isInstallOrRestore" = "install" ]; then performInstallVariantsPost addPrimaryUser "$LDAP_ADMIN_USER_USERNAME" "$LDAP_ADMIN_USER_PASSWORD" "${HOMESERVER_ABBREV^^}" "Admin" true # > /dev/null 2>&1 if ! [ -z "$FIRST_USER_USERNAME" ]; then echo "Adding first user($FIRST_USER_USERNAME)..." addPrimaryUser "$FIRST_USER_USERNAME" "$FIRST_USER_PASSWORD" "$FIRST_USER_FIRSTNAME" "$FIRST_USER_LASTNAME" false > /dev/null 2>&1 fi IS_INSTALLED=true updateConfigVar IS_INSTALLED $IS_INSTALLED IS_INSTALLING=false updateConfigVar IS_INSTALLING $IS_INSTALLING encryptConfigFile draw_progress_bar 100 destroy_scroll_area releaseLock hshqopen "postInstallation" false sanitizeHSHQLog setSystemState $SS_RUNNING logHSHQEvent info "Installation Complete!" echo -e "\n\n\n\n################################################################\n" echo " HomeServer Installation Complete!" echo " The system will automatically reboot in 60 seconds..." echo -e "\n################################################################\n\n" sleep 60 logHSHQEvent info "postInstallation - Rebooting" sudo cp $HSHQ_LOG_FILE $HSHQ_BASE_DIR/hshqInstall.log sudo chown $USERNAME:$USERNAME $HSHQ_BASE_DIR/hshqInstall.log chmod 0400 $HSHQ_BASE_DIR/hshqInstall.log sudo truncate -s 0 $HSHQ_LOG_FILE sudo reboot fi } function sanitizeHSHQLog() { sudo sed -i 's/\x1b//g' $HSHQ_LOG_FILE sudo sed -i 's/\^//g' $HSHQ_LOG_FILE sudo sed -i 's/\x1b//g' $HSHQ_LOG_FILE sudo sed -i "s|$USERNAME|hshquser|g" $HSHQ_LOG_FILE sudo sed -i "s|$LDAP_ADMIN_USER_USERNAME|hshqadmin|g" $HSHQ_LOG_FILE sudo sed -i "s|$HOMESERVER_DOMAIN|hshqexample.com|g" $HSHQ_LOG_FILE } function showSimpleBackupMenu() { set +e utilmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Configure Backup" \ "2" "Mount Existing Backup Disk" \ "3" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 0) return 1 ;; 1) showConfigureSimpleBackupMenu return 1 ;; 2) showMountBackupDriveMenu return 1 ;; 3) return 0 ;; esac } function showConfigureSimpleBackupMenu() { set +e showYesNoMessageBox "Continue?" "This is a very basic utility to set up your local encrypted backup. It will search for any unmounted disks and prompt you with a list. After making your selection and confirming, it will wipe all data and partitions on that disk, create a single partition, automount the filesystem on boot, then configure your backup in Duplicati for this disk. There are infinite ways you can configure your backup, and infinite tools to accomplish it. This utility is merely one simple way. If you decide to manually set up your own backup process, the directory you need to focus on is $HSHQ_DATA_DIR. The full system can be restored from the contents of this directory. Do you wish to continue?" if [ $? -ne 0 ]; then return fi sudo cat /etc/fstab | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1 if [ $? -eq 0 ]; then showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR in /etc/fstab. Please unmount this filesystem and remove it from /etc/fstab, returning..." return fi sudo findmnt | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1 if [ $? -eq 0 ]; then showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR. Please unmount this filesystem first, returning..." return fi chkfstab=$(sudo findmnt --verify 2> /dev/null) if [ $? -ne 0 ]; then showMessageBox "ERROR" "There is an error with /etc/fstab: ${chkfstab}\nPlease fix this issue and retry." return fi if ! sudo test -f $HSHQ_STACKS_DIR/duplicati/config/Duplicati-server.sqlite; then showMessageBox "ERROR" "Could not find the Duplicati database, returning..." return fi sudo sqlite3 $HSHQ_STACKS_DIR/duplicati/config/Duplicati-server.sqlite "select TargetURL from Backup;" | grep /backups/ > /dev/null 2>&1 if [ $? -eq 0 ]; then showMessageBox "ERROR" "There is already a backup in Duplicati that uses the /backups directory, returning..." return fi dtok="$(getDuplicatiToken)" if [ -z "$dtok" ]; then showMessageBox "ERROR" "The Duplicati auth token is empty, returning..." return fi dbackmenu=$(cat << EOF $(getLogo) EOF ) OLDIFS=$IFS IFS=$(echo -en "\n\b") diskarr=($(sudo hwinfo --disk --short | tail +2)) curListNum=1 scsbm_menu_items=( --title "Select Disk" --radiolist "$dbackmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT ) for curDisk in "${diskarr[@]}" do curDiskID=$(echo "$curDisk" | xargs | cut -d" " -f1) curDiskName=$(echo "$curDisk" | xargs | cut -d" " -f2-) findmnt | grep $curDiskID > /dev/null 2>&1 if [ $? -eq 0 ]; then continue fi curDiskSize=$(lsblk -o SIZE --noheadings --raw -d $curDiskID) curDiskParts=$(sudo partx -g $curDiskID 2> /dev/null | wc -l) scsbm_menu_items+=( "$curDiskID $curDiskSize ($curDiskParts Partitions) $curDiskName" ) scsbm_menu_items+=( "|" ) scsbm_menu_items+=( "off" ) ((curListNum++)) done IFS=$OLDIFS if [ $curListNum -le 1 ]; then showMessageBox "ERROR" "There are no available disks for this operation, returning..." return fi while true; do selDiskItem=$(whiptail "${scsbm_menu_items[@]}" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then return fi if [ -z "$selDiskItem" ]; then showMessageBox "ERROR" "You did not make a selection, press spacebar on your desired option. Please try again." else break fi done selDisk=$(echo "$selDiskItem" | cut -d" " -f1) if [ -z "$selDisk" ]; then showMessageBox "ERROR" "No disk was selected, returning..." return fi confirmFormat=$(promptUserInputMenu "" "Confirm Format" "WARNING - This operation will entirely ERASE the contents of the selected drive. To confirm, enter the word 'format' below:") if [ $? -ne 0 ]; then return fi if [ $? -ne 0 ] || [ -z "$confirmFormat" ] || ! [ "$confirmFormat" = "format" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi while true; do backupHour=$(promptUserInputMenu "" "Daily Backup Time" "Enter the hour of the day(0-23) that you wish to perform the daily backup. This can be modified later in Duplicati:") if [ $? -ne 0 ]; then return fi if [[ $backupHour =~ ^[0-9]+$ ]] && [ $backupHour -ge 0 ] && [ $backupHour -le 23 ]; then break else showMessageBox "Invalid Number" "The value is not a valid number." fi done tmp_pw1="" tmp_pw2="" while [ -z "$tmp_pw1" ] || ! [ "$tmp_pw1" = "$tmp_pw2" ] do tmp_pw1=$(promptPasswordMenu "Create Password" "Create a password to encrypt/decrypt your backup files. ENSURE you remember this or you will be IRREVERSIBLY locked out of your backup files (unless you have a quantum super-computer) and you will NOT be able to recover your data: ") if [ $? -ne 0 ]; then return fi if [ -z "$tmp_pw1" ]; then showMessageBox "Password Empty" "The password cannot be empty, please try again." continue fi if [ $(checkValidPassword "$tmp_pw1" 16) = "false" ]; then showMessageBox "Weak Password" "The password is invalid or is too weak. It must contain at least 16 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.," tmp_pw1="" tmp_pw2="" continue fi tmp_pw2=$(promptPasswordMenu "Confirm Password" "Enter the password again to confirm: ") if [ $? -ne 0 ]; then return fi if ! [ "$tmp_pw1" = "$tmp_pw2" ]; then showMessageBox "Password Mismatch" "The passwords do not match, please try again." fi done backupPW="$tmp_pw1" tmp_pw1="" tmp_pw2="" confirmFinal=$(promptUserInputMenu "" "Confirm" "The backup configuration is ready. To perform all actions, including erasing and formatting the selected disk, enter the word 'confirm' below:") if [ $? -ne 0 ] || [ -z "$confirmFinal" ] || ! [ "$confirmFinal" = "confirm" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi docker container stop duplicati > /dev/null 2>&1 docker container stop syncthing > /dev/null 2>&1 sudo sfdisk --delete $selDisk > /dev/null retV=$? if [ $retV -ne 0 ] && [ $retV -ne 1 ]; then docker container start duplicati > /dev/null 2>&1 docker container start syncthing > /dev/null 2>&1 showMessageBox "ERROR" "Encountered an error attempting to delete the partitions from the disk, exiting..." return fi # A little sleep never hurts sleep 1 echo ';' | sudo sfdisk $selDisk > /dev/null if [ $? -ne 0 ]; then docker container start duplicati > /dev/null 2>&1 docker container start syncthing > /dev/null 2>&1 showMessageBox "ERROR" "Encountered an error attempting to create the new partition, exiting..." return fi sleep 1 newPart=$(sudo fdisk -l $selDisk | grep '^/dev' | cut -d' ' -f1) sleep 1 sudo mkfs -F -t ext4 $newPart > /dev/null if [ $? -ne 0 ]; then docker container start duplicati > /dev/null 2>&1 docker container start syncthing > /dev/null 2>&1 showMessageBox "ERROR" "Encountered an error attempting to create the new filesystem, exiting..." return fi sleep 1 sudo mount $newPart $HSHQ_BACKUP_DIR > /dev/null if [ $? -ne 0 ]; then docker container start duplicati > /dev/null 2>&1 docker container start syncthing > /dev/null 2>&1 showMessageBox "ERROR" "Encountered an error attempting to mount the filesystem, exiting..." return fi sleep 1 sudo mkdir $HSHQ_BACKUP_DIR/$HOMESERVER_DOMAIN docker container start duplicati > /dev/null 2>&1 docker container start syncthing > /dev/null 2>&1 newPartID=$(sudo blkid -s UUID -o value $newPart) sudo cp /etc/fstab /etc/fstab.old echo "# Backup Drive" | sudo tee -a /etc/fstab > /dev/null 2>&1 if [ -z "$newPartID" ]; then echo "$newPart $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1 else echo "UUID=$newPartID $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1 fi sudo systemctl daemon-reload > /dev/null 2>&1 sudo findmnt --verify | grep "Success, no errors or warnings detected" > /dev/null 2>&1 if [ $? -ne 0 ]; then showMessageBox "ERROR" "There was a problem adding the requisite entry to /etc/fstab. Your backup disk will not be auto-mounted after a reboot." sudo mv /etc/fstab.old /etc/fstab else sudo rm -f /etc/fstab.old fi mydate=$(date --date="$(date +%Y-%m-%d) $backupHour:00") dtnow=$(date) if [ $(($(date -d "$dtnow" +%s) - $(date -d "$mydate" +%s))) -gt 0 ]; then mydate=$(date -d "$mydate + 1 day") fi utcdate=$(TZ="UTC" date --date="$mydate") cat < $HOME/hshq-backup.json { "CreatedByVersion": "2.0.7.1", "Schedule": { "Time": "$(TZ="UTC" date --date="$utcdate" +%FT%TZ)", "Repeat": "1D", "LastRun": "0001-01-01T05:51:00Z", "Rule": "AllowedWeekDays=Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday", "AllowedDays": [ "mon", "tue", "wed", "thu", "fri", "sat", "sun" ] }, "Backup": { "Name": "HSHQ Backup", "Description": "", "Tags": [], "TargetURL": "file:///backups/$HOMESERVER_DOMAIN", "Sources": [ "/source/" ], "Settings": [ { "Filter": "", "Name": "encryption-module", "Value": "aes", "Argument": null }, { "Filter": "", "Name": "compression-module", "Value": "zip", "Argument": null }, { "Filter": "", "Name": "dblock-size", "Value": "50mb", "Argument": null }, { "Filter": "", "Name": "passphrase", "Value": "$backupPW", "Argument": null }, { "Filter": "", "Name": "retention-policy", "Value": "1W:1D,4W:1W,12M:1M", "Argument": null }, { "Filter": "", "Name": "ignore-advisory-locking", "Value": "true", "Argument": null } ], "Filters": [], "Metadata": {}, "IsTemporary": false }, "DisplayNames": { "/source/": "source" } } EOFBU sleep 3 echo "Getting Duplicati auth token..." dtok="$(getDuplicatiToken)" if [ -z "$dtok" ]; then showMessageBox "ERROR" "ERROR: The Duplicati auth token is empty, returning..." else echo "Got token, importing backup configuration..." fi docker exec -i -e DTOK="$dtok" duplicati sh -c 'curl -sS --fail-with-body --max-time 300 -X POST -H "Authorization: Bearer $DTOK" -H "Content-Type: application/json" --data-binary @- http://localhost:8200/api/v1/backups' < "$HOME/hshq-backup.json" #http --check-status --ignore-stdin POST https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/api/v1/backups "Authorization: Bearer $dtok" @$HOME/hshq-backup.json > /dev/null 2>&1 #docker exec duplicati bash -c "mono /app/duplicati/Duplicati.CommandLine.ConfigurationImporter.exe /config/hshq-backup.json --import-metadata=false --server-datafolder=/config" uplRetval=$? sudo rm -f $HOME/hshq-backup.json if [ $uplRetval -ne 0 ]; then showMessageBox "ERROR" "ERROR: The backup job was not uploaded to Duplicati. Please report this error on Github or the Forum." else showMessageBox "SUCCESS" "Your backup is configured. It will run automatically starting at $mydate, or you can trigger it manually. You will recieve a daily email in your admin email account ($EMAIL_ADMIN_EMAIL_ADDRESS) after each backup. You can view/edit the configuration in https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN ." fi } function getDuplicatiToken() { maxDupTries=10 curDupTries=0 isSuccess=false dupToken="" while [ $curDupTries -lt $maxDupTries ] do dupToken=$(docker exec -e DUPLICATI_ADMIN_PASSWORD="$DUPLICATI_ADMIN_PASSWORD" duplicati sh -c 'curl -sS --fail-with-body --max-time 300 -X POST -H "Content-Type: application/json" -d "$(jq -nc --arg p "$DUPLICATI_ADMIN_PASSWORD" "{password: \$p}")" http://localhost:8200/api/v1/auth/login' | jq -r .AccessToken) #dupToken=$(http --check-status --ignore-stdin --timeout=300 https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/api/v1/auth/login password=$DUPLICATI_ADMIN_PASSWORD | jq -r .AccessToken) if [ $? -eq 0 ] && ! [ -z "$dupToken" ]; then isSuccess=true break fi ((curDupTries++)) sleep 3 done if [ "$isSuccess" = "true" ]; then echo "$dupToken" fi } function showMountBackupDriveMenu() { findmnt | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1 if [ $? -eq 0 ]; then showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR. Please unmount this filesystem first, returning..." return fi sudo cat /etc/fstab | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1 if [ $? -eq 0 ]; then showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR in /etc/fstab. Please unmount this filesystem and remove it from /etc/fstab, returning..." return fi mkdir -p $HSHQ_BACKUP_DIR if ! [ -z "$(ls -A $HSHQ_BACKUP_DIR)" ]; then showMessageBox "ERROR" "The backup directory ($HSHQ_BACKUP_DIR) is not empty, returning..." return fi dbackmenu=$(cat << EOF $(getLogo) EOF ) OLDIFS=$IFS IFS=$(echo -en "\n\b") diskarr=($(sudo hwinfo --disk --short | tail +2)) curListNum=1 scsbm_menu_items=( --title "Select Disk/Partition" --radiolist "$dbackmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT ) for curDisk in "${diskarr[@]}" do curDiskID=$(echo "$curDisk" | xargs | cut -d" " -f1) curDiskName=$(echo "$curDisk" | xargs | cut -d" " -f2-) findmnt | grep $curDiskID > /dev/null 2>&1 if [ $? -eq 0 ]; then continue fi curDiskPartitions=($(lsblk --noheadings --raw -o NAME,SIZE,TYPE $curDiskID)) for curDiskPart in "${curDiskPartitions[@]}" do curDiskPartName=$(echo $curDiskPart | cut -d" " -f1) curDiskPartSize=$(echo $curDiskPart | cut -d" " -f2) curDiskPartType=$(echo $curDiskPart | cut -d" " -f3) if [ "$curDiskPartType" = "part" ]; then scsbm_menu_items+=( "$curDiskPartName $curDiskPartSize ($curDiskName)" ) scsbm_menu_items+=( "|" ) scsbm_menu_items+=( "off" ) ((curListNum++)) fi done done IFS=$OLDIFS if [ $curListNum -le 1 ]; then showMessageBox "ERROR" "There are no available disks for this operation, returning..." return fi while true; do selDiskItem=$(whiptail "${scsbm_menu_items[@]}" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then return fi if [ -z "$selDiskItem" ]; then showMessageBox "ERROR" "You did not make a selection, please try again." else break fi done selDisk=$(echo "$selDiskItem" | cut -d" " -f1) if [ -z "$selDisk" ]; then showMessageBox "ERROR" "No partition was selected, returning..." return fi sudo mount /dev/$selDisk $HSHQ_BACKUP_DIR > /dev/null if [ $? -ne 0 ]; then showMessageBox "ERROR" "There was an error mounting this partition, returning..." return fi newPart=/dev/$selDisk newPartID=$(sudo blkid -s UUID -o value $newPart) sudo cp /etc/fstab /etc/fstab.old echo "# Backup Drive" | sudo tee -a /etc/fstab > /dev/null 2>&1 if [ -z "$newPartID" ]; then echo "$newPart $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1 else echo "UUID=$newPartID $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1 fi sudo systemctl daemon-reload > /dev/null 2>&1 sudo findmnt --verify | grep "Success, no errors or warnings detected" > /dev/null 2>&1 if [ $? -ne 0 ]; then showMessageBox "ERROR" "There was a problem adding the requisite entry to /etc/fstab. Your backup disk will not be auto-mounted after a reboot." sudo mv /etc/fstab.old /etc/fstab else sudo rm -f /etc/fstab.old fi } function initScreen() { # This function may appear to be overly complex and # contrived, but the main purpose is to inject the # super-user (sudo) password into the running screen # with as minimal of a security footprint as possible. # There's also some race conditions to overcome, hence # the added complexity. This method avoids anything # (long-running) from showing up in the process list # (ps auxwwe). screenName="$1" screenCmd="$2" set +e dirtest1=/tmp/sctest1 isScreenSuccess=false curScreenAttempt=0 maxScreenAttempt=5 screen -XS $screenName quit > /dev/null 2>&1 # The screen utility has a problem with stuffing # special characters in strings, specifically # in this case when trying to inject the sudo # password. Thus, the easiest way to handle this # is to create a temporary "safe" key, output # the encrypted password to file using the key # and transmit the key to the screen session. sudoPWKey=$(pwgen -c -n 32 1) echo -n "$USER_SUDO_PW" | openssl enc -e -aes256 -pbkdf2 -pass pass:$sudoPWKey > $ENC_PW_FILE while [ $curScreenAttempt -lt $maxScreenAttempt ] do ((curScreenAttempt++)) rm -fr $dirtest1 screen -L -Logfile $HSHQ_LOG_FILE -dmS $screenName screen -S $screenName -X stuff "stty -echo;mkdir $dirtest1\n" curCheckCount=0 while ! [ -d $dirtest1 ] && [ $curCheckCount -lt 5 ] do ((curCheckCount++)) sleep 1 done if ! [ -d $dirtest1 ]; then screen -XS $screenName quit > /dev/null 2>&1 continue fi screen -S $screenName -X stuff "$screenCmd\n" curCheckCount=0 while ! [ -d $TMP_PW_CHECKDIR ] && [ $curCheckCount -lt 15 ] do ((curCheckCount++)) sleep 1 done screen -S $screenName -X stuff "$sudoPWKey\n" curCheckCount=0 while [ -d $TMP_PW_CHECKDIR ] && [ $curCheckCount -lt 15 ] do ((curCheckCount++)) sleep 1 done rm -fr $dirtest1 if [ -d $TMP_PW_CHECKDIR ]; then rm -fr $TMP_PW_CHECKDIR echo "The sudo password was not correctly transmitted to the screen session, exiting..." exit 5 fi rm -fr $TMP_PW_CHECKDIR isScreenSuccess=true break done if ! [ "$isScreenSuccess" = "true" ]; then return 1 fi } function performInstallVariantsPre() { return } function performInstallVariantsPost() { return } # Stacks Functions function showStacksMenu() { set +e if ! [ "$IS_INSTALLED" = "true" ]; then showMessageBox "System Not Installed" "You must perform a system installation first before using this utility." exit fi svcsmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$svcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Install Service(s) From List" \ "2" "Install All Available Services" \ "3" "Update Service(s) From List" \ "4" "Update All Available Services" \ "5" "Remove Service(s) From List" \ "6" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ] || [ $menures -eq 6 ]; then return fi set +e getUpdateAssets if [ $? -ne 0 ]; then showMessageBox "ERROR" "ERROR: Could not update image assets, returning..." return fi case $menures in 0) return 0 ;; 1) installStacksFromList set +e return 1 ;; 2) installAllAvailableStacks true set +e return 1 ;; 3) performStackUpdatesFromList set +e return 1 ;; 4) performAllAvailableStackUpdates true set +e return 1 ;; 5) deleteStacksFromList set +e return 1 ;; 6) return 0 ;; esac } function installStacksFromList() { set +e setSystemState $SS_INSTALLING sortedStackList=$(sortCSVList $HSHQ_OPTIONAL_STACKS) stackListArr=($(echo $sortedStackList | tr "," "\n")) menu_items="" for curStack in "${stackListArr[@]}" do if ! [ -d $HSHQ_STACKS_DIR/$curStack ]; then menu_items=${menu_items}"$curStack | OFF " fi done if [ -z "$menu_items" ]; then showMessageBox "No Available Services" "There are no available services to install." return fi selsvcsmenu=$(cat << EOF $(getLogo) Select the services that you wish to install: EOF ) sel_svcs=($(whiptail --title "Select Services" --checklist "$selsvcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) if [ -z "$sel_svcs" ]; then showMessageBox "Empty Selection" "You have not selected anything, returning to main menu..." return 0 fi #showYesNoMessageBox "Perform Integration?" "Do you want to automatically integrate these services, i.e. within Caddy, Heimdall, UptimeKuma, etc.?" is_integrate=true #if [ $? -eq 0 ]; then # is_integrate=true #else # is_integrate=false #fi showYesNoMessageBox "Confirm Installation" "This selected service(s) will be installed, Continue?" if [ $? -ne 0 ]; then return fi setSudoTimeoutInstall for cur_svc in "${sel_svcs[@]}" do installStackByName ${cur_svc//\"} $is_integrate done removeSudoTimeoutInstall outputStackListsScriptServer setSystemState $SS_RUNNING } function installListOfServices() { echo "Installing list of services, Start time: $(date '+%Y-%m-%d %H:%M:%S')" setSystemState $SS_INSTALLING stackListArr=($(echo "$1" | tr "," "\n")) setSudoTimeoutInstall for curStack in "${stackListArr[@]}" do installStackByName $curStack true done sudo systemctl daemon-reload sudo systemctl restart ntpsec docker container restart authelia > /dev/null 2>&1 removeSudoTimeoutInstall outputStackListsScriptServer setSystemState $SS_RUNNING echo "Installing list of services, End time: $(date '+%Y-%m-%d %H:%M:%S')" } function installAllAvailableStacks() { is_msgbox_prompt="$1" set +e echo "Installing all services, Start time: $(date '+%Y-%m-%d %H:%M:%S')" setSystemState $SS_INSTALLING echo "Pulling common images..." pullCommonImages stackListArr=($(echo $HSHQ_OPTIONAL_STACKS | tr "," "\n")) sel_svcs=() is_list_empty=true for curStack in "${stackListArr[@]}" do if ! [ -d $HSHQ_STACKS_DIR/$curStack ] && ! [ "$(isServiceDisabled $curStack)" = "true" ]; then sel_svcs+=($curStack) is_list_empty=false fi done if [ "$is_list_empty" = "true" ]; then if [ "$is_msgbox_prompt" = "true" ]; then showMessageBox "No Available Services" "There are no available services to install." fi return fi is_integrate=true if [ "$is_msgbox_prompt" = "true" ]; then #showYesNoMessageBox "Perform Integration?" "Do you want to automatically integrate these services, i.e. within Caddy, Heimdall, UptimeKuma, etc.?" #if [ $? -eq 0 ]; then # is_integrate=true #else # is_integrate=false #fi showYesNoMessageBox "Confirm Installation" "All available service(s) will be installed, Continue?" if [ $? -ne 0 ]; then return fi fi set -e setSudoTimeoutInstall for cur_svc in "${sel_svcs[@]}" do installStackByName $cur_svc $is_integrate done sudo systemctl restart ntpsec docker container restart authelia > /dev/null 2>&1 removeSudoTimeoutInstall outputStackListsScriptServer setSystemState $SS_RUNNING echo "Installing all services, End time: $(date '+%Y-%m-%d %H:%M:%S')" } function getStacksToUpdate() { set +e stacks_to_update_list="" # Special case for portainer compose check_stack_firstline=$(sudo sed -n 1p $HSHQ_STACKS_DIR/portainer/docker-compose.yml) if [ "$(checkStackHSHQManaged $check_stack_firstline)" = "true" ]; then check_stack_name=$(getStackNameFromComposeLine "$check_stack_firstline") if [ $? -eq 0 ]; then check_stack_version=$(getVersionFromComposeLine "$check_stack_firstline") if [ $? -eq 0 ]; then hshq_stack_ver=$(getScriptStackVersionNumber $(getScriptStackVersion $check_stack_name)) if [ $? -eq 0 ]; then if [ $check_stack_version -lt $hshq_stack_ver ]; then stacks_to_update_list=${stacks_to_update_list}"${check_stack_name}," fi fi fi fi fi dirList=($(sudo ls $HSHQ_STACKS_DIR/portainer/compose/)) for curDir in "${dirList[@]}" do check_stack_firstline=$(sudo sed -n 1p $HSHQ_STACKS_DIR/portainer/compose/$curDir/docker-compose.yml) if ! [ "$(checkStackHSHQManaged $check_stack_firstline)" = "true" ]; then continue fi check_stack_name=$(getStackNameFromComposeLine "$check_stack_firstline") if [ $? -ne 0 ]; then continue; fi check_stack_version=$(getVersionFromComposeLine "$check_stack_firstline") if [ $? -ne 0 ] || [ -z "$check_stack_version" ]; then continue; fi hshq_stack_ver=$(getScriptStackVersionNumber $(getScriptStackVersion $check_stack_name)) if [ $? -ne 0 ] || [ -z "$hshq_stack_ver" ]; then continue; fi if [ $check_stack_version -lt $hshq_stack_ver ]; then stacks_to_update_list=${stacks_to_update_list}"${check_stack_name}," fi if [ $? -ne 0 ]; then continue; fi done if ! [ -z "$stacks_to_update_list" ]; then stacks_to_update_list=${stacks_to_update_list%?} fi sortCSVList "$stacks_to_update_list" } function performStackUpdatesFromList() { set +e this_stack_list=$(getStacksToUpdate) stackListArr=($(echo $this_stack_list | tr "," "\n")) menu_items="" for curStack in "${stackListArr[@]}" do if [ -z "$curStack" ]; then continue fi menu_items=${menu_items}"$curStack | OFF " done if [ -z "$menu_items" ]; then showMessageBox "No Available Updates" "All services are updated." return fi selsvcsmenu=$(cat << EOF $(getLogo) Select the services that you wish to update: EOF ) sel_svcs=($(whiptail --title "Select Services" --checklist "$selsvcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) if [ -z "$sel_svcs" ]; then showMessageBox "Empty Selection" "You have not selected anything, returning to main menu..." return 0 fi showYesNoMessageBox "Confirm Updates" "This selected service(s) will be updated, Continue?" if [ $? -ne 0 ]; then return fi update_list="" for cur_svc in "${sel_svcs[@]}" do update_list="${update_list}${cur_svc//\"}," done updateListOfStacks "$update_list" } function performAllAvailableStackUpdates() { is_msgbox_prompt="$1" stacksToUpdate="$(getStacksToUpdate)" if [ -z "$stacksToUpdate" ]; then if [ "$is_msgbox_prompt" = "true" ]; then showMessageBox "No Available Updates" "All services are updated." else echo "All services are updated." fi return fi if [ "$is_msgbox_prompt" = "true" ]; then showYesNoMessageBox "Confirm Updates" "All available stacks will be updated, Continue?" if [ $? -ne 0 ]; then return fi fi updateListOfStacks "$stacksToUpdate" } function updateListOfStacks() { echo "Updating list of services, Start time: $(date '+%Y-%m-%d %H:%M:%S')" stackListArr=($(echo "$1" | tr "," "\n")) stacks_need_update_list="$(getStacksToUpdate)" setSudoTimeoutInstall report_start_time=$(date) full_update_report="" full_update_report="${full_update_report} Services that updated successfully: \n" full_update_report="${full_update_report}--------------------------------------------------" upgrade_error_report="\n\n" upgrade_error_report="${upgrade_error_report} Services that encountered errors: \n" upgrade_error_report="${upgrade_error_report}--------------------------------------------------" set +e for cur_svc in "${stackListArr[@]}" do if [ -z "$cur_svc" ]; then continue; fi unset is_upgrade_error is_upgrade_error="" echo -e "\n\nUpdating ${cur_svc}..." if ! [ "$(isItemInCSVList $cur_svc $stacks_need_update_list)" = "true" ]; then continue fi performUpdateStackByName "$cur_svc" if [ "$is_upgrade_error" = "true" ]; then upgrade_error_report="${upgrade_error_report}\n$perform_update_report\n" else full_update_report="${full_update_report}\n$perform_update_report" fi done full_update_report="${full_update_report}\n${upgrade_error_report}" removeSudoTimeoutInstall report_end_time=$(date) full_update_report_header="" full_update_report_header="${full_update_report_header}==================================================\n" full_update_report_header="${full_update_report_header} Services Upgrade Report \n" full_update_report_header="${full_update_report_header}==================================================\n" full_update_report_header="${full_update_report_header}Start Time: $report_start_time\n" full_update_report_header="${full_update_report_header} End Time: $report_end_time\n\n" sendEmail -s "Services Upgrade Report" -b "${full_update_report_header}$full_update_report" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -t $EMAIL_ADMIN_EMAIL_ADDRESS echo "Sending update results to email manager ($EMAIL_ADMIN_EMAIL_ADDRESS). Refer to this for further details." outputStackListsScriptServer echo "Updating list of services, End time: $(date '+%Y-%m-%d %H:%M:%S')" } function deleteStacksFromList() { sortedStackList=$(sortCSVList $HSHQ_OPTIONAL_STACKS) stackListArr=($(echo $sortedStackList | tr "," "\n")) menu_items="" for curStack in "${stackListArr[@]}" do if [ -d $HSHQ_STACKS_DIR/$curStack ]; then menu_items=${menu_items}"$curStack | OFF " fi done if [ -z "$menu_items" ]; then showMessageBox "No Available Services" "There are no available services to remove." return fi selsvcsmenu=$(cat << EOF $(getLogo) Select the services that you wish to remove: EOF ) sel_svcs=($(whiptail --title "Select Services" --checklist "$selsvcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) if [ -z "$sel_svcs" ]; then showMessageBox "Empty Selection" "You have not selected anything, returning to main menu..." return 0 fi showYesNoMessageBox "Confirm Installation" "This selected service(s) will be removed, Continue?" if [ $? -ne 0 ]; then return fi del_stack_list="" for svc in "${sel_svcs[@]}" do del_stack_list="${del_stack_list}${svc//\"}," cur_svc=${svc//\"} done ENABLE_STACK_DELETE=true deleteListOfStacks false "$del_stack_list" ENABLE_STACK_DELETE=false } function deleteListOfStacks() { is_force_delete_stack="$1" stackListArr=($(echo "$2" | tr "," "\n")) setSudoTimeoutInstall docker container stop uptimekuma >/dev/null docker container stop heimdall >/dev/null for cur_stack in "${stackListArr[@]}" do checkDeleteStackAndDirectory $cur_stack $cur_stack true "$is_force_delete_stack" for curSVC in "${SVCS_ARR[@]}" do cur_stack_name=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f1) if [ "${cur_stack_name//\"}" = "$cur_stack" ]; then user_type=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f4) subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6) deleteSvcAll "${user_type//\"}" "https://${subdom//\"}.$HOMESERVER_DOMAIN" false fi done done docker container start uptimekuma >/dev/null docker container start heimdall >/dev/null removeSudoTimeoutInstall restartAllCaddyContainers outputStackListsScriptServer } function clearRedisTempDataByStackName() { stackName="$1" if ! [ -d $HSHQ_NONBACKUP_DIR/$stackName/redis ]; then echo "ERROR: Could not find corresponding redis directory" return fi stackStatus=$(getStackStatusByName "$stackName") if ! [ "$stackStatus" = "1" ]; then echo "WARNING: This stack is not currently running" sudo rm -fr $HSHQ_NONBACKUP_DIR/$stackName/redis/* return fi echo "Stopping $stackName..." startStopStack "$stackName" stop echo "Clearing redis data..." sudo rm -fr $HSHQ_NONBACKUP_DIR/$stackName/redis/* echo "Starting $stackName..." startStopStack "$stackName" start echo "Complete!" } # VPN Setup function setupVPNConnection() { if [[ "$(isProgramInstalled wg)" = "false" ]]; then echo "Installing WireGuard utils, please wait..." performAptInstall wireguard-tools > /dev/null 2>&1 fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "none" ]; then vpnmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option for your PRIMARY VPN connection" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Host a VPN" \ "2" "Setup Later" 3>&1 1>&2 2>&3) case $menures in 1) PRIMARY_VPN_SETUP_TYPE=host setupHostedVPN if [ $? -ne 0 ]; then return 1 fi ;; 2) PRIMARY_VPN_SETUP_TYPE=none updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE # Determine if this is a private IP, and if so, let the user know about networking. showMsg="Since you will set up a RelayServer later, you will need to manually change the DNS server on any client devices to the IP address of this HomeServer ($HOMESERVER_HOST_PRIMARY_INTERFACE_IP), in order to access your services. " if [ "$(checkIsIPPrivate $HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" = "true" ]; then showMsg=$showMsg"The client devices must be on the same local network (connected to the same router) as well." fi showMessageBox "DNS Server" "$showMsg" ;; esac fi } function setupJoinPrimaryVPN() { vpnmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Generate Join Application" \ "2" "Submit Join Configuration" \ "3" "Exit" 3>&1 1>&2 2>&3) case $menures in 1) applyHomeServerPrimaryVPNConfig releaseLock hshqopen "setupJoinPrimaryVPN" false return 1 ;; 2) createNetworkJoin false if [ $? -ne 0 ]; then return 1 fi PRIMARY_VPN_SETUP_TYPE=join updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE ;; 3) return 1 ;; esac } function initRelayServerCredentials() { RELAYSERVER_WG_SV_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev))) updateConfigVar RELAYSERVER_WG_SV_IP $RELAYSERVER_WG_SV_IP if [ -z "$RELAYSERVER_WG_SV_PRIVATEKEY" ]; then RELAYSERVER_WG_SV_PRIVATEKEY=$(wg genkey) updateConfigVar RELAYSERVER_WG_SV_PRIVATEKEY $RELAYSERVER_WG_SV_PRIVATEKEY RELAYSERVER_WG_SV_PUBLICKEY=$(echo $RELAYSERVER_WG_SV_PRIVATEKEY | wg pubkey) updateConfigVar RELAYSERVER_WG_SV_PUBLICKEY $RELAYSERVER_WG_SV_PUBLICKEY fi RELAYSERVER_WG_SV_PRESHAREDKEY=$(wg genpsk) updateConfigVar RELAYSERVER_WG_SV_PRESHAREDKEY $RELAYSERVER_WG_SV_PRESHAREDKEY RELAYSERVER_WG_SV_CLIENTDNS_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev)-1)) updateConfigVar RELAYSERVER_WG_SV_CLIENTDNS_IP $RELAYSERVER_WG_SV_CLIENTDNS_IP RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY=$(wg genkey) updateConfigVar RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY $RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY=$(wg genpsk) updateConfigVar RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY $RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY RELAYSERVER_WG_SV_CLIENTDNS_PUBLICKEY=$(echo $RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY | wg pubkey) RELAYSERVER_WG_HS_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev)-2)) updateConfigVar RELAYSERVER_WG_HS_IP $RELAYSERVER_WG_HS_IP RELAYSERVER_WG_HS_PRIVATEKEY=$(wg genkey) updateConfigVar RELAYSERVER_WG_HS_PRIVATEKEY $RELAYSERVER_WG_HS_PRIVATEKEY RELAYSERVER_WG_HS_PRESHAREDKEY=$(wg genpsk) updateConfigVar RELAYSERVER_WG_HS_PRESHAREDKEY $RELAYSERVER_WG_HS_PRESHAREDKEY RELAYSERVER_WG_HS_PUBLICKEY=$(echo $RELAYSERVER_WG_HS_PRIVATEKEY | wg pubkey) RELAYSERVER_WG_HS_CLIENTDNS_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev)-3)) updateConfigVar RELAYSERVER_WG_HS_CLIENTDNS_IP $RELAYSERVER_WG_HS_CLIENTDNS_IP RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY=$(wg genkey) updateConfigVar RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY $RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY=$(wg genpsk) updateConfigVar RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY $RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY RELAYSERVER_WG_HS_CLIENTDNS_PUBLICKEY=$(echo $RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY | wg pubkey) RELAYSERVER_WG_INTERNET_HS_IP=$(getRandomWireGuardIP) RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY=$(wg genkey) updateConfigVar RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY $RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY=$(wg genpsk) updateConfigVar RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY $RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY RELAYSERVER_WG_INTERNET_HS_PUBLICKEY=$(echo $RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY | wg pubkey) RELAYSERVER_WG_USER_IP=$(getRandomWireGuardIP) RELAYSERVER_WG_USER_PRIVATEKEY=$(wg genkey) RELAYSERVER_WG_USER_PRESHAREDKEY=$(wg genpsk) RELAYSERVER_WG_USER_PUBLICKEY=$(echo $RELAYSERVER_WG_USER_PRIVATEKEY | wg pubkey) SMTP_RELAY_HOST="[$SUB_POSTFIX.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$MAILU_PORT_5" updateConfigVar SMTP_RELAY_HOST $SMTP_RELAY_HOST SMTP_RELAY_USERNAME=$HOMESERVER_DOMAIN updateConfigVar SMTP_RELAY_USERNAME $SMTP_RELAY_USERNAME SMTP_RELAY_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SMTP_RELAY_PASSWORD $SMTP_RELAY_PASSWORD } function outputHostedVPNConfigs() { sudo rm -f $HSHQ_WIREGUARD_DIR/vpn/${RELAYSERVER_WG_VPN_NETNAME}.conf sudo tee $HSHQ_WIREGUARD_DIR/vpn/${RELAYSERVER_WG_VPN_NETNAME}.conf >/dev/null </dev/null </dev/null </dev/null < /dev/null 2>&1 docker container stop uptimekuma > /dev/null 2>&1 insertEnableSvcHeimdall adguard "${FMLNAME_ADGUARD}" relayserver "https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "adguardhome.png" false "$(getHeimdallOrderFromSub $SUB_ADGUARD.$INT_DOMAIN_PREFIX relayserver)" insertEnableSvcUptimeKuma adguard "${FMLNAME_ADGUARD}-RelayServer" relayserver "https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false insertEnableSvcHeimdall clientdns "${FMLNAME_CLIENTDNS}" relayserver "https://$SUB_CLIENTDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "dnsmasq.png" false "$(getHeimdallOrderFromSub $SUB_CLIENTDNS.$INT_DOMAIN_PREFIX relayserver)" checkInsertServiceHeimdall caddy-dns "${FMLNAME_CADDYDNS}" relayserver "https://$SUB_CADDYDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "dnsmasq.png" false 0 "$(getHeimdallOrderFromSub $SUB_CADDYDNS.$INT_DOMAIN_PREFIX relayserver)" insertEnableSvcHeimdall portainer "${FMLNAME_PORTAINER}" relayserver "https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "portainer.png" false "$(getHeimdallOrderFromSub $SUB_PORTAINER.$INT_DOMAIN_PREFIX relayserver)" insertEnableSvcUptimeKuma portainer "${FMLNAME_PORTAINER}-RelayServer" relayserver "https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false insertEnableSvcHeimdall rspamd "${FMLNAME_RSPAMD}" relayserver "https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "rspamd.png" false "$(getHeimdallOrderFromSub $SUB_RSPAMD.$INT_DOMAIN_PREFIX relayserver)" insertEnableSvcUptimeKuma rspamd "${FMLNAME_RSPAMD}-RelayServer" relayserver "https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false insertEnableSvcHeimdall syncthing "${FMLNAME_SYNCTHING}" relayserver "https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "syncthing.png" false "$(getHeimdallOrderFromSub $SUB_SYNCTHING.$INT_DOMAIN_PREFIX relayserver)" insertEnableSvcUptimeKuma syncthing "${FMLNAME_SYNCTHING}-RelayServer" relayserver "https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false checkInsertServiceHeimdall wgportal "${FMLNAME_WGPORTAL}" relayserver "https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "wgportal.png" false 0 "$(getHeimdallOrderFromSub $SUB_WGPORTAL.$INT_DOMAIN_PREFIX relayserver)" insertEnableSvcUptimeKuma wgportal "${FMLNAME_WGPORTAL}-RelayServer" relayserver "https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false checkInsertServiceUptimeKuma filebrowser "${FMLNAME_FILEBROWSER}-RelayServer" relayserver "https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false 0 checkInsertServiceHeimdall filebrowser "${FMLNAME_FILEBROWSER}" relayserver "https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "filebrowser.png" false 0 "$(getHeimdallOrderFromSub $SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX relayserver)" docker container start heimdall > /dev/null 2>&1 docker container start uptimekuma > /dev/null 2>&1 } function prepSvcsHostedVPN() { set +e echo "Updating Portainer and Jitsi..." updateSvcsIPChanges set +e echo "Updating IP tables..." checkUpdateAllIPTables prepSvcsHostedVPN } function webTransferHostedVPN() { # These variables should already be set by Script-server # rs_cur_username # rs_external_ip # rs_cur_password # rs_new_password # rs_cur_ssh_port # rs_new_ssh_port set +e # Clear out any old hosts ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1 ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SERVER_IP]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1 ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$rs_external_ip]:$rs_cur_ssh_port" > /dev/null 2>&1 ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$rs_external_ip]:$rs_new_ssh_port" > /dev/null 2>&1 if ! [ "$rs_cur_username" = "root" ] && ! [ "$rs_cur_username" = "$RELAYSERVER_REMOTE_USERNAME" ]; then echo "ERROR: The username on the new RelayServer must match the existing username ($RELAYSERVER_REMOTE_USERNAME)" return 2 fi rs_new_username="$RELAYSERVER_REMOTE_USERNAME" if ! [ "$rs_cur_username" = "root" ]; then rs_new_password="$rs_cur_password" fi # Login, upload check script, log back in and run check script # 1. Login echo "Logging into RelayServer..." perfRemoteAction -m ssh -p $rs_cur_ssh_port -s "$rs_cur_password" -o "-T -o ConnectTimeout=5 -o 'StrictHostKeyChecking accept-new'" -u "$rs_cur_username" -h "$rs_external_ip" -c "echo hello >/dev/null" -f is_err=$? if [ $is_err -ne 0 ]; then echo "ERROR: There was a problem logging in to the RelayServer, returning..." return 2 fi # 2. Upload check script echo "Uploading validation script..." outputRelayServerValidationScript outputRelayServerInstallSetupScript perfRemoteAction -m scp -p $rs_cur_ssh_port -s "$rs_cur_password" -a $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME -u $rs_cur_username -h $rs_external_ip -c ":~" -f is_err=$? rm -f $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME if [ $is_err -ne 0 ]; then echo "ERROR: There was an problem uploading the validation script to the RelayServer, returning..." return 2 fi perfRemoteAction -m scp -p $rs_cur_ssh_port -s "$rs_cur_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $rs_cur_username -h $rs_external_ip -c ":~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME" -f is_err=$? rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME if [ $is_err -ne 0 ]; then echo "ERROR: There was an problem uploading the lib script to the RelayServer, returning..." return 2 fi # 3. Log back in and run check script echo "Performing pre-installation checks, please wait..." perfRemoteAction -m ssh -p $rs_cur_ssh_port -s "$rs_cur_password" -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_cur_username -h $rs_external_ip -c "bash ~/$RS_INSTALL_VALIDATION_SCRIPT_NAME -s" -f -i "$rs_new_password" is_err=$? if [ $is_err -ne 0 ]; then echo "ERROR: There was a problem with the RelayServer, see above." return 2 fi echo "Testing RelayServer login with pub/priv key..." perfRemoteAction -m ssh -p $rs_cur_ssh_port -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $rs_external_ip -c "echo \"Successfully logged in to RelayServer with key! \"" -f is_err=$? if [ $is_err -ne 0 ]; then echo "ERROR: Could not login to RelayServer with pub/priv key." return 2 fi setSudoTimeoutInstall setSystemState $SS_TRANSFERRING # Pause syncthing RelayServer curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X PATCH -d "{\"paused\": true}" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices/$RELAYSERVER_SYNCTHING_DEVICE_ID echo "Compressing RelayServer data..." sudo tar cvzf $HOME/rsbackup.tar.gz -C $HSHQ_RELAYSERVER_DIR/ ./backup >/dev/null echo "Checking if RelayServer is ready..." countRSPreRetries=0 maxRSPreRetries=20 while true; do perfRemoteAction -m ssh -p $rs_cur_ssh_port -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "sleep 1;if [ -f ~/$RELAYSERVER_NOT_READY_FILE ]; then exit 1; fi" -r 1 -b 1 if [ $? -eq 0 ]; then break fi if [ $countRSPreRetries -ge $maxRSPreRetries ]; then echo "Max retries reached. Something has likely gone wrong with the RelayServer pre-installation script, exiting..." return 2 fi ((countRSPreRetries++)) echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for installation, sleeping 30 seconds then will retry..." sleep 30 done removeHomeNetIP ${RELAYSERVER_SERVER_IP}/32 false RELAYSERVER_SERVER_IP="$rs_external_ip" RELAYSERVER_CURRENT_SSH_PORT="$rs_cur_ssh_port" RELAYSERVER_SSH_PORT="$rs_new_ssh_port" outputRelayServerInstallSetupScript outputRelayServerInstallTransferScript perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_new_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $rs_new_username -h $rs_external_ip -c ":~/$RS_INSTALL_SETUP_SCRIPT_NAME" -f is_err=$? rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME if [ $is_err -ne 0 ]; then echo "ERROR: There was an problem uploading the setup script to the RelayServer, returning..." return 2 fi perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_new_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_TRANSFER_SCRIPT_NAME -u $rs_new_username -h $rs_external_ip -c ":~/$RS_INSTALL_TRANSFER_SCRIPT_NAME" -f is_err=$? if [ $is_err -ne 0 ]; then echo "ERROR: There was an problem uploading the transfer script to the RelayServer, returning..." return 2 fi perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HOME/rsbackup.tar.gz -u $rs_new_username -h $rs_external_ip -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f sudo rm -f $HOME/rsbackup.tar.gz echo "Initializing transfer process..." perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_new_password" -o "-tt -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "bash ~/$RS_INSTALL_TRANSFER_SCRIPT_NAME -i" -f -i "$rs_new_password" -r 1 -b 1 is_err=$? if [ $is_err -ne 0 ]; then echo "There was a problem initializing the transfer, see above." return $is_err fi echo "Transfer Initialization complete! Rebooting.." perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -s "$rs_new_password" -o "-tt -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "sudo reboot" -f -i "$rs_new_password" echo echo echo "============================================================" echo " RelayServer is rebooting... " echo " You can now modify your DNS Records to " echo " point to the new RelayServer's IP Address " echo "============================================================" echo echo sleep 30 countRSPreRetries=0 maxRSPreRetries=20 while true; do perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "echo hello >/dev/null" -r 1 -b 1 if [ $? -eq 0 ]; then break fi if [ $countRSPreRetries -ge $maxRSPreRetries ]; then echo "Max retries reached. Something has likely gone wrong with the RelayServer transfer init script." read -r -p "Enter cancel to exit or any other key to retry: " raRetry if [ "$raRetry" = "cancel" ]; then return 2 fi fi ((countRSPreRetries++)) echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for transfer, sleeping 15 seconds then will retry..." sleep 15 done echo "RelayServer has rebooted, finishing transfer..." perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -s "$rs_new_password" -o "-tt -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "bash ~/$RS_INSTALL_TRANSFER_SCRIPT_NAME -s" -f -i "$rs_new_password" -r 1 -b 1 is_err=$? if [ $is_err -ne 0 ]; then echo "There was a problem performing the transfer, see above." return $is_err fi echo "The RelayServer is fully prepped for transfer. If you haven't already updated your DNS records, ensure to do that now. Both A records should point to $rs_external_ip. Please also review the above logs to ensure there were no errors. If everything appears normal, then enter continue. If there are any errors, then enter cancel to halt the process." while true; do read -r -p "Enter continue or cancel: " raRetry if [ "$raRetry" = "cancel" ]; then return 2 elif [ "$raRetry" = "continue" ]; then break else echo "Invalid input." fi done updateConfigVar RELAYSERVER_SERVER_IP $RELAYSERVER_SERVER_IP updateConfigVar RELAYSERVER_CURRENT_SSH_PORT $RELAYSERVER_CURRENT_SSH_PORT updateConfigVar RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT addHomeNetIP ${RELAYSERVER_SERVER_IP}/32 true addDomainAdguardHS "*.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "$RELAYSERVER_SERVER_IP" # Restart all vpn connections echo "Restarting all wg connections..." db_id=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='primary';") ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") sudo systemctl restart wg-quick@$ifaceName db_id=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_internet' and NetworkType='primary';") ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${ifaceName}.conf restart clientdns_arr=($(docker ps -a --filter name=clientdns.*wireguard --format "{{.Names}}")) for curCDNS in "${clientdns_arr[@]}" do curStackName=$(echo "$curCDNS" | rev | cut -d"-" -f2- | rev) if ! [ -d "$HSHQ_STACKS_DIR/$curStackName" ] || ! [ -f "$HSHQ_STACKS_DIR/$curStackName/${curStackName}.conf" ]; then continue fi startStopStack "$curStackName" stop startStopStack "$curStackName" start done updateEndpointIPs jsonbody="{\"paused\": false}" curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X PATCH -d "$jsonbody" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices/$RELAYSERVER_SYNCTHING_DEVICE_ID docker container restart syncthing perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "echo \"Successful! IP Address is: \$(curl --silent https://api.ipify.org)\"" -f notifyMyNetworkTransferRelayServer removeSudoTimeoutInstall setSystemState $SS_RUNNING } function webSetupHostedVPN() { set +e # These variables should already be set by Script-server # rs_name # rs_cur_username # rs_cur_password # rs_cur_ssh_port # rs_external_ip # rs_ledomains # rs_new_username # rs_new_password # rs_new_ssh_port # rs_primary_vpn_subnet case "$PRIMARY_VPN_SETUP_TYPE" in "host") echo "ERROR: You are already hosting a VPN, please remove the existing one first, returning..." return ;; "join") echo "ERROR: You have already joined an existing primary VPN. You must remove this first, returning..." return ;; "none"|"manual") PRIMARY_VPN_SETUP_TYPE=none updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE "$PRIMARY_VPN_SETUP_TYPE" ;; *) echo "ERROR: Unknown VPN option, returning..." return ;; esac pullImage $IMG_WIREGUARD if [ $? -ne 0 ]; then echo "ERROR: There was a problem pulling the WireGuard docker image..." return fi pullImage $IMG_DNSMASQ if [ $? -ne 0 ]; then echo "ERROR: There was a problem pulling the DNSMasq docker image..." return fi RELAYSERVER_WG_VPN_NETNAME="vpn-"${HOMESERVER_ABBREV:0:6} curNum=1 while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ] && [ $curNum -lt 100 ] do RELAYSERVER_WG_VPN_NETNAME="vpn-${HOMESERVER_ABBREV:0:5}${curNum}" ((curNum++)) done if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ]; then echo "ERROR: You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!? Returning..." return fi RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:6}" curNum=1 while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ] && [ $curNum -lt 100 ] do RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:5}${curNum}" ((curNum++)) done if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ]; then echo "ERROR: You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!? Returning..." return fi RELAYSERVER_EXT_EMAIL_HOSTNAME=$SUB_POSTFIX.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN if [ $(checkValidStringUpperLowerNumbers "$rs_name" "[:space:],.-") = "false" ]; then echo "ERROR: The selected RelayServer name contains invalid characters, returning..." return fi RELAYSERVER_NAME="$rs_name" if ! [ -z "$rs_ledomains" ] && [ $(checkValidString "$rs_ledomains" ",.-") = "false" ]; then echo "ERROR: The selected LetsEncrypt subdomains list contains invalid characters, returning..." return fi if [ "$rs_cur_username" = "root" ]; then if [ "$(checkValidUsername $rs_new_username)" = "false" ]; then echo "ERROR: The selected username is invalid. It must consists of a-z and/or 0-9. No special characters, the length must be between 4-32 characters, and it must begin with a letter, returning..." return fi if [ "$(checkValidPassword "$rs_new_password" 16)" = "false" ]; then echo "ERROR: The password is invalid or is too weak($rs_new_password). It must contain at least 16 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.," return fi USER_RELAY_SUDO_PW="$rs_new_password" else rs_new_username="$rs_cur_username" USER_RELAY_SUDO_PW="$rs_cur_password" fi RELAYSERVER_LE_CERT_DOMAINS="$rs_ledomains" RELAYSERVER_REMOTE_USERNAME="$rs_new_username" RELAYSERVER_SERVER_IP="$rs_external_ip" RELAYSERVER_CURRENT_SSH_PORT="$rs_cur_ssh_port" if [ "$(checkValidIPAddress $rs_primary_vpn_subnet)" = "false" ]; then echo "ERROR: Invalid RelayServer IP address, returning..." return fi if ! [ "$(echo "$rs_primary_vpn_subnet" | cut -d"." -f1)" = "10" ]; then echo "ERROR: VPN subnet must be in the 10.0.0.0/8 range, returning..." return fi is_intersect="$(isNetworkIntersectOurNetworks ${rs_primary_vpn_subnet}/24 false true)" if ! [ -z "$is_intersect" ]; then echo "ERROR: VPN subnet collision - $is_intersect, returning..." return fi PRIMARY_VPN_SUBNET="${rs_primary_vpn_subnet}/24" PRIMARY_VPN_SETUP_TYPE=host setupPortForwardingDB chkSSHPort="$(checkPortForwardingIntersect tcp $rs_new_ssh_port $rs_new_ssh_port)" if ! [ -z "$chkSSHPort" ]; then sudo sqlite3 $HSHQ_DB "drop table portforwarding;" echo "ERROR: $chkSSHPort, returning..." return fi RELAYSERVER_SSH_PORT="$rs_new_ssh_port" RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT="" tmp_port=$PORTAINER_LOCAL_HTTPS_PORT while [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ] do chkSSHPort="$(checkPortForwardingIntersect tcp $tmp_port $tmp_port)" if ! [ -z "$chkSSHPort" ]; then tmp_port=$((9000 + $RANDOM % 999)) else RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=$tmp_port break fi done # Clear out any old hosts ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1 ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SERVER_IP]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1 if [ -z "$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME" ]; then echo "Generating keys, please wait..." RELAYSERVER_SSH_PRIVATE_KEY_FILENAME=$HOMESERVER_ABBREV".key" updateConfigVar RELAYSERVER_SSH_PRIVATE_KEY_FILENAME $RELAYSERVER_SSH_PRIVATE_KEY_FILENAME rm -f $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME rm -f $HSHQ_CONFIG_DIR/${RELAYSERVER_SSH_PRIVATE_KEY_FILENAME}.pub RELAYSERVER_SSH_PRIVATE_KEY_PHRASE=$(pwgen -c -n 64 1) updateConfigVar RELAYSERVER_SSH_PRIVATE_KEY_PHRASE $RELAYSERVER_SSH_PRIVATE_KEY_PHRASE ssh-keygen -f $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME -N $RELAYSERVER_SSH_PRIVATE_KEY_PHRASE chmod 0400 $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME chmod 0400 $HSHQ_CONFIG_DIR/${RELAYSERVER_SSH_PRIVATE_KEY_FILENAME}.pub fi # Login, upload check script, log back in and run check script # 1. Login echo "Logging into RelayServer..." perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -o "-T -o ConnectTimeout=5 -o 'StrictHostKeyChecking accept-new'" -u "$rs_cur_username" -h "$RELAYSERVER_SERVER_IP" -c "echo hello >/dev/null" -f is_err=$? if [ $is_err -ne 0 ]; then echo "ERROR: There was a problem logging in to the RelayServer, returning..." return fi # 2. Upload check script echo "Uploading validation script..." outputRelayServerValidationScript outputRelayServerInstallSetupScript perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -a $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c ":~" -f is_err=$? rm -f $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME if [ $is_err -ne 0 ]; then echo "ERROR: There was an problem uploading the validation script to the RelayServer, returning..." return fi perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c ":~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME" -f is_err=$? rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME if [ $is_err -ne 0 ]; then echo "ERROR: There was an problem uploading the setup script to the RelayServer, returning..." return fi rm -f $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME # 3. Log back in and run check script if ! [ "$rs_cur_username" = "root" ]; then rs_new_password="$rs_cur_password" fi echo "Performing pre-installation checks, please wait..." perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c "bash ~/$RS_INSTALL_VALIDATION_SCRIPT_NAME -s" -f -i "$rs_new_password" is_err=$? if [ $is_err -ne 0 ]; then echo "ERROR: There was a problem with the RelayServer, see above." return fi echo "Testing RelayServer login with pub/priv key..." perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "echo \"Successfully logged in to RelayServer with key! \"" -f is_err=$? if [ $is_err -ne 0 ]; then echo "ERROR: Could not login to RelayServer with pub/priv key." return fi echo "All is good, assigning values..." RELAYSERVER_HSHQ_BASE_DIR=/home/$RELAYSERVER_REMOTE_USERNAME/hshq RELAYSERVER_HSHQ_DATA_DIR=$RELAYSERVER_HSHQ_BASE_DIR/data RELAYSERVER_HSHQ_NONBACKUP_DIR=$RELAYSERVER_HSHQ_BASE_DIR/nonbackup RELAYSERVER_HSHQ_SCRIPTS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/scripts RELAYSERVER_HSHQ_SECRETS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/secrets RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/stacks RELAYSERVER_HSHQ_SSL_DIR=$RELAYSERVER_HSHQ_DATA_DIR/ssl resetRelayServerData updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE "$PRIMARY_VPN_SETUP_TYPE" updateConfigVar RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT # SSH (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-SSH', $RELAYSERVER_SSH_PORT, $RELAYSERVER_SSH_PORT, 0, 0, 'both','','','$curdt');" updateConfigVar RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT # Portainer (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-Portainer', $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, 0, 0, 'both','','','$curdt');" updateConfigVar RELAYSERVER_WG_VPN_NETNAME "$RELAYSERVER_WG_VPN_NETNAME" updateConfigVar RELAYSERVER_WG_INTERNET_NETNAME "$RELAYSERVER_WG_INTERNET_NETNAME" updateConfigVar RELAYSERVER_EXT_EMAIL_HOSTNAME $RELAYSERVER_EXT_EMAIL_HOSTNAME updateConfigVar RELAYSERVER_NAME "$RELAYSERVER_NAME" updateConfigVar RELAYSERVER_LE_CERT_DOMAINS $RELAYSERVER_LE_CERT_DOMAINS updateConfigVar RELAYSERVER_REMOTE_USERNAME $RELAYSERVER_REMOTE_USERNAME updateConfigVar RELAYSERVER_HSHQ_BASE_DIR $RELAYSERVER_HSHQ_BASE_DIR updateConfigVar RELAYSERVER_HSHQ_DATA_DIR $RELAYSERVER_HSHQ_DATA_DIR updateConfigVar RELAYSERVER_HSHQ_NONBACKUP_DIR $RELAYSERVER_HSHQ_NONBACKUP_DIR updateConfigVar RELAYSERVER_HSHQ_SCRIPTS_DIR $RELAYSERVER_HSHQ_SCRIPTS_DIR updateConfigVar RELAYSERVER_HSHQ_SECRETS_DIR $RELAYSERVER_HSHQ_SECRETS_DIR updateConfigVar RELAYSERVER_HSHQ_STACKS_DIR $RELAYSERVER_HSHQ_STACKS_DIR updateConfigVar RELAYSERVER_HSHQ_SSL_DIR $RELAYSERVER_HSHQ_SSL_DIR updateConfigVar RELAYSERVER_SERVER_IP $RELAYSERVER_SERVER_IP updateConfigVar RELAYSERVER_CURRENT_SSH_PORT $RELAYSERVER_CURRENT_SSH_PORT updatePlaintextRootConfigVar PRIMARY_VPN_SUBNET $PRIMARY_VPN_SUBNET leCertsArr=($(echo $RELAYSERVER_LE_CERT_DOMAINS | tr "," "\n")) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains;" for leCert in "${leCertsArr[@]}" do sudo sqlite3 $HSHQ_DB "insert or ignore into lecertdomains(Domain,BaseDomain) values('$leCert','$HOMESERVER_DOMAIN');" curBaseDomain=$(getBaseDomain $leCert) curSubDomain=$(getSubDomain $leCert) if [ "$curBaseDomain" = "$HOMESERVER_DOMAIN" ]; then for curSVC in "${SVCS_ARR[@]}" do var_base=$(echo $curSVC | cut -d"=" -f1 | cut -d"_" -f 2-) subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6) subdom="${subdom//\"}" if [ "$subdom" = "$curSubDomain" ]; then var_name="SVCD_"$var_base var_value=$(getConfigVar $var_name) var_valueArr=($(echo $var_value | tr "," "\n")) var_valueArr[6]=le printf -v new_value '%s,' "${var_valueArr[@]}" new_value="$(echo "${new_value%,}")" updateConfigVar $var_name $new_value fi done fi done initRelayServerCredentials outputHostedVPNConfigs insertSQLHostedVPN # This upload must take place as soon as possible after the # validation check, as the RelayServer might be installing # docker and reboooting (required for debian 12 atm) # See "Performing pre-installation checks" comment above. echo "Generating RelayServer install scripts..." outputRelayServerInstallSetupScript outputRelayServerInstallFreshScript addDomainAdguardHS "*.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "$RELAYSERVER_SERVER_IP" perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "touch ~/$RELAYSERVER_SCRIPTS_UPLOADED_FILE" -f rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME set +e sendDNSRecordsEmail $HOMESERVER_DOMAIN sleep 2 prepSvcsHostedVPN set +e startStopStack mailu stop sleep 1 echo "Generating mailu certs..." generateCert mail "$SMTP_HOSTNAME,$SUB_POSTFIX.$HOMESERVER_DOMAIN" echo "Setting mailu relay creds..." updateMailuStackRelayHost # Start Install set +e echo "Starting installation on RelayServer..." countRSPreRetries=0 maxRSPreRetries=20 while true; do perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "sleep 1;if [ -f ~/$RELAYSERVER_NOT_READY_FILE ]; then exit 1; fi" -r 1 -b 1 if [ $? -eq 0 ]; then break fi if [ $countRSPreRetries -ge $maxRSPreRetries ]; then echo "Max retries reached. Something has likely gone wrong with the RelayServer pre-installation script, exiting..." exit 2 fi ((countRSPreRetries++)) echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for installation, sleeping 30 seconds then will retry..." sleep 30 done perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "bash $RS_INSTALL_FRESH_SCRIPT_NAME -s" -i "$USER_RELAY_SUDO_PW" -f perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-t -o ConnectTimeout=10" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "export TERM=linux; screen -r hshqInstall" -r 1 -b 1 echo "RelayServer is rebooting..." echo "Sleeping 10 seconds, then will attempt to connect..." sleep 10 set +e connectPrimaryInternet set +e connectPrimaryVPN true set +e updateEndpointIPs set +e rs_wg_user_conf=$(sudo cat $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf) wgconfig="" wgconfig="$wgconfig""============= User WireGuard Configuration ============\n" wgconfig="$wgconfig""""""$rs_wg_user_conf""""\n" wgconfig="$wgconfig""=======================================================\n" if [ "$IS_DESKTOP_ENV" = "true" ]; then wgconfigfile="$HOME/Desktop/User1_WireGuard.conf" else wgconfigfile="$HOME/User1_WireGuard.conf" fi rm -f $wgconfigfile echo -e "$wgconfig" > $wgconfigfile } function setupHostedVPN() { echo "Checking for required utils, please wait..." if [[ "$(isProgramInstalled dig)" = "false" ]]; then echo "Installing dig, please wait..." performAptInstall dnsutils > /dev/null 2>&1 fi if [[ "$(isProgramInstalled sshpass)" = "false" ]]; then echo "Installing sshpass, please wait..." performAptInstall sshpass > /dev/null 2>&1 fi if [[ "$(isProgramInstalled sipcalc)" = "false" ]]; then echo "Installing sipcalc, please wait..." performAptInstall sipcalc > /dev/null 2>&1 fi resetRelayServerData if [ -z "$RELAYSERVER_WG_VPN_NETNAME" ]; then RELAYSERVER_WG_VPN_NETNAME="vpn-"${HOMESERVER_ABBREV:0:6} curNum=1 while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ] && [ $curNum -lt 100 ] do RELAYSERVER_WG_VPN_NETNAME="vpn-${HOMESERVER_ABBREV:0:5}${curNum}" ((curNum++)) done if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ]; then showMessageBox "Too Many Connections" "You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!?" return 1 fi updateConfigVar RELAYSERVER_WG_VPN_NETNAME "$RELAYSERVER_WG_VPN_NETNAME" fi if [ -z "$RELAYSERVER_WG_INTERNET_NETNAME" ]; then RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:6}" curNum=1 while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ] && [ $curNum -lt 100 ] do RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:5}${curNum}" ((curNum++)) done if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ]; then showMessageBox "Too Many Connections" "You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!?" return 1 fi updateConfigVar RELAYSERVER_WG_INTERNET_NETNAME "$RELAYSERVER_WG_INTERNET_NETNAME" fi if [ -z "$IS_ACCEPT_DEFAULTS" ]; then set +e showYesNoMessageBox "Accept Defaults?" "Do you wish to use defaults where applicable?" mbres=$? if [ $mbres -eq 0 ]; then IS_ACCEPT_DEFAULTS=yes else IS_ACCEPT_DEFAULTS=no fi set -e fi set +e sudo sqlite3 $HSHQ_DB "drop table portforwarding;" > /dev/null 2>&1 set -e setupPortForwardingDB RELAYSERVER_EXT_EMAIL_HOSTNAME=$SUB_POSTFIX.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN updateConfigVar RELAYSERVER_EXT_EMAIL_HOSTNAME $RELAYSERVER_EXT_EMAIL_HOSTNAME RELAYSERVER_SSH_PORT="" while [ -z "$RELAYSERVER_SSH_PORT" ] do RELAYSERVER_SSH_PORT=$(promptUserInputMenu "$SSH_PORT" "Enter NEW RelayServer SSH Port" "Enter your desired SSH Port for your RelayServer. It is highly advised to change your default SSH port (22). Bots will constantly probe port 22:") if [ -z "$RELAYSERVER_SSH_PORT" ]; then showMessageBox "SSH Port Empty" "The SSH port cannot be empty" elif [ "$(checkValidNumber $RELAYSERVER_SSH_PORT)" = "false" ]; then showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number." RELAYSERVER_SSH_PORT="" elif [ $RELAYSERVER_SSH_PORT -lt 1024 ] || [ $RELAYSERVER_SSH_PORT -gt 65536 ]; then showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536." RELAYSERVER_SSH_PORT="" fi if ! [ -z "$RELAYSERVER_SSH_PORT" ]; then chkSSHPort="$(checkPortForwardingIntersect tcp $RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT)" if ! [ -z "$chkSSHPort" ]; then showMessageBox "ERROR" "Error with port: $chkSSHPort" RELAYSERVER_SSH_PORT="" else updateConfigVar RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT fi fi done # SSH (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-SSH', $RELAYSERVER_SSH_PORT, $RELAYSERVER_SSH_PORT, 0, 0, 'both','','','$curdt');" RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT="" while [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ] do tmp_port=$((9000 + $RANDOM % 999)) while [ "$tmp_port" = "$RELAYSERVER_SSH_PORT" ] do tmp_port=$((9000 + $RANDOM % 999)) done if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT="$tmp_port" else RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=$(promptUserInputMenu "$tmp_port" "Enter RelayServer Portainer Port" "Enter a port to use for Portainer on the RelayServer. A random port between 9000-9999 has been generated for you.") fi if [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ]; then showMessageBox "Port Empty" "The port cannot be empty" elif [ "$(checkValidNumber $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT)" = "false" ]; then showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number." RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT="" elif [ $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT -lt 1024 ] || [ $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT -gt 65536 ]; then showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536." RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT="" fi if ! [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ]; then chkSSHPort="$(checkPortForwardingIntersect tcp $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT)" if ! [ -z "$chkSSHPort" ]; then showMessageBox "ERROR" "Error with port: $chkSSHPort" RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT="" else updateConfigVar RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT fi fi done # Portainer (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-Portainer', $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, 0, 0, 'both','','','$curdt');" while [ -z "$RELAYSERVER_NAME" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then RELAYSERVER_NAME="$HOMESERVER_NAME RelayServer" else RELAYSERVER_NAME=$(promptUserInputMenu "$HOMESERVER_NAME RelayServer" "Enter RelayServer Name" "Enter the RelayServer Name:") fi if [ -z "$RELAYSERVER_NAME" ]; then showMessageBox "RelayServer Name Empty" "The RelayServer Name cannot be empty" elif [ $(checkValidStringUpperLowerNumbers "$RELAYSERVER_NAME" "[:space:],.-") = "false" ]; then showMessageBox "Invalid Character(s)" "The RelayServer name contains invalid character(s). It must consist of upper/lowercase letters, numbers, spaces, commas, periods, or hyphens." RELAYSERVER_NAME="" else updateConfigVar RELAYSERVER_NAME "$RELAYSERVER_NAME" fi done while [ "$RELAYSERVER_LE_CERT_DOMAINS" = "unset" ] do lecert_def=$(getLetsEncryptCertsDefault) if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then RELAYSERVER_LE_CERT_DOMAINS="$lecert_def" else RELAYSERVER_LE_CERT_DOMAINS=$(promptUserInputMenu "$lecert_def" "Enter LE Cert Subdomains" "Enter the subdomains for which the certificates will be managed by LetsEncrypt (comma-separated):") if ! [ -z "$RELAYSERVER_LE_CERT_DOMAINS" ] && [ $(checkValidString "$RELAYSERVER_LE_CERT_DOMAINS" ",.-") = "false" ]; then showMessageBox "Invalid Character(s)" "The domain list contains invalid character(s). It must consist of a-z (lowercase), 0-9, -, and/or ." RELAYSERVER_LE_CERT_DOMAINS=unset fi fi done updateConfigVar RELAYSERVER_LE_CERT_DOMAINS $RELAYSERVER_LE_CERT_DOMAINS leCertsArr=($(echo $RELAYSERVER_LE_CERT_DOMAINS | tr "," "\n")) isReload=false sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains;" for leCert in "${leCertsArr[@]}" do sudo sqlite3 $HSHQ_DB "insert or ignore into lecertdomains(Domain,BaseDomain) values('$leCert','$HOMESERVER_DOMAIN');" curBaseDomain=$(getBaseDomain $leCert) curSubDomain=$(getSubDomain $leCert) if [ "$curBaseDomain" = "$HOMESERVER_DOMAIN" ]; then for curSVC in "${SVCS_ARR[@]}" do var_base=$(echo $curSVC | cut -d"=" -f1 | cut -d"_" -f 2-) subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6) subdom="${subdom//\"}" if [ "$subdom" = "$curSubDomain" ]; then var_name="SVCD_"$var_base var_value=$(getConfigVar $var_name) var_valueArr=($(echo $var_value | tr "," "\n")) var_valueArr[6]=le printf -v new_value '%s,' "${var_valueArr[@]}" new_value="$(echo "${new_value%,}")" updateConfigVar $var_name $new_value isReload=true fi done fi done if [ "$isReload" = "true" ]; then loadSvcVars fi echo "Initializing RelayServer service credentials..." num_tries=1 max_tries=100 PRIMARY_VPN_SUBNET="" while [ -z "$PRIMARY_VPN_SUBNET" ] && [ $num_tries -lt $max_tries ] do ((num_tries++)) if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then PRIMARY_VPN_SUBNET=10.$(( $RANDOM % 256 )).$(( $RANDOM % 256 )).0/24 else PRIMARY_VPN_SUBNET=$(promptUserInputMenu "10.$(( $RANDOM % 256 )).$(( $RANDOM % 256 )).0/24" "Enter Subnet" "Enter the VPN Hosting Subnet (in CIDR): ") fi if [ -z "$PRIMARY_VPN_SUBNET" ] || [ "$(checkValidIPAddress $PRIMARY_VPN_SUBNET)" = "false" ]; then showMessageBox "Invalid Subnet" "The VPN Subnet is invalid." PRIMARY_VPN_SUBNET="" continue fi is_intersect="$(isNetworkIntersectOurNetworks $PRIMARY_VPN_SUBNET false true)" if ! [ -z "$is_intersect" ]; then if ! [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then showMessageBox "Network Collision" "Network Collision: $is_intersect" fi PRIMARY_VPN_SUBNET="" continue fi set +e firstOct=$(echo "$PRIMARY_VPN_SUBNET" | cut -d"." -f1) if ! [ "$firstOct" = "10" ]; then showYesNoMessageBox "Confirm" "This subnet is not in the 10.0.0.0/8 range. It is highly recommended that you stay within this range. Are you sure you know what you are doing?" if [ $? -ne 0 ]; then PRIMARY_VPN_SUBNET="" continue fi fi vpnCIDR=$(echo "$PRIMARY_VPN_SUBNET" | cut -d"/" -f2-) if ! [ "$vpnCIDR" = "24" ]; then showYesNoMessageBox "The VPN subnet must be of size /24." PRIMARY_VPN_SUBNET="" continue fi set -e updatePlaintextRootConfigVar PRIMARY_VPN_SUBNET $PRIMARY_VPN_SUBNET done if [ -z "$PRIMARY_VPN_SUBNET" ]; then # We tried... showMessageBox "ERROR" "Could not allocate VPN network subnet, returning..." return 1 fi initRelayServerCredentials outputHostedVPNConfigs insertSQLHostedVPN echo "Generating RelayServer install scripts..." outputRelayServerInstallSetupScript outputRelayServerInstallFreshScript set +e uploadVPNInstallScripts if [ $? -ne 0 ]; then sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains;" return 1 fi if [ "$IS_INSTALLED" = "true" ]; then echo "Preparing mailu, please wait..." startStopStack mailu stop sleep 5 echo "Generating mailu certs..." generateCert mail "$SMTP_HOSTNAME,$SUB_POSTFIX.$HOMESERVER_DOMAIN" startStopStack mailu start prepSvcsHostedVPN fi updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE } function outputRelayServerValidationScript() { # This function assumes rs_cur_username and rs_new_username have been defined before calling it. # It also assumes that the caller will send the current/new user's password immediately via # standard input. if [ -z "$rs_cur_username" ] || [ -z "$rs_new_username" ]; then # Fatal echo "RelayServer usernames undefined for script, exiting..." exit 5 fi tee $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME >/dev/null < /dev/null 2>&1 if [ \$? -ne 0 ]; then if [ "\$curUsername" = "root" ]; then DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 dpkg --configure -a > /dev/null 2>&1 DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' sudo > /dev/null 2>&1 else echo "-----------------------------------------------------------------" echo " ERROR: Sudo is not installed. Please either log in with" echo " the root account or install it manually." echo "-----------------------------------------------------------------" exit 2 fi fi if [ "\$curUsername" = "root" ]; then read -r -s -p "" USER_RELAY_SUDO_PW id "\$newUsername" > /dev/null 2>&1 if [ \$? -eq 0 ]; then echo "-----------------------------------------------------------------" echo " ERROR: User (\$newUsername) already exists." echo "-----------------------------------------------------------------" removeMyself exit 1 fi checkForHSHQ checkForRunningDockerContainers checkForBoundPorts sudo useradd -m -G sudo -s /bin/bash "\$newUsername" > /dev/null 2>&1 echo "\$newUsername:\$USER_RELAY_SUDO_PW" | sudo chpasswd > /dev/null 2>&1 else read -r -s -p "" USER_RELAY_SUDO_PW echo "\$USER_RELAY_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "-----------------------------------------------------------------" echo " ERROR: The sudo password for \$curUsername is incorrect." echo "-----------------------------------------------------------------" removeMyself exit 2 fi sudo bash -c "\$(declare -f checkForValidOSVersion); checkForValidOSVersion \$curUsername" 2> /dev/null if [ \$? -ne 0 ]; then removeMyself exit 2 fi sudo bash -c "\$(declare -f checkForHSHQ); checkForHSHQ" 2> /dev/null if [ \$? -ne 0 ]; then removeMyself exit 2 fi sudo bash -c "\$(declare -f checkForRunningDockerContainers); checkForRunningDockerContainers" 2> /dev/null if [ \$? -ne 0 ]; then removeMyself exit 2 fi sudo bash -c "\$(declare -f checkBoundPort; declare -f checkForBoundPorts); checkForBoundPorts" 2> /dev/null if [ \$? -ne 0 ]; then removeMyself exit 2 fi fi getent group docker >/dev/null || sudo groupadd docker > /dev/null 2>&1 sudo usermod -aG docker \$newUsername > /dev/null 2>&1 mkdir -p /home/\$newUsername/.ssh chmod 775 /home/\$newUsername/.ssh pubk=\$(echo "\$pubkey" | cut -d " " -f2) grep "\$pubk" /home/\$newUsername/.ssh/authorized_keys > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "\$pubkey" >> /home/\$newUsername/.ssh/authorized_keys fi chown -R \$newUsername:\$newUsername /home/\$newUsername/.ssh rm -f /home/\$newUsername/$RS_INSTALL_SETUP_SCRIPT_NAME rm -f /home/\$newUsername/$RS_INSTALL_FRESH_SCRIPT_NAME checkPerformPreInstall } function checkForValidOSVersion() { source /home/\$1/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME lib checkSupportedHostOS rtVal=\$? if [ \$rtVal -ne 0 ]; then exit \$rtVal fi } function checkForHSHQ() { findhshq="\$(find /home -maxdepth 3 -type d -name hshq 2>/dev/null | head -n 1)" if ! [ -z "\$findhshq" ]; then echo "------------------------------------------------------------------------------------" echo " ERROR: An hshq directory already exists: \$findhshq. You must log into the" echo " RelayServer and run the nuke.sh script (bash nuke.sh) to clear everything out." echo "------------------------------------------------------------------------------------" exit 5 fi } function checkForRunningDockerContainers() { is_containers=\$(docker ps -q 2>/dev/null | head -n 1) if ! [ -z "\$is_containers" ]; then echo "------------------------------------------------------------------------------------" echo " ERROR: There are currently running docker containers," echo " the server must be clear of all activity." echo "------------------------------------------------------------------------------------" exit 6 fi } function checkForBoundPorts() { checkBoundPort $MAILU_PORT_1 checkBoundPort $MAILU_PORT_5 checkBoundPort $CADDY_HTTP_PORT checkBoundPort $CADDY_HTTPS_PORT checkBoundPort $SYNCTHING_SYNC_PORT checkBoundPort $SYNCTHING_DISC_PORT checkBoundPort $SYNCTHING_LOCAL_WEB_PORT checkBoundPort $RELAYSERVER_WG_PORTAL_PORT checkBoundPort $RELAYSERVER_WG_PORT } function checkBoundPort() { chkPort=\$1 set +e sudo netstat -tulpn 2>/dev/null | grep :\$chkPort > /dev/null 2>&1 if [ \$? -eq 0 ]; then echo "------------------------------------------------------------------------------------" echo " A needed port is bound by another service: \$chkPort" echo " The server must be clear of all services on the following port lists:" echo " External - $MAILU_PORT_1, $CADDY_HTTP_PORT, $CADDY_HTTPS_PORT, $RELAYSERVER_WG_PORT" echo " Internal - $MAILU_PORT_5, $SYNCTHING_SYNC_PORT, $SYNCTHING_DISC_PORT, $SYNCTHING_LOCAL_WEB_PORT, $RELAYSERVER_WG_PORTAL_PORT" echo "------------------------------------------------------------------------------------" exit 7 fi } function removeMyself() { rm -f ~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME rm -f \$0 } function checkPerformPreInstall() { sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo dpkg --configure -a > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1 which screen > /dev/null 2>&1 if [ \$? -ne 0 ]; then DEBIAN_FRONTEND=noninteractive sudo apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' screen > /dev/null 2>&1 fi which pwgen > /dev/null 2>&1 if [ \$? -ne 0 ]; then DEBIAN_FRONTEND=noninteractive sudo apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' pwgen > /dev/null 2>&1 fi which netstat > /dev/null 2>&1 if [ \$? -ne 0 ]; then DEBIAN_FRONTEND=noninteractive sudo apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' net-tools > /dev/null 2>&1 fi docker ps > /dev/null 2>&1 if [ \$? -ne 0 ]; then source ~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME lib if [ "\$DISTRO_ID" = "debian" ] && [[ "\$DISTRO_VERSION" =~ ^12. ]]; then echo "Installing docker on RelayServer and rebooting it..." # Must install docker and reboot touch /home/\$newUsername/$RELAYSERVER_NOT_READY_FILE scName=hshqPreInstall initScreen "\$scName" screen -S "\$scName" -X stuff "bash \$0 -i\n" return fi fi removeMyself } function performDebian12PreInstall() { sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1 source ~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME lib installDocker echo "Checking for uploaded scripts..." # Need to ensure other files have been uploading before rebooting. maxCount=120 curCount=0 while [ \$curCount -lt \$maxCount ] do if [ -f /home/\$newUsername/$RELAYSERVER_SCRIPTS_UPLOADED_FILE ]; then break fi sleep 5 ((curCount++)) done echo "Scripts found, rebooting..." rm -f /home/\$newUsername/$RELAYSERVER_SCRIPTS_UPLOADED_FILE removeMyself rm -f /home/\$newUsername/$RELAYSERVER_NOT_READY_FILE reboot } function getSuper() { set +e stty -echo mkdir -p $TMP_PW_CHECKDIR read -r -s -p "" sudoPWKey USER_RELAY_SUDO_PW="\$(openssl enc -d -aes256 -pbkdf2 -in $ENC_PW_FILE -pass pass:\$sudoPWKey)" rm -f $ENC_PW_FILE echo "\$USER_RELAY_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ \$? -ne 0 ]; then exit 6 fi rm -fr $TMP_PW_CHECKDIR clear stty echo } function initScreen() { set +e screenName="\$1" dirtest1=/tmp/scPre1 isScreenSuccess=false curScreenAttempt=0 maxScreenAttempt=5 screen -XS \$screenName quit > /dev/null 2>&1 sudoPWKey=\$(pwgen -c -n 32 1) echo -n "\$USER_RELAY_SUDO_PW" | openssl enc -e -aes256 -pbkdf2 -pass pass:\$sudoPWKey > $ENC_PW_FILE while [ \$curScreenAttempt -lt \$maxScreenAttempt ] do ((curScreenAttempt++)) rm -fr \$dirtest1 rm -fr $TMP_PW_CHECKDIR screen -dmS \$screenName screen -S \$screenName -X stuff "stty -echo;mkdir \$dirtest1\n" curCheckCount=0 while ! [ -d \$dirtest1 ] && [ \$curCheckCount -lt 5 ] do ((curCheckCount++)) sleep 1 done if ! [ -d \$dirtest1 ]; then screen -XS \$screenName quit > /dev/null 2>&1 continue fi screen -S \$screenName -X stuff "bash \$0 -p\n" curCheckCount=0 while ! [ -d $TMP_PW_CHECKDIR ] && [ \$curCheckCount -lt 15 ] do ((curCheckCount++)) sleep 1 done screen -S \$screenName -X stuff "\$sudoPWKey\n" curCheckCount=0 while [ -d $TMP_PW_CHECKDIR ] && [ \$curCheckCount -lt 15 ] do ((curCheckCount++)) sleep 1 done rm -fr \$dirtest1 if [ -d $TMP_PW_CHECKDIR ]; then rm -fr $TMP_PW_CHECKDIR echo "The sudo password was not correctly transmitted to the screen session, exiting..." exit 5 fi rm -fr $TMP_PW_CHECKDIR isScreenSuccess=true break done USER_RELAY_SUDO_PW="" if ! [ "\$isScreenSuccess" = "true" ]; then return 1 fi } main "\$@" EOFRS chmod 600 $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME } function outputRelayServerInstallSetupScript() { rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME cat < $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME #!/bin/bash set +e function init() { USERNAME=\$(id -u -n) RELAYSERVER_HSHQ_BASE_DIR=\$HOME/hshq RELAYSERVER_HSHQ_DATA_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/data RELAYSERVER_HSHQ_NONBACKUP_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/nonbackup RELAYSERVER_HSHQ_SCRIPTS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/scripts NET_EXTERNAL_SUBNET=$RELAYSERVER_NET_EXTERNAL_SUBNET NET_EXTERNAL_SUBNET_PREFIX=$RELAYSERVER_NET_EXTERNAL_SUBNET_PREFIX NET_WEBPROXY_SUBNET=$RELAYSERVER_NET_WEBPROXY_SUBNET NET_WEBPROXY_SUBNET_PREFIX=$RELAYSERVER_NET_WEBPROXY_SUBNET_PREFIX MAIL_RELAY_SUBNET=$RELAYSERVER_MAIL_RELAY_SUBNET MAIL_RELAY_SUBNET_PREFIX=$RELAYSERVER_MAIL_RELAY_SUBNET_PREFIX CLIENT_DNS_SUBNET=$RELAYSERVER_CLIENT_DNS_SUBNET CLIENT_DNS_SUBNET_PREFIX=$RELAYSERVER_CLIENT_DNS_SUBNET_PREFIX loadVersionVars IS_RUST_UTILS=false if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^26\.04. ]]; then IS_RUST_UTILS=true fi } function setupMain() { init echo "Running setup script..." rm -f ~/$RELAYSERVER_SCRIPTS_UPLOADED_FILE outputNukeScript new_hostname="RelayServer-$(echo $HOMESERVER_DOMAIN | sed 's/\./-/g')" if [ -z "\$(cat /etc/hosts | grep \$new_hostname)" ]; then echo "127.0.1.1 \$new_hostname" | sudo tee -a /etc/hosts fi sudo hostnamectl set-hostname \$new_hostname rm -f \$HOME/dead.letter # Ensure we can login during the installation process, # since the firewall will block the connection if we # haven't logged in before the firewall goes up. cur_ssh_port=\$(sudo grep ^Port /etc/ssh/sshd_config) if [ \$? -ne 0 ]; then cur_ssh_port=22 else cur_ssh_port=\$(sudo grep ^Port /etc/ssh/sshd_config | xargs | cut -d" " -f2) fi if [[ "\$(isProgramInstalled iptables)" = "false" ]]; then sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 performAptInstall iptables > /dev/null 2>&1 fi sudo iptables -C INPUT -p tcp -m tcp --dport \$cur_ssh_port -j ACCEPT > /dev/null 2>&1 || sudo iptables -A INPUT -p tcp -m tcp --dport \$cur_ssh_port -j ACCEPT # Change SSH port on host sudo sed -i "s|^#*Port .*\$|Port $RELAYSERVER_SSH_PORT|g" /etc/ssh/sshd_config sudo sed -i "s|^Port .*\$|Port $RELAYSERVER_SSH_PORT|g" /etc/ssh/sshd_config # Some other SSH settings sudo sed -i "s|^ClientAliveInterval .*\$|ClientAliveInterval 15|g" /etc/ssh/sshd_config sudo sed -i "s|^ClientAliveCountMax .*\$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config sudo sed -i "s|^PermitEmptyPasswords .*\$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config sudo sed -i "s|^#*ClientAliveInterval .*\$|ClientAliveInterval 15|g" /etc/ssh/sshd_config sudo sed -i "s|^#*ClientAliveCountMax .*\$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config sudo sed -i "s|^#*PermitEmptyPasswords .*\$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config # Update sudoers file sudo sed -i '/\/userasroot/d' /etc/sudoers >/dev/null echo "\$USERNAME ALL=(ALL) NOPASSWD: \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/*.sh" | sudo tee -a /etc/sudoers >/dev/null sudo sed -i '/timestamp_timeout/d' /etc/sudoers >/dev/null echo "Defaults timestamp_timeout=$SUDO_NORMAL_TIMEOUT" | sudo tee -a /etc/sudoers >/dev/null sudo sed -i '/logfile=/d' /etc/sudoers >/dev/null if [ "\$IS_RUST_UTILS" = "true" ]; then echo "authpriv.* /var/log/sudo.log" | sudo tee /etc/rsyslog.d/50-sudo.conf >/dev/null sudo systemctl restart rsyslog else echo "Defaults logfile=/var/log/sudo.log" | sudo tee -a /etc/sudoers >/dev/null fi sudo sed -i '/includedir/d' /etc/sudoers >/dev/null echo "@includedir /etc/sudoers.d" | sudo tee -a /etc/sudoers >/dev/null # Set timezone sudo timedatectl set-timezone "$TZ" # Create swap file createSwapfile installDependencies createDockerNetworks } function createSwapfile() { sudo grep swapfile /etc/fstab > /dev/null 2>&1 if [ \$? -ne 0 ] && ! [ -f /swapfile ]; then sudo fallocate -l $RELAYSERVER_SWAP_SIZE /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo "# Swap file" | sudo tee -a /etc/fstab > /dev/null 2>&1 echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1 fi } function updateSysctl() { set +e sudo tee /etc/sysctl.d/88-hshq.conf >/dev/null < /dev/null 2>&1 } function updateMOTD() { cat < \$HOME/88-hshq #!/bin/bash echo echo " #===============================================================#" echo " # ▀█ █ ▄▄ ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄ █ █ █▀▀█ #" echo " # █▀▀█ █ █ █ █ █ ▄▄ ▀▀▀▄▄ ▄▄ ▄▄▄▀ ▀▄ ▄▀ ▄▄ ▄▄▄▀ █▀▀█ █ █ #" echo " # █ █ ▀▄▄▀ █ █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄ ▀█▀ ▄▄▄ ▄▄▄▄ █ █ █▄▄█▄ #" echo " #===============================================================#" echo echo " Linux OS: \\\$(lsb_release -d | cut -d":" -f2 | cut -d"(" -f1 | xargs)" printf " Memory Usage: %.1f%% of \\\$(free -h | awk '/Mem:/{print \\\$2}')\n" \\\$((10000 - \\\$((10**4 * \\\$(grep "MemAvailable" /proc/meminfo | xargs | cut -d" " -f2) / \\\$(grep "MemTotal" /proc/meminfo | xargs | cut -d" " -f2)))))e-2 totSwap=\\\$(grep "SwapTotal" /proc/meminfo | xargs | cut -d" " -f2) if [ \\\$totSwap -gt 0 ]; then printf " Swap Usage: %.1f%% of \\\$(free -h | awk '/Swap:/{print \\\$2}')\n" \\\$((10000 - \\\$((10**4 * \\\$(grep "SwapFree" /proc/meminfo | xargs | cut -d" " -f2) / \\\$totSwap))))e-2 fi echo echo " Disks: " echo " -----------------------------------------------------------------" echo " Filesystem Size Used Avail Use% Mounted on" echo " -----------------------------------------------------------------" df -hP | grep "^/dev" | awk '{printf " %-21s %-7s %-7s %-7s %-7s %-8s\n", \\\$1, \\\$2, \\\$3, \\\$4, \\\$5, \\\$6}' echo # Let's only show zombie count if greater than 10 zombie_count=\\\$(ps aux | grep "defunct" | wc -l) if [ \\\$zombie_count -gt 10 ]; then echo " => There are \\\$zombie_count zombie processes." echo fi EOFMD chmod 755 \$HOME/88-hshq sudo chown root:root \$HOME/88-hshq sudo chmod -x /etc/update-motd.d/* sudo mv \$HOME/88-hshq /etc/update-motd.d/ if [ -f /etc/motd ]; then sudo mv /etc/motd /etc/motd.old fi } function performSuggestedSecUpdates() { disa_curE=\${-//[^e]/} set +e grep "enabled=1" /etc/default/apport > /dev/null 2>&1 if [ \$? -eq 0 ]; then sudo sed -i "s/enabled=1/enabled=0/g" /etc/default/apport sudo systemctl stop apport sudo systemctl --now disable apport sudo systemctl daemon-reload fi echo "Authorized uses only." | sudo tee /etc/issue > /dev/null 2>&1 echo "Authorized uses only." | sudo tee /etc/issue.net > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y telnet > /dev/null 2>&1 # Assume the caller will restart sshd - don't want to risk breaking the installation process sudo sed -i "s/^#MaxAuthTries.*/MaxAuthTries 4/" /etc/ssh/sshd_config sudo sed -i "s/^#Banner none.*/Banner \/etc\/issue.net/" /etc/ssh/sshd_config sudo sed -i "s/^#MaxStartups.*/MaxStartups 10:30:60/" /etc/ssh/sshd_config sudo sed -i "s/^#LoginGraceTime.*/LoginGraceTime 60/" /etc/ssh/sshd_config sudo sed -i "s/^#AllowTcpForwarding yes.*/AllowTcpForwarding no/" /etc/ssh/sshd_config sudo chown root:root /etc/crontab sudo chmod og-rwx /etc/crontab sudo chown root:root /etc/cron.hourly/ sudo chmod og-rwx /etc/cron.hourly/ sudo chown root:root /etc/cron.daily/ sudo chmod og-rwx /etc/cron.daily/ sudo chown root:root /etc/cron.weekly/ sudo chmod og-rwx /etc/cron.weekly/ sudo chown root:root /etc/cron.monthly/ sudo chmod og-rwx /etc/cron.monthly/ sudo chown root:root /etc/cron.d/ sudo chmod og-rwx /etc/cron.d/ if ! [ -z \$disa_curE ]; then set -e fi } function performAptInstall() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' \$1 } function installDependencies() { UTILS_LIST="$RELAYSERVER_UTILS_LIST" APT_REMOVE_LIST="$APT_REMOVE_LIST" echo "Updating host..." sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1 echo "Host update complete!" performAptInstall ssmtp > /dev/null 2>&1 sudo tee /etc/ssmtp/ssmtp.conf >/dev/null </dev/null < /dev/null 2>&1 getent group mailsenders >/dev/null || sudo groupadd mailsenders sudo usermod -aG mailsenders \$USERNAME sudo chown root:mailsenders /usr/bin/mail.mailutils sudo chmod 750 /usr/bin/mail.mailutils set +e # Install Rsyslog sudo systemctl status rsyslog > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Installing rsyslog, please wait..." performAptInstall rsyslog > /dev/null 2>&1 sudo systemctl enable rsyslog sudo systemctl start rsyslog fi for util in \$UTILS_LIST; do if [[ "\$(isProgramInstalled \$util)" = "false" ]]; then lib_name=\$(echo \$util | cut -d"|" -f2) echo "Installing \$lib_name, please wait..." performAptInstall \$lib_name > /dev/null 2>&1 fi done updateSysctl updateMOTD performSuggestedSecUpdates installDocker if sudo test -f /etc/logrotate.d/rsyslog; then # Set max size of syslog to 2G grep maxsize /etc/logrotate.d/rsyslog > /dev/null 2>&1 if [ \$? -ne 0 ]; then sudo sed -i "/weekly\$/a\ maxsize 2G" /etc/logrotate.d/rsyslog fi fi # Move logrotate into hourly if sudo test -f /etc/cron.daily/logrotate; then sudo mv /etc/cron.daily/logrotate /etc/cron.hourly fi # Set conditions for docker container logs sudo tee /etc/logrotate.d/docker >/dev/null < /dev/null | grep "^VERSION_CODENAME=" | cut -d"=" -f2 | xargs) } function checkSupportedHostOS() { if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^22\.04. ]]; then return fi if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^24\.04. ]]; then return fi if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^26\.04. ]]; then return fi if [ "\$DISTRO_ID" = "debian" ] && [[ "\$DISTRO_VERSION" =~ ^12. ]]; then return fi if [ "\$DISTRO_ID" = "linuxmint" ] && [[ "\$DISTRO_VERSION" =~ ^22. ]]; then return fi echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" echo "@ Unsupported Host Operating System @" echo "@ @" echo "@ This installation only supports the following Linux distribution(s): @" echo "@ @" echo "@ - Ubuntu 22.04 (Jammy Jellyfish) @" echo "@ - Ubuntu 24.04 (Noble Numbat) @" echo "@ - Debian 12 (Bookworm) @" echo "@ - Mint 22 (Wilma) @" echo "@ @" echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" return 2 } function outputDockerSettings() { sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json >/dev/null < /dev/null 2>&1 if [ \$? -ne 0 ]; then sudo tee /etc/rsyslog.d/docker-logs.conf >/dev/null < /dev/null 2>&1 performAptInstall ca-certificates > /dev/null 2>&1 performAptInstall curl > /dev/null 2>&1 performAptInstall gnupg > /dev/null 2>&1 performAptInstall gnupg2 > /dev/null 2>&1 performAptInstall software-properties-common > /dev/null 2>&1 performAptInstall lsb-release > /dev/null 2>&1 case "\$DISTRO_ID" in "ubuntu") if [[ "\$DISTRO_VERSION" =~ ^22\.04. ]]; then installDockerUbuntu2204 elif [[ "\$DISTRO_VERSION" =~ ^24\.04. ]]; then installDockerUbuntu2404 elif [[ "\$DISTRO_VERSION" =~ ^26\.04. ]]; then installDockerUbuntu2604 else echo "Linux version not found when installing Docker, exiting..." exit 5 fi ;; "debian") if [[ "\$DISTRO_VERSION" =~ ^12. ]]; then installDockerDebian12 else echo "Linux version not found when installing Docker, exiting..." exit 5 fi ;; "linuxmint") if [[ "\$DISTRO_VERSION" =~ ^22. ]]; then installDockerUbuntu2404 elif [[ "\$DISTRO_VERSION" =~ ^6. ]]; then installDockerDebian12 else echo "Linux version not found when installing Docker, exiting..." exit 5 fi ;; *) ;; esac # See https://www.portainer.io/blog/portainer-and-docker-26 sudo apt-mark hold docker-ce sudo apt-mark hold docker-ce-cli sudo systemctl restart rsyslog sudo systemctl restart docker sudo usermod -aG docker \$USERNAME } function installDockerUbuntu2204() { # Install Docker (https://docs.docker.com/engine/install/ubuntu/) curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg --yes echo "deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2204 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2204 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 } function installDockerUbuntu2404() { # Install Docker (https://docs.docker.com/engine/install/ubuntu/) sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \$(. /etc/os-release && echo "\$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2404 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2404 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 } function installDockerUbuntu2604() { # Install Docker (https://docs.docker.com/engine/install/ubuntu/) sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \$(. /etc/os-release && echo "\$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2604 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2604 containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null 2>&1 } function installDockerDebian12() { # Install Docker (https://docs.docker.com/engine/install/debian/) sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "\$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_DEBIAN_12 docker-ce-cli=$DOCKER_VERSION_DEBIAN_12 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 } function outputNukeScript() { cat < \$HOME/$NUKE_SCRIPT_NAME HSHQ_BASE_DIR=\$RELAYSERVER_HSHQ_BASE_DIR HSHQ_SCRIPTS_DIR=\$RELAYSERVER_HSHQ_SCRIPTS_DIR set +e function removeWGInterfaceQuick() { config_filename=\\\$1 sudo systemctl stop wg-quick@\\\${config_filename}.service sudo systemctl disable wg-quick@\\\${config_filename}.service sudo systemctl daemon-reload sudo rm -f /etc/wireguard/\\\${config_filename}.conf } function main() { echo echo echo echo echo "============================================================" echo "This script will entirely remove HSHQ and all artifacts." echo "It will permanently delete ALL data" echo "It will not delete any previously downloaded docker images." echo "In order to remove them as well, type 'docker image prune -f'" echo "Please type 'nuclear' in order to continue." echo "============================================================" echo echo read -r -p "Type 'nuclear' (no quotes): " isnuke if ! [ "\\\$isnuke" = "nuclear" ]; then echo "String does not match, exiting..." exit 1 fi sudo -k sudo crontab -r sudo docker ps -q | xargs sudo docker stop sleep 5 sudo docker container prune -f sudo docker volume rm \\\$(sudo docker volume ls -q) sudo docker network prune -f sudo ls /etc/wireguard | while read fname do bname=\\\$(basename \\\$fname .conf) removeWGInterfaceQuick \\\$bname done sudo run-parts --regex '.*sh\\\$' \\\$HSHQ_SCRIPTS_DIR/root/portforwarding sudo systemctl stop runOnBootRoot sudo systemctl disable runOnBootRoot sudo rm -f /etc/systemd/system/runOnBootRoot.service sudo \\\$HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh sudo rm -f /etc/sysctl.d/88-hshq.conf sudo sysctl --system > /dev/null 2>&1 sudo rm -f /etc/resolv.conf > /dev/null 2>&1 # Check if systemd-resolved is installed. # If not, then modify /etc/resolv.conf directly. if [ -f /etc/systemd/resolved.conf ]; then sudo systemctl unmask systemd-resolved > /dev/null 2>&1 sudo systemctl enable systemd-resolved > /dev/null 2>&1 sudo systemctl start systemd-resolved > /dev/null 2>&1 sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf sudo grep -q "^nameserver 9.9.9.9" /etc/resolv.conf > /dev/null 2>&1 if [ \\\$? -ne 0 ]; then sudo tee /etc/systemd/resolved.conf >/dev/null < /dev/null 2>&1 fi else sudo tee /etc/resolv.conf >/dev/null < $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_TRANSFER_SCRIPT_NAME #!/bin/bash set +e TZ=$TZ USERNAME=\$(id -u -n) USERID=\$(id -u) GROUPID=\$(id -g) cd ~ RELAYSERVER_HSHQ_BASE_DIR=\$HOME/hshq RELAYSERVER_HSHQ_DATA_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/data RELAYSERVER_HSHQ_NONBACKUP_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/nonbackup RELAYSERVER_HSHQ_SCRIPTS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/scripts RELAYSERVER_HSHQ_SECRETS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/secrets RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/stacks RELAYSERVER_HSHQ_SSL_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/ssl function main() { if [ \$USERID = "0" ]; then echo "This script should be run as a non-root user. Exiting..." exit 1 fi read -r -s -t 30 -p "[sudo] password for \$USERNAME: " USER_RELAY_SUDO_PW echo "\$USER_RELAY_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Error with RelayServer sudo password, exiting..." exit 8 fi while getopts ':is' opt; do case "\$opt" in s) startTransfer ;; i) initTransfer ;; ?|h) echo "Usage: \$(basename \$0)" exit 1 ;; esac done shift "\$((\$OPTIND -1))" } function initTransfer() { sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo -e "\n\nInstalling a few utilities..." performAptInstall curl > /dev/null 2>&1 performAptInstall dnsutils > /dev/null 2>&1 performAptInstall screen > /dev/null 2>&1 bash \$HOME/$RS_INSTALL_SETUP_SCRIPT_NAME mkdir -p \$RELAYSERVER_HSHQ_BASE_DIR sudo tar xvzf \$HOME/rsbackup.tar.gz >/dev/null sudo rm -fr \$RELAYSERVER_HSHQ_DATA_DIR sudo mv \$HOME/backup \$RELAYSERVER_HSHQ_DATA_DIR sudo sed -i "s/SSH_PORT=.*/SSH_PORT=$RELAYSERVER_SSH_PORT/" \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh restoreNonBackupDir restoreSSL pullDockerImages restoreScripts sudo systemctl restart ssh > /dev/null 2>&1 sudo systemctl restart sshd > /dev/null 2>&1 } function startTransfer() { restorePortainer restoreAdguard restoreMailRelay restoreWireGuard restoreCaddy restoreOfelia restoreSyncthing startWazuhAgent sudo rm -f \$HOME/rsbackup.tar.gz sudo rm -f \$HOME/$RS_INSTALL_SETUP_SCRIPT_NAME sudo rm -f \$HOME/$RS_INSTALL_TRANSFER_SCRIPT_NAME } function getIPFromHostname() { echo \$(dig \$1 +short | grep '^[.0-9]*\$') } function performAptInstall() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' \$1 } function restoreNonBackupDir() { mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/adguard mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/adguard/work mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/caddy mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/caddy/config mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/caddy/data mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/syncthing mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/syncthing/data } function restoreSSL() { docker volume create --driver local -o device=\$RELAYSERVER_HSHQ_SSL_DIR/caddy -o o=bind -o type=none caddy-certs sudo cp \$RELAYSERVER_HSHQ_DATA_DIR/ssl/${CERTS_ROOT_CA_NAME}.crt /usr/local/share/ca-certificates/ sudo cp \$RELAYSERVER_HSHQ_DATA_DIR/ssl/relayserver-ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates } function pullImage() { img_and_version=\$1 echo "Pulling Image: \$img_and_version" is_success=1 num_tries=1 set +e while [ \$is_success -ne 0 ] && [ \$num_tries -lt $MAX_DOCKER_PULL_TRIES ] do # Refresh the sudo timestamp sudo -v docker pull \$img_and_version docker image inspect \$img_and_version > /dev/null 2>&1 is_success=\$? ((num_tries++)) if [ \$is_success -ne 0 ]; then echo "Error pulling image, retrying in 15 seconds..." sleep 15 fi done if [ \$is_success -ne 0 ]; then echo "Error pulling docker image: \$img_and_version" return 5 fi } function pullDockerImages() { portainerImage=\$(sudo grep -r "image: " \$RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml | xargs | cut -d" " -f2) pullImage "\$portainerImage" if [ \$? -ne 0 ]; then echo "There was a problem pulling the portainer image, exiting..." exit 5 fi OLDIFS=\$IFS IFS=\$(echo -en "\n\b") img_arr=(\$(sudo grep -r "image: " \$RELAYSERVER_HSHQ_STACKS_DIR/portainer/compose)) for cur_item in "\${img_arr[@]}" do cur_img=\$(echo "\$cur_item" | xargs | cut -d" " -f3) if [[ "\$cur_img" =~ ^hshq ]]; then continue; fi pullImage "\$cur_img" done IFS=\$OLDIFS mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR/build } function restoreScripts() { sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh sudo rm -f /etc/systemd/system/runOnBootRoot.service sudo ln -s \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/runOnBootRoot.service /etc/systemd/system/runOnBootRoot.service sudo systemctl daemon-reload sudo systemctl enable runOnBootRoot } function getPortainerToken() { while [ -n "\$1" ] do case "\$1" in -u) port_username="\$2";; -p) port_password="\$2" shift ;; --) shift break ;; esac shift done if [ -z "\$port_username" ]; then read -r -p "Enter the portainer username: " port_username echo fi if [ -z "\$port_password" ]; then read -r -s -p "Enter the portainer password: " port_password echo fi echo \$(http --check-status --ignore-stdin --verify=no https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/auth username=\$port_username password=\$port_password | jq -r .jwt) } function getStackID() { stackID="NA" stackName=\$1 qry=\$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":1} "Authorization: Bearer \$RELAYSERVER_PORTAINER_TOKEN") for row in \$(echo "\${qry}" | jq -r '.[] | @base64'); do _jq() { echo \${row} | base64 --decode | jq -r \${1} } if [ "\$(_jq '.Name')" = "\$stackName" ]; then stackID=\$(_jq '.Id') break fi done if ! [ "\$stackID" = "NA" ]; then echo \$stackID fi } function startStopStack() { stackName=\$1 startStop=\$2 stackID=\$(getStackID \$stackName) http --check-status --ignore-stdin --verify=no --timeout=300 POST https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks/\$stackID/\$startStop endpointId==1 "Authorization: Bearer \$RELAYSERVER_PORTAINER_TOKEN" > /dev/null } function restorePortainer() { sed -i "s|^PORTAINER_UID=.*|PORTAINER_UID=\${USERID}|g" $RELAYSERVER_HSHQ_STACKS_DIR/portainer/portainer.env sed -i "s|^PORTAINER_GID=.*|PORTAINER_GID=\${GROUPID}|g" $RELAYSERVER_HSHQ_STACKS_DIR/portainer/portainer.env cd ~ docker compose -f \$RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml up -d RELAYSERVER_PORTAINER_TOKEN="\$(getPortainerToken -u $RELAYSERVER_PORTAINER_ADMIN_USERNAME -p $RELAYSERVER_PORTAINER_ADMIN_PASSWORD)" if [ \$? -ne 0 ] || [ -z "\$RELAYSERVER_PORTAINER_TOKEN" ]; then echo "There was a problem obtaining the portainer token, exiting..." exit 6 fi } function restoreAdguard() { sudo systemctl stop systemd-resolved > /dev/null 2>&1 sudo systemctl disable systemd-resolved > /dev/null 2>&1 sudo systemctl mask systemd-resolved > /dev/null 2>&1 sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 sudo sed -i "/$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/{n;s/.*/ answer: $RELAYSERVER_SERVER_IP/}" \$RELAYSERVER_HSHQ_STACKS_DIR/adguard/conf/AdGuardHome.yaml sudo chown -R \${USERID}:\${GROUPID} \$RELAYSERVER_HSHQ_STACKS_DIR/adguard/conf sudo chown -R \${USERID}:\${GROUPID} \$RELAYSERVER_HSHQ_NONBACKUP_DIR/adguard/work startStopStack adguard stop startStopStack adguard start } function restoreMailRelay() { startStopStack mail-relay stop startStopStack mail-relay start } function restoreWireGuard() { sudo mv \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/$RELAYSERVER_WG_INTERFACE_NAME.conf /etc/wireguard/$RELAYSERVER_WG_INTERFACE_NAME.conf # Gotta make it a hard link just as it was before, dummy. sudo ln /etc/wireguard/$RELAYSERVER_WG_INTERFACE_NAME.conf \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/$RELAYSERVER_WG_INTERFACE_NAME.conf sudo systemctl enable wg-quick@$RELAYSERVER_WG_INTERFACE_NAME.service sudo systemctl start wg-quick@$RELAYSERVER_WG_INTERFACE_NAME.service startStopStack wgportal stop startStopStack wgportal start startStopStack clientdns stop startStopStack clientdns start # Let's restart the interface just to make sure timeout 10 sudo systemctl restart wg-quick@$RELAYSERVER_WG_INTERFACE_NAME.service } function restoreCaddy() { startStopStack caddy stop startStopStack caddy start } function restoreOfelia() { startStopStack ofelia stop startStopStack ofelia start } function restoreSyncthing() { startStopStack syncthing stop startStopStack syncthing start } function startWazuhAgent() { curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | sudo gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import && sudo chmod 644 /usr/share/keyrings/wazuh.gpg echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" | sudo tee /etc/apt/sources.list.d/wazuh.list sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo WAZUH_MANAGER="$SUB_WAZUH.$HOMESERVER_DOMAIN" DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' wazuh-agent=$WAZUH_AGENT_VERSION sudo apt-mark hold wazuh-agent sudo systemctl daemon-reload set +e sudo grep "/var/log/docker/" /var/ossec/etc/ossec.conf > /dev/null 2>&1 if [ \$? -ne 0 ]; then sudo sed -i "/<\/ossec_config>/{s// \n syslog<\/log_format>\n \/var\/log\/docker\/*<\/location>\n <\/localfile>\n\n<\/ossec_config>/;:p;n;bp}" /var/ossec/etc/ossec.conf fi sudo tee /var/ossec/etc/authd.pass >/dev/null < $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME #!/bin/bash set -e TZ=$TZ USERNAME=\$(id -u -n) USERID=\$(id -u) GROUPID=\$(id -g) if [ \$USERID = "0" ]; then echo "This script should be run as a non-root user. Exiting..." exit 1 fi if [ -d /tmp/hshqopen ]; then echo "Installation already in progess, exiting..." exit 2 fi cd ~ RELAYSERVER_HSHQ_BASE_DIR=\$HOME/hshq RELAYSERVER_HSHQ_DATA_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/data RELAYSERVER_HSHQ_NONBACKUP_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/nonbackup RELAYSERVER_HSHQ_SCRIPTS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/scripts RELAYSERVER_HSHQ_SECRETS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/secrets RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/stacks RELAYSERVER_HSHQ_SSL_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/ssl NET_EXTERNAL_SUBNET=$RELAYSERVER_NET_EXTERNAL_SUBNET NET_EXTERNAL_SUBNET_PREFIX=$RELAYSERVER_NET_EXTERNAL_SUBNET_PREFIX NET_WEBPROXY_SUBNET=$RELAYSERVER_NET_WEBPROXY_SUBNET NET_WEBPROXY_SUBNET_PREFIX=$RELAYSERVER_NET_WEBPROXY_SUBNET_PREFIX MAIL_RELAY_SUBNET=$RELAYSERVER_MAIL_RELAY_SUBNET MAIL_RELAY_SUBNET_PREFIX=$RELAYSERVER_MAIL_RELAY_SUBNET_PREFIX CLIENT_DNS_SUBNET=$RELAYSERVER_CLIENT_DNS_SUBNET CLIENT_DNS_SUBNET_PREFIX=$RELAYSERVER_CLIENT_DNS_SUBNET_PREFIX function main() { while getopts ':ips' opt; do case "\$opt" in s) startInstall ;; p) getSuper ;; i) performInstall ;; ?|h) echo "Usage: \$(basename \$0)" exit 1 ;; esac done shift "\$((\$OPTIND -1))" } function startInstall() { set +e read -r -s -t 30 -p "[sudo] password for \$USERNAME: " USER_RELAY_SUDO_PW echo "\$USER_RELAY_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Error with RelayServer sudo password, exiting..." exit 8 fi loadVersionVars echo "Update apt..." sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo dpkg --configure -a > /dev/null 2>&1 echo "Checking for required utils..." set +e which curl > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Installing curl..." performAptInstall curl > /dev/null 2>&1 fi which dig > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Installing dnsutils..." performAptInstall dnsutils > /dev/null 2>&1 fi which screen > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Installing screen..." performAptInstall screen > /dev/null 2>&1 fi which pwgen > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Installing pwgen..." performAptInstall pwgen > /dev/null 2>&1 fi set -e mkdir -p \$RELAYSERVER_HSHQ_BASE_DIR installLogNotify "Start Installation" scName=hshqInstall initScreen "\$scName" screen -S "\$scName" -X stuff "bash \$0 -i\n" sleep 10 } function getSuper() { set +e stty -echo mkdir -p $TMP_PW_CHECKDIR read -r -s -p "" sudoPWKey USER_RELAY_SUDO_PW="\$(openssl enc -d -aes256 -pbkdf2 -in $ENC_PW_FILE -pass pass:\$sudoPWKey)" rm -f $ENC_PW_FILE echo "\$USER_RELAY_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1 if [ \$? -ne 0 ]; then installLogNotify "Error with sudo password, exiting installation..." exit 6 fi rm -fr $TMP_PW_CHECKDIR clear stty echo } function initScreen() { set +e screenName="\$1" dirtest1=/tmp/sctest1 isScreenSuccess=false curScreenAttempt=0 maxScreenAttempt=5 screen -XS \$screenName quit > /dev/null 2>&1 # The screen utility has a problem with stuffing # special characters in strings, specifically # in this case when trying to inject the sudo # password. Thus, the easiest way to handle this # is to create a temporary "safe" key, output # the encrypted password to file using the key # and transmit the key to the screen session. sudoPWKey=\$(pwgen -c -n 32 1) echo -n "\$USER_RELAY_SUDO_PW" | openssl enc -e -aes256 -pbkdf2 -pass pass:\$sudoPWKey > $ENC_PW_FILE while [ \$curScreenAttempt -lt \$maxScreenAttempt ] do ((curScreenAttempt++)) rm -fr \$dirtest1 rm -fr $TMP_PW_CHECKDIR screen -L -Logfile \$RELAYSERVER_HSHQ_BASE_DIR/$RELAYSERVER_HSHQ_FULL_LOG_NAME -dmS \$screenName screen -S \$screenName -X stuff "stty -echo;mkdir \$dirtest1\n" curCheckCount=0 while ! [ -d \$dirtest1 ] && [ \$curCheckCount -lt 5 ] do ((curCheckCount++)) sleep 1 done if ! [ -d \$dirtest1 ]; then screen -XS \$screenName quit > /dev/null 2>&1 continue fi screen -S \$screenName -X stuff "bash \$0 -p\n" curCheckCount=0 while ! [ -d $TMP_PW_CHECKDIR ] && [ \$curCheckCount -lt 15 ] do ((curCheckCount++)) sleep 1 done screen -S \$screenName -X stuff "\$sudoPWKey\n" curCheckCount=0 while [ -d $TMP_PW_CHECKDIR ] && [ \$curCheckCount -lt 15 ] do ((curCheckCount++)) sleep 1 done rm -fr \$dirtest1 if [ -d $TMP_PW_CHECKDIR ]; then rm -fr $TMP_PW_CHECKDIR echo "The sudo password was not correctly transmitted to the screen session, exiting..." exit 5 fi rm -fr $TMP_PW_CHECKDIR isScreenSuccess=true break done USER_RELAY_SUDO_PW="" if ! [ "\$isScreenSuccess" = "true" ]; then return 1 fi } function performAptInstall() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' \$1 } function isProgramInstalled() { bin_name=\$(echo \$1 | cut -d"|" -f1) lib_name=\$(echo \$1 | cut -d"|" -f2) if [ -z "\$(sudo which \${bin_name})" ]; then echo "false" else echo "true" fi } function installLogNotify() { log_message=\$1 echo \$(date)" - \$log_message" >> \$RELAYSERVER_HSHQ_BASE_DIR/$RELAYSERVER_HSHQ_TIMESTAMP_LOG_NAME } function getIPFromHostname() { echo \$(dig \$1 +short | grep '^[.0-9]*\$') } function getConnectingIPAddress() { cIP=\$(echo \$SSH_CLIENT | xargs | cut -d" " -f1) if [ "\$(checkValidIPAddress \$cIP)" = "true" ]; then echo "\$cIP" else echo "" fi } function checkIsIPPrivate() { check_ip="\$1" set +e priv_arr=(10.0.0.0/8 172.16.0.0/12 192.168.0.0/16) for subnet in "\${priv_arr[@]}" do is_in_subnet=\$(isIPInSubnet \$check_ip \$subnet) if [ "\$is_in_subnet" = "true" ]; then echo "true" return fi done echo "false" } function isIPInSubnet() { iiis_curE=\${-//[^e]/} check_ipaddr=\$1 check_subnet=\$2 set +e grepcidr \${check_subnet} <(echo \${check_ipaddr}) > /dev/null 2>&1 if [ \$? -eq 0 ]; then echo "true" else echo "false" fi set +e if ! [ -z \$iiis_curE ]; then set -e fi } function checkValidIPAddress() { ip=\$(echo \$1 | cut -d "/" -f1) stat=1 if [[ \$ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\$ ]]; then OLDIFS=\$IFS IFS='./' ip=(\$ip) IFS=\$OLDIFS [[ \${ip[0]} -le 255 && \${ip[1]} -le 255 && \${ip[2]} -le 255 && \${ip[3]} -le 255 ]] stat=\$? fi if [ \$stat -eq 0 ]; then echo "true" else echo "false" fi } function loadVersionVars() { set +e DISTRO_ID=\$(cat /etc/*-release | grep "^ID=" | cut -d"=" -f2 | xargs | tr '[:upper:]' '[:lower:]') DISTRO_NAME=\$(cat /etc/*-release | grep "^NAME=" | cut -d"=" -f2 | xargs) DISTRO_VERSION=\$(cat /etc/*-release | grep "^VERSION=" | cut -d"=" -f2 | xargs) DISTRO_CODENAME=\$(cat /etc/*-release 2> /dev/null | grep "^VERSION_CODENAME=" | cut -d"=" -f2 | xargs) } function performInstall() { mkdir /tmp/hshqopen sudo -v bash \$HOME/$RS_INSTALL_SETUP_SCRIPT_NAME docker ps > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Docker was not install properly, exiting..." touch ~/$RELAYSERVER_INSTALL_FAILED_FILE exit 6 fi set -e mkdir -p \$RELAYSERVER_HSHQ_DATA_DIR mkdir -p \$RELAYSERVER_HSHQ_NONBACKUP_DIR mkdir -p \$RELAYSERVER_HSHQ_SCRIPTS_DIR mkdir -p \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot mkdir -p \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts mkdir -p \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user sudo mkdir -p \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root sudo chmod 700 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root sudo mkdir -p \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot sudo chmod 700 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot sudo mkdir -p \$RELAYSERVER_HSHQ_SECRETS_DIR mkdir -p \$RELAYSERVER_HSHQ_SSL_DIR mkdir -p \$RELAYSERVER_HSHQ_STACKS_DIR outputCerts pullDockerImages outputScripts installLogNotify "Installing Stacks" installPortainer installMailRelay installWireGuard installFileBrowser installCaddy installOfelia installSyncthing installLogNotify "Starting Wazuh Agent" startWazuhAgent installAdGuard sudo rm -f \$HOME/$RS_INSTALL_FRESH_SCRIPT_NAME sudo rm -f \$HOME/$RS_INSTALL_SETUP_SCRIPT_NAME docker image prune -af installLogNotify "Rebooting" echo -e "\n\n\n########################################\n" echo " RelayServer Installation Complete!" echo " Restarting server..." echo -e "\n########################################\n\n" rm -fr /tmp/hshqopen # Successfull installation, clean up the logs #rm -f \$RELAYSERVER_HSHQ_BASE_DIR/$RELAYSERVER_HSHQ_FULL_LOG_NAME #rm -f \$RELAYSERVER_HSHQ_BASE_DIR/$RELAYSERVER_HSHQ_TIMESTAMP_LOG_NAME touch \$RELAYSERVER_HSHQ_BASE_DIR/$RELAYSERVER_INSTALL_COMPLETE_FILE sudo reboot } function outputCerts() { mkdir \$RELAYSERVER_HSHQ_SSL_DIR/caddy docker volume create --driver local -o device=\$RELAYSERVER_HSHQ_SSL_DIR/caddy -o o=bind -o type=none caddy-certs echo """$(cat $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt)""" > \$RELAYSERVER_HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt openssl x509 -in \$RELAYSERVER_HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -out \$RELAYSERVER_HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der -outform DER echo "Generating DH parameters, 512 bit long safe prime..." openssl dhparam -out \$RELAYSERVER_HSHQ_SSL_DIR/dhparam.pem $CERTS_INTERNAL_DHPARAMS_KEYLENGTH > /dev/null 2>&1 chmod 0444 \$RELAYSERVER_HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt chmod 0444 \$RELAYSERVER_HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der chmod 0444 \$RELAYSERVER_HSHQ_SSL_DIR/dhparam.pem sudo cp \$RELAYSERVER_HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt initCertificateAuthority sudo update-ca-certificates } function initCertificateAuthority() { # Certificate Authority Key openssl genrsa -out \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.key 4096 cat < \$HOME/ca.conf [ req ] distinguished_name = req_distinguished_name prompt = no x509_extensions = v3_ca [ req_distinguished_name ] C = $CERTS_INTERNAL_COUNTRY ST = $CERTS_INTERNAL_STATE L = $CERTS_INTERNAL_LOCALITY O = $HOMESERVER_NAME RelayServer OU = $CERTS_INTERNAL_OU_NAME CN = $CERTS_INTERNAL_ROOT_CN [ v3_ca ] basicConstraints = critical,CA:TRUE subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer keyUsage = critical, cRLSign, keyCertSign EOFCA # Certificate Authority Certificate openssl req -new -x509 -sha256 -nodes -days $CERTS_INTERNAL_CA_DAYS -key \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.key -out \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.crt -config \$HOME/ca.conf rm -f \$HOME/ca.conf chmod 0400 \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.key chmod 0444 \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.crt sudo cp \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.crt /usr/local/share/ca-certificates/ } function generateCert() { CERT_NAME=\$1 DNS=\$2 IP_ADDRESSES=\$3 rm -fr \$RELAYSERVER_HSHQ_SSL_DIR/\$CERT_NAME.key rm -fr \$RELAYSERVER_HSHQ_SSL_DIR/\$CERT_NAME.crt CERTS_INTERNAL_COUNTRY="$CERTS_INTERNAL_COUNTRY" CERTS_INTERNAL_STATE="$CERTS_INTERNAL_STATE" CERTS_INTERNAL_LOCALITY="$CERTS_INTERNAL_LOCALITY" CERTS_INTERNAL_OU_NAME="$CERTS_INTERNAL_OU_NAME" ORGANIZATION_NAME="$HOMESERVER_NAME" CERTS_INTERNAL_CA_DAYS=$CERTS_INTERNAL_CA_DAYS # Generate certificate key openssl genrsa -out \$RELAYSERVER_HSHQ_SSL_DIR/\$CERT_NAME.key 4096 cat < \$HOME/\$CERT_NAME-csr.cnf [ req ] distinguished_name = req_distinguished_name prompt = no req_extensions = req_ext [ req_distinguished_name ] C = \$CERTS_INTERNAL_COUNTRY ST = \$CERTS_INTERNAL_STATE L = \$CERTS_INTERNAL_LOCALITY O = \$ORGANIZATION_NAME RelayServer OU = \$CERTS_INTERNAL_OU_NAME CN = \$CERT_NAME [ req_ext ] subjectAltName = @alt_names [ alt_names ] EOFCR cat < \$HOME/\$CERT_NAME-ext.cnf basicConstraints = CA:FALSE nsComment = "OpenSSL Certificate Generated By \$ORGANIZATION_NAME" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth,clientAuth subjectAltName = @alt_names [ alt_names ] EOFCR dnslist=\$(sed 's|,|\n|g' <<< \$DNS) ipnum=1 for dn in \$dnslist do echo "DNS."\$ipnum" = "\$dn >> \$HOME/\$CERT_NAME-csr.cnf echo "DNS."\$ipnum" = "\$dn >> \$HOME/\$CERT_NAME-ext.cnf ((ipnum++)) done iplist=\$(sed 's|,|\n|g' <<< \$IP_ADDRESSES) ipnum=1 for ip in \$iplist do echo "IP."\$ipnum" = "\$ip >> \$HOME/\$CERT_NAME-csr.cnf echo "IP."\$ipnum" = "\$ip >> \$HOME/\$CERT_NAME-ext.cnf ((ipnum++)) done # Generate certificate request openssl req -config \$HOME/\$CERT_NAME-csr.cnf -new -sha256 -key \$RELAYSERVER_HSHQ_SSL_DIR/\$CERT_NAME.key -out \$HOME/\$CERT_NAME.csr openssl x509 -req -sha256 -days \$CERTS_INTERNAL_CA_DAYS -in \$HOME/\$CERT_NAME.csr -CA \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.crt -CAkey \$RELAYSERVER_HSHQ_SSL_DIR/relayserver-ca.key -out \$RELAYSERVER_HSHQ_SSL_DIR/\$CERT_NAME.crt -extfile \$HOME/\$CERT_NAME-ext.cnf -CAcreateserial rm -f \$HOME/\$CERT_NAME-csr.cnf rm -f \$HOME/\$CERT_NAME-ext.cnf rm -f \$HOME/\$CERT_NAME.csr chmod 0444 \$RELAYSERVER_HSHQ_SSL_DIR/\$CERT_NAME.key chmod 0444 \$RELAYSERVER_HSHQ_SSL_DIR/\$CERT_NAME.crt } function pullImage() { img_and_version=\$1 echo "Pulling Image: \$img_and_version" is_success=1 num_tries=1 set +e while [ \$is_success -ne 0 ] && [ \$num_tries -lt $MAX_DOCKER_PULL_TRIES ] do # Refresh the sudo timestamp sudo -v docker pull \$img_and_version docker image inspect \$img_and_version > /dev/null 2>&1 is_success=\$? ((num_tries++)) if [ \$is_success -ne 0 ]; then echo "Error pulling image, retrying in 15 seconds..." sleep 15 fi done set -e if [ \$is_success -ne 0 ]; then echo "Error pulling docker image: $img_and_version" exit 5 fi set -e } function pullDockerImages() { pullImage $IMG_ADGUARD pullImage $IMG_CADDY pullImage $IMG_DNSMASQ pullImage $IMG_MAIL_RELAY_UNBOUND pullImage $IMG_FILEBROWSER pullImage $IMG_OFELIA pullImage $IMG_PORTAINER pullImage $IMG_SYNCTHING pullImage $IMG_REDIS pullImage $IMG_WGPORTAL pullImage $IMG_WIREGUARD pullImage $IMG_MAIL_RELAY_POSTFIX pullImage $IMG_MAIL_RELAY_RSPAMD } function outputScripts() { outputBootScripts outputCaddyScripts outputRelayedDomainsScript outputPortforwardingScripts } function outputBootScripts() { exposedPortsList=53,$MAILU_PORT_5,$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT,$RELAYSERVER_WG_PORTAL_PORT,$SYNCTHING_SYNC_PORT,$SYNCTHING_DISC_PORT sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/onBootRoot.sh >/dev/null </dev/null < /dev/null sudo iptables -t filter -X DOCKER-USER > /dev/null sudo iptables -t filter -N DOCKER-USER > /dev/null portsArr=(\\\$(echo \\\$ports_list | tr "," "\n")) for cur_port in "\\\${portsArr[@]}" do iptables -D DOCKER-USER -s 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j RETURN 2> /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j DROP 2> /dev/null iptables -I DOCKER-USER -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j DROP iptables -I DOCKER-USER -s 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j RETURN done # See https://gist.github.com/mattia-beta/bd5b1c68e3d51db933181d8a3dc0ba64?permalink_comment_id=3728715#gistcomment-3728715 iptables -t raw -F PREROUTING iptables -t raw -F chain-icmp > /dev/null 2>&1 iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 iptables -t raw -X chain-icmp > /dev/null 2>&1 iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 iptables -t raw -N chain-icmp > /dev/null 2>&1 iptables -t raw -N chain-bad_tcp > /dev/null 2>&1 iptables -t raw -A PREROUTING -s 192.0.0.0/24,192.0.2.0/24,198.51.100.0/24,203.0.113.0/24 -j DROP iptables -t raw -A PREROUTING -d 192.0.0.0/24,192.0.2.0/24,198.51.100.0/24,203.0.113.0/24 -j DROP iptables -t raw -A PREROUTING -d 0.0.0.0/8 -j DROP # Block spoofed packets iptables -t raw -A PREROUTING -s 0.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,224.0.0.0/4,198.18.0.0/15,100.64.0.0/10,192.88.99.0/24 -i \$default_iface -j DROP iptables -t raw -A PREROUTING -s 127.0.0.0/8 ! -i lo -j DROP iptables -t raw -A PREROUTING -p udp -m udp -m multiport --ports 0 -j DROP iptables -t raw -A PREROUTING -p icmp -j chain-icmp iptables -t raw -A PREROUTING -p tcp -m tcp -j chain-bad_tcp iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-reply -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type network-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type host-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type protocol-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type port-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type fragmentation-needed -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-request -i \$default_iface -j DROP iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-request -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type time-exceeded -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type parameter-problem -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type any -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN FIN,SYN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,RST SYN,RST -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,RST FIN,RST -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,ACK FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,URG URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,FIN FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,PSH PSH -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL ALL -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL NONE -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp -m tcp -m multiport --ports 0 -j DROP iptables -t raw -P PREROUTING ACCEPT # Configure loopback iptables -C INPUT -i lo -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -i lo -j ACCEPT iptables -C INPUT ! -i lo -s 127.0.0.0/8 -j DROP > /dev/null 2>&1 || iptables -A INPUT ! -i lo -s 127.0.0.0/8 -j DROP # Limit connections per source IP iptables -C INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset > /dev/null 2>&1 || iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset # Allow established connections iptables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Drop invalid packets iptables -C INPUT -m conntrack --ctstate INVALID -j DROP > /dev/null 2>&1 || iptables -A INPUT -m conntrack --ctstate INVALID -j DROP # Drop fragments iptables -C INPUT -f -j DROP > /dev/null 2>&1 || iptables -A INPUT -f -j DROP # Drop SYN packets with suspicious MSS value iptables -C INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP > /dev/null 2>&1 || iptables -A INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP # Allow ICMP iptables -C INPUT -p icmp -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p icmp -j ACCEPT # Allow SSH iptables -C INPUT -p tcp -m tcp --dport \\\$SSH_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p tcp -m tcp --dport \\\$SSH_PORT -j ACCEPT # Allow WireGuard iptables -C INPUT -p udp --dport \\\$WG_CON_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p udp --dport \\\$WG_CON_PORT -j ACCEPT # Special case for WG Portal from reverse proxy iptables -C INPUT -p tcp -m tcp -i brdockext -s \\\$DOCK_EXT_NET --dport \\\$WG_PORTAL_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p tcp -m tcp -i brdockext -s \\\$DOCK_EXT_NET --dport \\\$WG_PORTAL_PORT -j ACCEPT iptables -D DOCKER-USER -s 127.0.0.0/8 -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j RETURN 2> /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j DROP 2> /dev/null iptables -I DOCKER-USER -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j DROP iptables -I DOCKER-USER -s 127.0.0.0/8 -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j RETURN # Policy drop for input and forward iptables -P INPUT DROP iptables -P FORWARD DROP # Policy drop all ipv6 traffic ip6tables -P INPUT DROP ip6tables -P FORWARD DROP ip6tables -P OUTPUT DROP sysctl --system > /dev/null 2>&1 EOFBS # Special case - if RelayServer is behind a firewall and using a private IP for default route, # then allow traffic from the gateway, since raw table blocks all private ranges def_route_gate=\$(ip route | grep -e "^default" | awk '{print \$3}') is_def_priv=\$(checkIsIPPrivate \$def_route_gate) def_route_cidr_part=\$(ip route | grep \$(ip route | grep -e "^default" | head -n 1 | awk -F'dev ' '{print \$2}' | xargs | cut -d" " -f1) | grep / | xargs | cut -d" " -f1 | cut -d"/" -f2) if [ "\$is_def_priv" = "true" ]; then echo "Default route is in private range, adding allowance to (raw)iptables..." sudo sed -i "/ # Block spoofed packets.*/a\ iptables -t raw -C PREROUTING -s \$def_route_gate\/\$def_route_cidr_part -i \$default_iface -j ACCEPT > \/dev\/null 2>&1 || iptables -t raw -I PREROUTING -s \$def_route_gate\/\$def_route_cidr_part -i \$default_iface -j ACCEPT" \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh else echo "Default route is in public range." fi sudo chmod 744 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh >/dev/null < /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j DROP 2> /dev/null done iptables -D INPUT -f -j DROP > /dev/null 2>&1 iptables -D INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP > /dev/null 2>&1 iptables -D INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset > /dev/null 2>&1 iptables -D INPUT -m conntrack --ctstate INVALID -j DROP > /dev/null 2>&1 iptables -D INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -i lo -j ACCEPT > /dev/null 2>&1 iptables -D INPUT ! -i lo -s 127.0.0.0/8 -j DROP > /dev/null 2>&1 iptables -D INPUT -p icmp -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -p tcp -m tcp --dport \\\$SSH_PORT -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -p udp --dport \\\$WG_CON_PORT -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -p tcp -m tcp -i brdockext -s \\\$DOCK_EXT_NET --dport \\\$WG_PORTAL_PORT -j ACCEPT > /dev/null 2>&1 iptables -D DOCKER-USER -s 127.0.0.0/8 -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j ACCEPT 2> /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j DROP 2> /dev/null iptables -t raw -F PREROUTING iptables -t raw -F chain-icmp > /dev/null 2>&1 iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 iptables -t raw -X chain-icmp > /dev/null 2>&1 iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 iptables -P INPUT ACCEPT ip6tables -P INPUT ACCEPT ip6tables -P FORWARD ACCEPT ip6tables -P OUTPUT ACCEPT EOFBS sudo chmod 500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/runOnBootRoot.service >/dev/null </dev/null < \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addLECertDomains.sh #!/bin/bash subdomlist=\\\$1 RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_STACKS_DIR function main() { set +e leCertsArr=(\\\$(echo "\\\$subdomlist" | tr "," "\n")) for subdom in "\\\${leCertsArr[@]}" do if [ -z "\\\$subdom" ]; then continue; fi grep "# LE certs path \\\$subdom BEGIN" \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile >/dev/null if [ \\\$? -ne 0 ]; then strBlock="" strBlock=\\\$strBlock"# LE certs path \\\$subdom BEGIN\n" strBlock=\\\$strBlock"http://\\\$subdom {\n" strBlock=\\\$strBlock" handle /.well-known/acme-challenge/* {\n" strBlock=\\\$strBlock" reverse_proxy \\\$subdom {\n" strBlock=\\\$strBlock" import sn-resolver\n" strBlock=\\\$strBlock" }\n" strBlock=\\\$strBlock" }\n" strBlock=\\\$strBlock"}\n" strBlock=\\\$strBlock"# LE certs path \\\$subdom END\n" echo -e "\\\$strBlock" >> \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile fi done docker container restart caddy } main "\\\$@" EOFCD chmod 0500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addLECertDomains.sh cat < \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeLECertDomains.sh #!/bin/bash set +e RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_STACKS_DIR function main() { subdomlist="\\\$1" leCertsArr=(\\\$(echo "\\\$subdomlist" | tr "," "\n")) for subdom in "\\\${leCertsArr[@]}" do if [ -z "\\\$subdom" ]; then continue; fi sed -i "/# LE certs path \\\$subdom BEGIN/,/# LE certs path \\\$subdom END/d" \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile done cat -s \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile > \\\$HOME/tmpcadfile mv \\\$HOME/tmpcadfile \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile docker container restart caddy } main "\\\$@" EOFCD chmod 0500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeLECertDomains.sh cat < \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addExposeDomains.sh #!/bin/bash RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_STACKS_DIR function main() { subdomlist="\\\$1" exposeArr=(\\\$(echo "\\\$subdomlist" | tr "," "\n")) for subdom in "\\\${exposeArr[@]}" do if [ -z "\\\$subdom" ]; then continue; fi grep "# Expose domain \\\$subdom BEGIN" \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile >/dev/null if [ \\\$? -ne 0 ]; then strBlock="" strBlock=\\\$strBlock"# Expose domain \\\$subdom BEGIN\n" strBlock=\\\$strBlock"https://\\\$subdom {\n" strBlock=\\\$strBlock" import safe-header\n" strBlock=\\\$strBlock" reverse_proxy https://\\\$subdom {\n" strBlock=\\\$strBlock" import sn-resolver\n" strBlock=\\\$strBlock" }\n" strBlock=\\\$strBlock"}\n" strBlock=\\\$strBlock"# Expose domain \\\$subdom END\n" echo -e "\\\$strBlock" >> \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile fi done docker container restart caddy } main "\\\$@" EOFEX chmod 500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addExposeDomains.sh cat < \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeExposeDomains.sh #!/bin/bash set +e RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_STACKS_DIR function main() { subdomlist="\\\$1" exposeArr=(\\\$(echo "\\\$subdomlist" | tr "," "\n")) for subdom in "\\\${exposeArr[@]}" do if [ -z "\\\$subdom" ]; then continue; fi sed -i "/# Expose domain \\\$subdom BEGIN/,/# Expose domain \\\$subdom END/d" \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile done cat -s \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile > \\\$HOME/tmpcadfile mv \\\$HOME/tmpcadfile \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile docker container restart caddy } main "\\\$@" EOFEX chmod 500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeExposeDomains.sh } function outputRelayedDomainsScript() { cat < \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addRelayedDomains.sh #!/bin/bash set +e domains_to_add=\\\$1 deliver_to_host=\\\$2 deliver_to_ip=\\\$3 RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_STACKS_DIR function main() { addRelayedDomains } function addRelayedDomains() { if [ -z \\\$deliver_to_ip ]; then deliver_ip=\\\$(getIPFromHostname \\\$deliver_to_host) else deliver_ip=\\\$deliver_to_ip fi docker exec mail-relay-postfix /etc/postfix/scripts/addRelayedMailDomains.sh \\\$domains_to_add \\\$deliver_to_host # Add domains to caddy dnsmasq container domains_to_add_Arr=(\\\$(echo \\\$domains_to_add | tr "," "\n")) for add_domain in "\\\${domains_to_add_Arr[@]}" do sed -i "/.\\\$add_domain/d" \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/dns/dnsmasq.conf echo "address=/.\\\$add_domain/\\\$deliver_ip" >> \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/dns/dnsmasq.conf done docker container restart caddy-dns grep \\\${deliver_to_host} \\\$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/mailhosts.conf >/dev/null if [ \\\$? -ne 0 ]; then echo "local-data: \"\\\${deliver_to_host}. A \\\$deliver_ip\"" >> \\\$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/mailhosts.conf echo "local-data-ptr: \"\\\$deliver_ip \\\${deliver_to_host}.\"" >> \\\$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/mailhosts.conf fi docker container restart mail-relay-unbound } function getIPFromHostname() { echo \\\$(dig \\\$1 +short | grep '^[.0-9]*\\\$') } main "\\\$@" EOFRD chmod 0500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addRelayedDomains.sh cat < \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeRelayedDomains.sh #!/bin/bash set +e domains_to_remove=\\\$1 deliver_to_host=\\\$2 mail_user_domain=\\\$3 RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_STACKS_DIR function main() { removeRelayedDomains } function removeRelayedDomains() { docker exec mail-relay-postfix /etc/postfix/scripts/removeRelayedMailDomains.sh \\\$domains_to_remove \\\$mail_user_domain # Remove domains from caddy dnsmasq container domains_to_rem_Arr=(\\\$(echo \\\$domains_to_remove | tr "," "\n")) for rem_domain in "\\\${domains_to_rem_Arr[@]}" do sed -i "/.\\\$rem_domain/d" \\\$RELAYSERVER_HSHQ_STACKS_DIR/caddy/dns/dnsmasq.conf done docker container restart caddy-dns if ! [ -z \\\$deliver_to_host ]; then sed -i "/\\\$deliver_to_host/d" \\\$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/mailhosts.conf docker container restart mail-relay-unbound fi } main "\\\$@" EOFRD chmod 0500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeRelayedDomains.sh } function outputPortforwardingScripts() { sudo mkdir -p \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root/portforwarding sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPortForward.sh >/dev/null </dev/null < /dev/null 2>&1 || iptables -A PREROUTING -t nat -i \\\$DEFAULT_IFACE -p tcp --dport \\\$ExtStart:\\\$ExtEnd -j DNAT --to-destination \\\$IPAddress:\\\$IntStart-\\\$IntEnd/\\\$ExtStart iptables -C FORWARD -i \\\$DEFAULT_IFACE -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p tcp -d \\\$IPAddress --dport \\\$IntStart:\\\$IntEnd --syn -m conntrack --ctstate NEW -j ACCEPT > /dev/null 2>&1 || iptables -A FORWARD -i \\\$DEFAULT_IFACE -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p tcp -d \\\$IPAddress --dport \\\$IntStart:\\\$IntEnd --syn -m conntrack --ctstate NEW -j ACCEPT iptables -C FORWARD -i \\\$RELAYSERVER_WG_INTERFACE_NAME -o \\\$DEFAULT_IFACE -p tcp -s \\\$IPAddress --sport \\\$IntStart:\\\$IntEnd -m state --state ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 || iptables -A FORWARD -i \\\$RELAYSERVER_WG_INTERFACE_NAME -o \\\$DEFAULT_IFACE -p tcp -s \\\$IPAddress --sport \\\$IntStart:\\\$IntEnd -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -C POSTROUTING -t nat -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p tcp --dport \\\$IntStart:\\\$IntEnd -d \\\$IPAddress -j SNAT --to-source \\\$RELAYSERVER_WG_SV_IP:\\\$ExtStart-\\\$ExtEnd/\\\$IntStart > /dev/null 2>&1 || iptables -A POSTROUTING -t nat -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p tcp --dport \\\$IntStart:\\\$IntEnd -d \\\$IPAddress -j SNAT --to-source \\\$RELAYSERVER_WG_SV_IP:\\\$ExtStart-\\\$ExtEnd/\\\$IntStart } function addUDP() { iptables -C PREROUTING -t nat -i \\\$DEFAULT_IFACE -p udp --dport \\\$ExtStart:\\\$ExtEnd -j DNAT --to-destination \\\$IPAddress:\\\$IntStart-\\\$IntEnd/\\\$ExtStart > /dev/null 2>&1 || iptables -A PREROUTING -t nat -i \\\$DEFAULT_IFACE -p udp --dport \\\$ExtStart:\\\$ExtEnd -j DNAT --to-destination \\\$IPAddress:\\\$IntStart-\\\$IntEnd/\\\$ExtStart iptables -C FORWARD -i \\\$DEFAULT_IFACE -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p udp -d \\\$IPAddress --dport \\\$IntStart:\\\$IntEnd -j ACCEPT > /dev/null 2>&1 || iptables -A FORWARD -i \\\$DEFAULT_IFACE -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p udp -d \\\$IPAddress --dport \\\$IntStart:\\\$IntEnd -j ACCEPT iptables -C POSTROUTING -t nat -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p udp --dport \\\$IntStart:\\\$IntEnd -d \\\$IPAddress -j SNAT --to-source \\\$RELAYSERVER_WG_SV_IP:\\\$ExtStart-\\\$ExtEnd/\\\$IntStart > /dev/null 2>&1 || iptables -A POSTROUTING -t nat -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p udp --dport \\\$IntStart:\\\$IntEnd -d \\\$IPAddress -j SNAT --to-source \\\$RELAYSERVER_WG_SV_IP:\\\$ExtStart-\\\$ExtEnd/\\\$IntStart } main "\\\\\\\$@" EOFSC chmod 744 \\\$RELAYSERVER_PF_ADD_DIR/15-pfAdd-\\\${ID}.sh \\\$RELAYSERVER_PF_ADD_DIR/15-pfAdd-\\\${ID}.sh tee \\\$RELAYSERVER_PF_REMOVE_DIR/pfRem-\\\${ID}.sh >/dev/null < /dev/null 2>&1 iptables -D FORWARD -i \\\$DEFAULT_IFACE -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p tcp -d \\\$IPAddress --dport \\\$IntStart:\\\$IntEnd --syn -m conntrack --ctstate NEW -j ACCEPT > /dev/null 2>&1 iptables -D FORWARD -i \\\$RELAYSERVER_WG_INTERFACE_NAME -o \\\$DEFAULT_IFACE -p tcp -s \\\$IPAddress --sport \\\$IntStart:\\\$IntEnd -m state --state ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 iptables -D POSTROUTING -t nat -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p tcp --dport \\\$IntStart:\\\$IntEnd -d \\\$IPAddress -j SNAT --to-source \\\$RELAYSERVER_WG_SV_IP:\\\$ExtStart-\\\$ExtEnd/\\\$IntStart > /dev/null 2>&1 } function remUDP() { iptables -D PREROUTING -t nat -i \\\$DEFAULT_IFACE -p udp --dport \\\$ExtStart:\\\$ExtEnd -j DNAT --to-destination \\\$IPAddress:\\\$IntStart-\\\$IntEnd/\\\$ExtStart > /dev/null 2>&1 iptables -D FORWARD -i \\\$DEFAULT_IFACE -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p udp -d \\\$IPAddress --dport \\\$IntStart:\\\$IntEnd -j ACCEPT > /dev/null 2>&1 iptables -D POSTROUTING -t nat -o \\\$RELAYSERVER_WG_INTERFACE_NAME -p udp --dport \\\$IntStart:\\\$IntEnd -d \\\$IPAddress -j SNAT --to-source \\\$RELAYSERVER_WG_SV_IP:\\\$ExtStart-\\\$ExtEnd/\\\$IntStart > /dev/null 2>&1 } main "\\\\\\\$@" EOFSC chmod 744 \\\$RELAYSERVER_PF_REMOVE_DIR/pfRem-\\\${ID}.sh } main "\\\$@" EOFCD sudo chmod 500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPortForward.sh sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/removePortForward.sh >/dev/null < /dev/null } function installStack() { stack_name=\$1 container_name=\$2 stack_search_string=\$3 envfile=\$4 installLogNotify "Installing Stack (\$stack_name)" sudo -v echo echo "Creating stack: \$stack_name" echo "\$(createStackJson \$stack_name \$HOME/\$stack_name-compose.yml "\$envfile")" > \$HOME/\$stack_name-json.tmp http --check-status --ignore-stdin --verify=no --timeout=300 https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks/create/standalone/string endpointId==1 "Authorization: Bearer \$RELAYSERVER_PORTAINER_TOKEN" @\$HOME/\$stack_name-json.tmp > /dev/null search=\$stack_search_string isFound=false i=0 set +e while [ \$i -le 300 ] do findtext=\$(docker logs \$container_name 2>&1 | grep "\$search") if ! [ -z "\$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 1 second, total wait=\$i seconds..." sleep 1 i=\$((i+1)) done set -e if ! [ "\$isFound" = "true" ]; then echo "\$stack_name did not start up correctly..." exit 1 fi sleep 5 echo rm -f \$HOME/\$stack_name-json.tmp rm -f \$HOME/\$stack_name-compose.yml rm -f \$envfile } function startWazuhAgent() { curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | sudo gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import && sudo chmod 644 /usr/share/keyrings/wazuh.gpg > /dev/null 2>&1 echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" | sudo tee /etc/apt/sources.list.d/wazuh.list > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo WAZUH_MANAGER="$SUB_WAZUH.$HOMESERVER_DOMAIN" DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' wazuh-agent=$WAZUH_AGENT_VERSION > /dev/null 2>&1 sudo apt-mark hold wazuh-agent sudo systemctl daemon-reload set +e sudo grep "/var/log/docker/" /var/ossec/etc/ossec.conf if [ \$? -ne 0 ]; then sudo sed -i "/<\/ossec_config>/{s// \n syslog<\/log_format>\n \/var\/log\/docker\/*<\/location>\n <\/localfile>\n\n<\/ossec_config>/;:p;n;bp}" /var/ossec/etc/ossec.conf fi sudo tee /var/ossec/etc/authd.pass >/dev/null </dev/null sudo chmod 0400 \$RELAYSERVER_HSHQ_SECRETS_DIR/portainer_key.txt cd ~ docker compose -f \$RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml up -d search="starting HTTPS server" isFound=false i=0 set +e while [ \$i -le 300 ] do findtext=\$(docker logs portainer 2>&1 | grep "\$search") if ! [ -z "\$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 1 second, total wait=\$i seconds..." sleep 1 i=\$((i+1)) done set -e if ! [ "\$isFound" = "true" ]; then echo "Portainer did not start up correctly..." exit 1 fi echo "Portainer has been loaded and started..." echo "Sleeping 10 seconds to ensure Portainer has loaded completely." sleep 10 RELAYSERVER_PORTAINER_ADMIN_USERID=\$(http --verify=no --print="b" POST https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/users/admin/init Username="$RELAYSERVER_PORTAINER_ADMIN_USERNAME" Password="$RELAYSERVER_PORTAINER_ADMIN_PASSWORD" | jq -r .Id) RELAYSERVER_PORTAINER_TOKEN="\$(getPortainerToken -u $RELAYSERVER_PORTAINER_ADMIN_USERNAME -p $RELAYSERVER_PORTAINER_ADMIN_PASSWORD)" http -f --verify=no --timeout=300 --print="b" POST https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/endpoints "Authorization: Bearer \$RELAYSERVER_PORTAINER_TOKEN" Name="$RELAYSERVER_NAME" EndpointCreationType=1 >/dev/null # Enable dark mode because it looks better echo "{\"theme\":{\"color\":\"dark\"}}" > portainer-json.tmp http --verify=no --timeout=300 PUT https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/users/\$RELAYSERVER_PORTAINER_ADMIN_USERID "Authorization: Bearer \$RELAYSERVER_PORTAINER_TOKEN" @portainer-json.tmp > /dev/null rm portainer-json.tmp # Add gcr registry echo "{\"Name\": \"GCR\",\"Type\": 3,\"URL\": \"mirror.gcr.io\"}" > portainer-json.tmp http --verify=no --timeout=300 POST https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/registries "Authorization: Bearer \$RELAYSERVER_PORTAINER_TOKEN" @portainer-json.tmp > /dev/null rm portainer-json.tmp } function outputConfigPortainer() { cat < \$RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml $STACK_VERSION_PREFIX portainer $(getScriptStackVersion portainer) services: portainer: image: $IMG_PORTAINER container_name: portainer hostname: portainer restart: unless-stopped security_opt: - no-new-privileges:true env_file: portainer.env networks: - dock-proxy-net # Comment out the external network to disable external (web) access # Also ensure to comment out the ports section and dock-ext-net section at the bottom - dock-ext-net ports: - "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT:9443" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/run/docker.sock:/var/run/docker.sock - \$RELAYSERVER_HSHQ_STACKS_DIR/portainer:/data - \$RELAYSERVER_HSHQ_SSL_DIR/portainer.crt:/certs/portainer.crt - \$RELAYSERVER_HSHQ_SSL_DIR/portainer.key:/certs/portainer.key - \$RELAYSERVER_HSHQ_SECRETS_DIR/portainer_key.txt:/run/secrets/portainer command: --tlscert /certs/portainer.crt --tlskey /certs/portainer.key --http-disabled --no-setup-token networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFPC cat < \$RELAYSERVER_HSHQ_STACKS_DIR/portainer/portainer.env PORTAINER_RELAYSERVER_HSHQ_DATA_DIR=\$RELAYSERVER_HSHQ_DATA_DIR PORTAINER_RELAYSERVER_HSHQ_NONBACKUP_DIR=\$RELAYSERVER_HSHQ_NONBACKUP_DIR PORTAINER_RELAYSERVER_HSHQ_SCRIPTS_DIR=\$RELAYSERVER_HSHQ_SCRIPTS_DIR PORTAINER_RELAYSERVER_HSHQ_SECRETS_DIR=\$RELAYSERVER_HSHQ_SECRETS_DIR PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR=\$RELAYSERVER_HSHQ_STACKS_DIR PORTAINER_RELAYSERVER_HSHQ_SSL_DIR=\$RELAYSERVER_HSHQ_SSL_DIR PORTAINER_TZ=\$TZ PORTAINER_UID=\$USERID PORTAINER_GID=\$GROUPID EOFPC } function installAdGuard() { mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/adguard mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/adguard/conf mkdir \$RELAYSERVER_HSHQ_NONBACKUP_DIR/adguard mkdir \$RELAYSERVER_HSHQ_NONBACKUP_DIR/adguard/work outputConfigAdGuard generateCert adguard adguard set +e sudo systemctl stop systemd-resolved > /dev/null 2>&1 sudo systemctl disable systemd-resolved > /dev/null 2>&1 sudo systemctl mask systemd-resolved > /dev/null 2>&1 sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 fi set -e installStack adguard adguard "entering listener loop proto=tls" \$HOME/adguard.env } function outputConfigAdGuard() { RELAYSERVER_ADGUARD_ADMIN_PASSWORD_HASH=\$(htpasswd -B -n -b $RELAYSERVER_ADGUARD_ADMIN_USERNAME $RELAYSERVER_ADGUARD_ADMIN_PASSWORD | cut -d":" -f2-) cat < \$HOME/adguard-compose.yml $STACK_VERSION_PREFIX adguard $(getScriptStackVersion adguard) services: adguard: image: $IMG_ADGUARD container_name: adguard hostname: adguard restart: unless-stopped env_file: stack.env user: "\\\${PORTAINER_UID}:\\\${PORTAINER_GID}" security_opt: - no-new-privileges:true networks: dock-proxy-net: dock-ext-net: ipv4_address: \${NET_EXTERNAL_SUBNET_PREFIX}.253 ports: - "53:53/tcp" - "53:53/udp" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/adguard/conf:/opt/adguardhome/conf - \\\${PORTAINER_RELAYSERVER_HSHQ_NONBACKUP_DIR}/adguard/work:/opt/adguardhome/work - \\\${PORTAINER_RELAYSERVER_HSHQ_SSL_DIR}/adguard.crt:/opt/adguardhome/conf/cert.pem - \\\${PORTAINER_RELAYSERVER_HSHQ_SSL_DIR}/adguard.key:/opt/adguardhome/conf/key.pem networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFAC cat < \$HOME/adguard.env GODEBUG=tlskyber=0 EOFAC cat < \$RELAYSERVER_HSHQ_STACKS_DIR/adguard/conf/AdGuardHome.yaml http: pprof: port: 6060 enabled: false address: 0.0.0.0:3000 session_ttl: 720h users: - name: $RELAYSERVER_ADGUARD_ADMIN_USERNAME password: \$RELAYSERVER_ADGUARD_ADMIN_PASSWORD_HASH auth_attempts: 5 block_auth_min: 15 http_proxy: "" language: en theme: dark dns: bind_hosts: - 0.0.0.0 port: 53 anonymize_client_ip: false ratelimit: 0 ratelimit_subnet_len_ipv4: 24 ratelimit_subnet_len_ipv6: 56 ratelimit_whitelist: [] refuse_any: true upstream_dns: - https://dns.quad9.net/dns-query - https://anycast.dns.nextdns.io/dns-query - quic://dns-unfiltered.adguard.com:784 - https://dns.nextdns.io/dns-query upstream_dns_file: "" bootstrap_dns: - 9.9.9.9 - 149.112.112.112 fallback_dns: - 9.9.9.10 - 149.112.112.10 - 94.140.14.14 - 94.140.15.15 upstream_mode: parallel fastest_timeout: 1s allowed_clients: - 127.0.0.0/8 - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 disallowed_clients: [] blocked_hosts: - version.bind - id.server - hostname.bind trusted_proxies: - 127.0.0.0/8 - ::1/128 cache_size: 41943040 cache_ttl_min: 0 cache_ttl_max: 0 cache_optimistic: true bogus_nxdomain: [] aaaa_disabled: true enable_dnssec: true edns_client_subnet: custom_ip: "" enabled: false use_custom: false max_goroutines: 300 handle_ddr: true ipset: [] ipset_file: "" bootstrap_prefer_ipv6: false upstream_timeout: 3s private_networks: [] use_private_ptr_resolvers: true local_ptr_upstreams: [] use_dns64: false dns64_prefixes: [] serve_http3: false use_http3_upstreams: false serve_plain_dns: true hostsfile_enabled: true tls: enabled: true server_name: adguard force_https: true port_https: 443 port_dns_over_tls: 853 port_dns_over_quic: 853 port_dnscrypt: 0 dnscrypt_config_file: "" allow_unencrypted_doh: false certificate_chain: "" private_key: "" certificate_path: /opt/adguardhome/conf/cert.pem private_key_path: /opt/adguardhome/conf/key.pem strict_sni_check: false querylog: dir_path: "" ignored: - $HOMESERVER_DOMAIN - '*.$HOMESERVER_DOMAIN' interval: 720h size_memory: 1000 enabled: true file_enabled: true statistics: dir_path: "" ignored: - $HOMESERVER_DOMAIN - '*.$HOMESERVER_DOMAIN' interval: 720h enabled: true filters: - enabled: true url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt name: AdGuard DNS filter id: 1 - enabled: true url: https://adaway.org/hosts.txt name: AdAway Default Blocklist id: 2 - enabled: false url: https://someonewhocares.org/hosts/zero/hosts name: Dan Pollock's List id: 1657273139 - enabled: false url: https://raw.githubusercontent.com/Perflyst/PiHoleBlocklist/master/SmartTV-AGH.txt name: Perflyst and Dandelion Sprout's Smart-TV Blocklist id: 1657273140 - enabled: false url: https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt name: WindowsSpyBlocker - Hosts spy rules id: 1657273141 - enabled: false url: https://raw.githubusercontent.com/DandelionSprout/adfilt/master/Alternate%20versions%20Anti-Malware%20List/AntiMalwareAdGuardHome.txt name: Dandelion Sprout's Anti-Malware List id: 1657273142 - enabled: false url: https://raw.githubusercontent.com/hoshsadiq/adblock-nocoin-list/master/hosts.txt name: NoCoin Filter List id: 1657273143 - enabled: false url: https://raw.githubusercontent.com/durablenapkin/scamblocklist/master/adguard.txt name: Scam Blocklist by DurableNapkin id: 1657273144 - enabled: false url: https://raw.githubusercontent.com/mitchellkrogza/The-Big-List-of-Hacked-Malware-Web-Sites/master/hosts name: The Big List of Hacked Malware Web Sites id: 1657273145 - enabled: false url: https://malware-filter.gitlab.io/malware-filter/urlhaus-filter-agh-online.txt name: Online Malicious URL Blocklist id: 1657273146 - enabled: true url: https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adblockplus&showintro=1&mimetype=plaintext name: Peter Lowe's List id: 1657273147 whitelist_filters: [] user_rules: - '@@||api.ipify.org^' dhcp: enabled: false interface_name: "" local_domain_name: lan dhcpv4: gateway_ip: "" subnet_mask: "" range_start: "" range_end: "" lease_duration: 86400 icmp_timeout_msec: 1000 options: [] dhcpv6: range_start: "" lease_duration: 86400 ra_slaac_only: false ra_allow_slaac: false filtering: blocking_ipv4: "" blocking_ipv6: "" blocked_services: schedule: time_zone: Local ids: [] protection_disabled_until: null safe_search: enabled: false bing: true duckduckgo: true ecosia: true google: true pixabay: true yandex: true youtube: true blocking_mode: default parental_block_host: family-block.dns.adguard.com safebrowsing_block_host: standard-block.dns.adguard.com rewrites: - domain: '*.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN' answer: $RELAYSERVER_SERVER_IP - domain: '*.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN' answer: $RELAYSERVER_WG_SV_IP - domain: '$HOMESERVER_DOMAIN' answer: $RELAYSERVER_WG_HS_IP - domain: '*.$HOMESERVER_DOMAIN' answer: $RELAYSERVER_WG_HS_IP safe_fs_patterns: - /opt/adguardhome/work/data/userfilters/* safebrowsing_cache_size: 10485760 safesearch_cache_size: 10485760 parental_cache_size: 10485760 cache_time: 30 filters_update_interval: 24 blocked_response_ttl: 10 filtering_enabled: true parental_enabled: false safebrowsing_enabled: false protection_enabled: true clients: runtime_sources: whois: true arp: true rdns: true dhcp: true hosts: true persistent: [] log: enabled: true file: "" max_backups: 0 max_size: 100 max_age: 3 compress: false local_time: false verbose: false os: group: "" user: "" rlimit_nofile: 0 schema_version: 29 EOFAD } function installMailRelay() { mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/config mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/sasl mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/tls mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/var mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/clamav mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound pw_hash=\$(docker run --rm $IMG_MAIL_RELAY_RSPAMD rspamadm pw -p $RELAYSERVER_RSPAMD_ADMIN_PASSWORD) outputConfigMailRelay echo "Generating DH parameters, 1024 bit long safe prime..." openssl dhparam -out \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/tls/dh1024.pem 1024 > /dev/null 2>&1 echo "Generating DH parameters, 512 bit long safe prime..." openssl dhparam -out \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/tls/dh512.pem 512 > /dev/null 2>&1 openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/tls/key.pem -out \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/tls/cert.pem -subj "/C=US/ST=MO/L=STL/O=HomeServerHQ/OU=SampleCert/CN=$RELAYSERVER_EXT_EMAIL_HOSTNAME" chmod 0400 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/tls/cert.pem chmod 0400 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/tls/key.pem installStack mail-relay mail-relay-postfix "Starting Postfix Mail Transport Agent" \$HOME/mail-relay.env docker exec mail-relay-postfix bash -c "echo $SMTP_RELAY_PASSWORD | saslpasswd2 -c -p -f /etc/postfix/sasl/sasldb2 $SMTP_RELAY_USERNAME" } function outputConfigMailRelay() { cat < \$HOME/mail-relay-compose.yml $STACK_VERSION_PREFIX mail-relay $(getScriptStackVersion mail-relay) services: mail-relay-postfix: image: $IMG_MAIL_RELAY_POSTFIX container_name: mail-relay-postfix hostname: mail-relay-postfix restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-mailrelay-net ports: - "$MAILU_PORT_1:$MAILU_PORT_1" - "$MAILU_PORT_5:$MAILU_PORT_5" dns: - \\\${SUBNET_PREFIX}.253 depends_on: - mail-relay-redis - mail-relay-unbound volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/etc/postfix/tls/${CERTS_ROOT_CA_NAME}.crt:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/postfix/config:/etc/postfix/config - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/postfix/sasl:/etc/postfix/sasl - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/postfix/scripts:/etc/postfix/scripts - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/postfix/tls:/etc/postfix/tls - caddy-certs:/caddycerts:ro labels: - "ofelia.enabled=true" - "ofelia.job-exec.mailcerts-15minute.schedule=@every 15m" - "ofelia.job-exec.mailcerts-15minute.command=/etc/postfix/scripts/copycerts.sh" - "ofelia.job-exec.mailcerts-daily.schedule=0 0 8 * * *" - "ofelia.job-exec.mailcerts-daily.command=/etc/postfix/scripts/copycerts.sh" - "ofelia.job-exec.mailcerts-daily.smtp-host=$SUB_POSTFIX.$HOMESERVER_DOMAIN" - "ofelia.job-exec.mailcerts-daily.smtp-port=$MAILU_PORT_5" - "ofelia.job-exec.mailcerts-daily.smtp-user=$EMAIL_SMTP_EMAIL_ADDRESS" - "ofelia.job-exec.mailcerts-daily.smtp-password=$EMAIL_SMTP_PASSWORD" - "ofelia.job-exec.mailcerts-daily.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.mailcerts-daily.email-from=Mail Certificate Updater <$EMAIL_SMTP_EMAIL_ADDRESS>" - "ofelia.job-exec.mailcerts-daily.mail-only-on-error=true" mail-relay-rspamd: image: $IMG_MAIL_RELAY_RSPAMD container_name: mail-relay-rspamd hostname: mail-relay-rspamd restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-mailrelay-net dns: - \\\${SUBNET_PREFIX}.253 depends_on: - mail-relay-redis - mail-relay-unbound - mail-relay-postfix volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-mail-relay-rspamd-var:/var/lib/rspamd - v-mail-relay-rspamd-conf:/etc/rspamd/local.d mail-relay-unbound: image: $IMG_MAIL_RELAY_UNBOUND container_name: mail-relay-unbound hostname: mail-relay-unbound restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: dock-ext-net: dock-mailrelay-net: ipv4_address: \\\${SUBNET_PREFIX}.253 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/unbound/mailhosts.conf:/opt/unbound/etc/unbound/a-records.conf:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/unbound/forward-records.conf:/opt/unbound/etc/unbound/forward-records.conf:ro # mail-relay-clamav: # image: $IMG_MAIL_RELAY_CLAMAV # container_name: mail-relay-clamav # hostname: mail-relay-clamav # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # networks: # - dock-ext-net # - dock-mailrelay-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - v-mail-relay-clamav:/var/lib/clamav mail-relay-redis: image: $IMG_REDIS container_name: mail-relay-redis hostname: mail-relay-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --appendonly no networks: - dock-mailrelay-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: caddy-certs: external: true v-mail-relay-rspamd-var: driver: local driver_opts: type: none o: bind device: \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/rspamd/var v-mail-relay-rspamd-conf: driver: local driver_opts: type: none o: bind device: \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/rspamd/conf # v-mail-relay-clamav: # driver: local # driver_opts: # type: none # o: bind # device: \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/mail-relay/clamav networks: dock-ext-net: name: dock-ext external: true dock-proxy-net: name: dock-proxy external: true dock-mailrelay-net: name: dock-mailrelay external: true EOFPF cat < \$HOME/mail-relay.env TZ=\\\${PORTAINER_TZ} MAIL_FQDN=$RELAYSERVER_EXT_EMAIL_HOSTNAME POSTMASTER_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS INTERNAL_CA_CERT_FILENAME=${CERTS_ROOT_CA_NAME}.crt REDIS_HOST=mail-relay-redis WEB_GUI_PASSWORD_HASH='\$pw_hash' SUBNET=\$MAIL_RELAY_SUBNET SUBNET_PREFIX=\$MAIL_RELAY_SUBNET_PREFIX EOFPF cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/groups.conf group "policies" { symbols = { "DMARC_POLICY_REJECT" = { weight = 100.0; } } } EOFHC sudo chown root:root \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/groups.conf sudo chmod 644 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/groups.conf cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/multimap.conf IP_WHITELIST { type = "ip"; prefilter = true; map = "/etc/rspamd/local.d/ip_whitelist.map"; action = "accept"; } IP_BLACKLIST { type = "ip"; prefilter = true; map = "/etc/rspamd/local.d/ip_blacklist.map"; action = "reject"; } DOMAIN_WHITELIST { regexp = true; type = "from"; filter = "email:domain"; map = "/etc/rspamd/local.d/domain_whitelist.map"; score = -100.0; action = "accept"; } DOMAIN_BLACKLIST { regexp = true; type = "from"; filter = "email:domain"; map = "/etc/rspamd/local.d/domain_blacklist.map"; score = 100.0; action = "reject"; } EOFHC touch \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/ip_whitelist.map sudo chown 101:102 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/ip_whitelist.map sudo chmod 644 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/ip_whitelist.map touch \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/ip_blacklist.map sudo chown 101:102 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/ip_blacklist.map sudo chmod 644 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/ip_blacklist.map touch \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/domain_whitelist.map sudo chown 101:102 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/domain_whitelist.map sudo chmod 644 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/domain_whitelist.map touch \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/domain_blacklist.map sudo chown 101:102 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/domain_blacklist.map sudo chmod 644 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/domain_blacklist.map sudo chown 101:102 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/config/header_checks /^Received:/ IGNORE /^X-Originating-IP:/ IGNORE /^X-Mailer:/ IGNORE /^User-Agent:/ IGNORE /^X-Mailer-Type:/ IGNORE /^Authentication-Results:/ IGNORE EOFHC cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/config/relay $HOMESERVER_DOMAIN OK EOFHC cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/config/transport $HOMESERVER_DOMAIN relay:[$SUB_POSTFIX.$HOMESERVER_DOMAIN]:25 EOFHC cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/config/sasl_senders @$HOMESERVER_DOMAIN $HOMESERVER_DOMAIN@mail-relay-postfix EOFHC cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/copycerts.sh #!/bin/bash find_certs=(\\\$(find /caddycerts -name \\\${MAIL_FQDN}.crt)) find_cert=\\\${find_certs[0]} if ! [ -z "\\\$find_cert" ]; then find_key=\\\$(dirname \\\$find_cert)/\\\${MAIL_FQDN}.key fi if ! [ -z "\\\$find_cert" ] && ! [ -z "\\\$find_key" ] && [ -f \\\$find_key ]; then diff \\\$find_cert /etc/postfix/tls/cert.pem >/dev/null if [ \\\$? -ne 0 ]; then cp \\\$find_cert /etc/postfix/tls/cert.pem cp \\\$find_key /etc/postfix/tls/key.pem # Bypass the health check reload rm -rf /tmp/tls 2> /dev/null cp -a /etc/postfix/tls /tmp/tls postfix reload echo "Certificates Successfully Updated" exit 2 fi else echo "Certificate (\\\${MAIL_FQDN}.crt) does not exist" exit 1 fi EOFCC chmod 544 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/copycerts.sh cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/addRelayedMailDomains.sh #!/bin/bash set -e domains_to_add=\\\$1 deliver_to_host=\\\$2 if [ -z "\\\$domains_to_add" ] || [ -z "\\\$deliver_to_host" ]; then echo "Empty Parameters" exit 1 fi deliver_to_host_username=\\\$(echo \\\$deliver_to_host | cut -d"." -f2-) domains_to_add_Arr=(\\\$(echo \\\$domains_to_add | tr "," "\n")) for curDomain in "\\\${domains_to_add_Arr[@]}" do sed -i "/^@\\\$curDomain /d" /etc/postfix/config/sasl_senders sed -i "/\\\$curDomain/d" /etc/postfix/config/transport sed -i "/^\\\$curDomain /d" /etc/postfix/config/relay echo "@\\\${curDomain} \\\${deliver_to_host_username}@mail-relay-postfix" >> /etc/postfix/config/sasl_senders sed -i "1s/^/\\\${curDomain} relay:[\\\${deliver_to_host}]:25\n/" /etc/postfix/config/transport echo "\\\${curDomain} OK" >> /etc/postfix/config/relay done postmap /etc/postfix/config/transport postmap /etc/postfix/config/sasl_senders postmap /etc/postfix/config/relay postfix reload EOFCS chmod 544 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/addRelayedMailDomains.sh cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/removeRelayedMailDomains.sh #!/bin/bash set +e domains_to_remove=\\\$1 mail_user_domain=\\\$2 domains_to_rem_Arr=(\\\$(echo \\\$domains_to_remove | tr "," "\n")) for curDomain in "\\\${domains_to_rem_Arr[@]}" do sed -i "/^@\\\$curDomain /d" /etc/postfix/config/sasl_senders sed -i "/\\\$curDomain/d" /etc/postfix/config/transport sed -i "/^\\\$curDomain /d" /etc/postfix/config/relay done if ! [ -z \\\$mail_user_domain ]; then saslpasswd2 -f /etc/postfix/sasl/sasldb2 -d \\\$mail_user_domain fi postmap /etc/postfix/config/transport postmap /etc/postfix/config/sasl_senders postmap /etc/postfix/config/relay postfix reload EOFCS chmod 544 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/removeRelayedMailDomains.sh cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/addMailUser.sh #!/bin/bash set -e username=\\\$1 password=\\\$2 echo \\\$password | saslpasswd2 -c -p -f /etc/postfix/sasl/sasldb2 \\\$username postfix reload EOFCS chmod 544 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/addMailUser.sh cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/mailhosts.conf local-data: "$SUB_POSTFIX.${HOMESERVER_DOMAIN}. A $RELAYSERVER_WG_HS_IP" local-data-ptr: "$RELAYSERVER_WG_HS_IP $SUB_POSTFIX.${HOMESERVER_DOMAIN}." EOFUB chmod 644 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/mailhosts.conf touch \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/forward-records.conf chmod 644 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/unbound/forward-records.conf } function getAllowedPublicIPs() { set +e ips="0.0.0.0/5,8.0.0.0/7,11.0.0.0/8,12.0.0.0/6,16.0.0.0/4,32.0.0.0/3,64.0.0.0/2,128.0.0.0/3,160.0.0.0/5,168.0.0.0/6,172.0.0.0/12,172.32.0.0/11,172.64.0.0/10,172.128.0.0/9,173.0.0.0/8,174.0.0.0/7,176.0.0.0/4,192.0.0.0/9,192.128.0.0/11,192.160.0.0/13,192.169.0.0/16,192.170.0.0/15,192.172.0.0/14,192.176.0.0/12,192.192.0.0/10,193.0.0.0/8,194.0.0.0/7,196.0.0.0/6,200.0.0.0/5,208.0.0.0/4" if ! [ -z \$1 ]; then ips+=","\$1 fi set -e echo \$ips } function installWireGuard() { mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/clientdns mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal wgdate=\$(date) wginitdate=\$(date --date="\$wgdate" -u '+%Y-%m-%d %H:%M:%S.%N %z %Z') wgdbdate=\$(date --date="\$wgdate" -u '+%Y-%m-%d %H:%M:%S.%N') clientdns_subnet=\$(getDockerSubnet cdns-user1) clientdns_subnet_prefix=\$(echo \$clientdns_subnet | rev | cut -d "." -f2- | rev) outputConfigWireGuard sudo ln /etc/wireguard/$RELAYSERVER_WG_INTERFACE_NAME.conf \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/$RELAYSERVER_WG_INTERFACE_NAME.conf sudo systemctl enable wg-quick@$RELAYSERVER_WG_INTERFACE_NAME.service sudo systemctl start wg-quick@$RELAYSERVER_WG_INTERFACE_NAME.service installStack wgportal wgportal "starting web service on" \$HOME/wgportal.env startStopStack wgportal stop sudo sqlite3 \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal/wg_portal.db "update devices set display_name='WireGuard Server', mtu=$RELAYSERVER_SERVER_DEFAULT_MTU, dns_str='$RELAYSERVER_WG_SV_IP', post_up='\$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/wgupdown.sh up;', post_down='\$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/wgupdown.sh down;', default_endpoint='$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN:$RELAYSERVER_WG_PORT', default_allowed_ips_str='\$(getAllowedPublicIPs $PRIMARY_VPN_SUBNET)', default_persistent_keepalive=0, created_at='\$wgdbdate', updated_at='\$wgdbdate' where device_name='$RELAYSERVER_WG_INTERFACE_NAME';" sudo chmod 0600 \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal/wg_portal.db startStopStack wgportal start sleep 2 wgPortalAuth="\$(echo \$(echo -n ${RELAYSERVER_WGPORTAL_ADMIN_EMAIL}:${RELAYSERVER_WGPORTAL_ADMIN_PASSWORD} | base64) | sed 's| ||g')" sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh "RelayServerClientDNS" "$EMAIL_ADMIN_EMAIL_ADDRESS" "$RELAYSERVER_WG_SV_CLIENTDNS_PUBLICKEY" "$RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY" "$RELAYSERVER_WG_SV_CLIENTDNS_IP" "false" "true" "\$wgPortalAuth" sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh "HomeServerClientDNS" "$EMAIL_ADMIN_EMAIL_ADDRESS" "$RELAYSERVER_WG_HS_CLIENTDNS_PUBLICKEY" "$RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY" "$RELAYSERVER_WG_HS_CLIENTDNS_IP" "false" "true" "\$wgPortalAuth" sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh "Primary-VPN-$HOMESERVER_DOMAIN" "$EMAIL_ADMIN_EMAIL_ADDRESS" "$RELAYSERVER_WG_HS_PUBLICKEY" "$RELAYSERVER_WG_HS_PRESHAREDKEY" "$RELAYSERVER_WG_HS_IP" "false" "true" "\$wgPortalAuth" sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh "Primary-Internet-$HOMESERVER_DOMAIN" "$EMAIL_ADMIN_EMAIL_ADDRESS" "$RELAYSERVER_WG_INTERNET_HS_PUBLICKEY" "$RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY" "$RELAYSERVER_WG_INTERNET_HS_IP" "true" "false" "\$wgPortalAuth" sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh "User-1" "$EMAIL_ADMIN_EMAIL_ADDRESS" "$RELAYSERVER_WG_USER_PUBLICKEY" "$RELAYSERVER_WG_USER_PRESHAREDKEY" "$RELAYSERVER_WG_USER_IP" "true" "false" "\$wgPortalAuth" sudo systemctl stop wg-quick@$RELAYSERVER_WG_INTERFACE_NAME.service installStack clientdns clientdns-wireguard " " \$HOME/clientdns.env } function getDefaultIface() { echo \$(ip route | grep -e "^default" | head -n 1 | awk -F'dev ' '{print \$2}' | xargs | cut -d" " -f1) } function outputConfigWireGuard() { sudo touch \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/inetusers.ipset sudo chmod 600 \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/inetusers.ipset default_iface=\$(getDefaultIface) sudo tee \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/wgupdown.sh >/dev/null </dev/null </dev/null if [ \\\$? -ne 0 ]; then echo "There was a problem adding this client..." exit 1 fi ipset add alldevices \\\${ipAddress}/32 if ! [ "\\\$isInVPNSubnet" = "true" ]; then route add \\\${ipAddress}/32 $RELAYSERVER_WG_INTERFACE_NAME fi if [ "\\\$isInet" = "true" ]; then ipset add inetusers \\\${ipAddress}/32 echo \\\${ipAddress}/32 | tee -a \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/inetusers.ipset >/dev/null fi EOFWA sudo chmod 500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/removePeer.sh >/dev/null </dev/null if [ \\\$? -ne 0 ]; then echo "There was a problem removing this client..." fi ipset del alldevices \\\${ipAddress}/32 if ! [ "\\\$isInVPNSubnet" = "true" ]; then route del \\\${ipAddress}/32 $RELAYSERVER_WG_INTERFACE_NAME fi if [ "\\\$isInet" = "true" ]; then ipset del inetusers \\\${ipAddress}/32 sed -i '/\\\${ipAddress}/d' \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/inetusers.ipset fi } function urlEncode() { strLength="\\\${#1}" for (( i = 0; i < strLength; i++ )); do c="\\\${1:i:1}" case \\\$c in [a-zA-Z0-9.~_-]) printf "\\\$c" ;; *) printf '%%%02X' "'\\\$c" ;; esac done } function urlDecode() { url_encoded="\\\${1//+/ }" printf '%b' "\\\${url_encoded//%/\\x}" } main "\\\$@" EOFWA sudo chmod 500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/removePeer.sh network_bits=\$(echo $PRIMARY_VPN_SUBNET | cut -d "/" -f2) sudo tee /etc/wireguard/$RELAYSERVER_WG_INTERFACE_NAME.conf >/dev/null < \$HOME/wgportal-compose.yml $STACK_VERSION_PREFIX wgportal $(getScriptStackVersion wgportal) services: wgportal: image: $IMG_WGPORTAL container_name: wgportal hostname: wgportal restart: unless-stopped env_file: stack.env network_mode: "host" cap_add: - NET_ADMIN volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/wireguard:/etc/wireguard - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/wireguard/wgportal:/app/data EOFWP cat < \$HOME/wgportal.env CONFIG_FILE=/app/data/config.yml EOFWP cat < \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal/config.yml core: listeningAddress: :$RELAYSERVER_WG_PORTAL_PORT externalUrl: https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ adminUser: $RELAYSERVER_WGPORTAL_ADMIN_EMAIL adminPass: $RELAYSERVER_WGPORTAL_ADMIN_PASSWORD editableKeys: true createDefaultPeer: false ldapEnabled: false company: $RELAYSERVER_NAME title: $RELAYSERVER_NAME WG Portal mailFrom: WireGuard <$EMAIL_SMTP_EMAIL_ADDRESS> database: type: sqlite database: data/wg_portal.db email: host: $SUB_POSTFIX.$HOMESERVER_DOMAIN port: $MAILU_PORT_5 tls: true user: $EMAIL_SMTP_EMAIL_ADDRESS pass: $EMAIL_SMTP_PASSWORD wg: devices: - $RELAYSERVER_WG_INTERFACE_NAME defaultDevice: $RELAYSERVER_WG_INTERFACE_NAME configDirectory: /etc/wireguard manageIPAddresses: true EOFWC chmod 600 \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal/config.yml cat < \$HOME/clientdns-compose.yml $STACK_VERSION_PREFIX clientdns $(getScriptStackVersion clientdns) services: clientdns-dnsmasq: image: $IMG_DNSMASQ container_name: clientdns-dnsmasq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: dock-ext-net: dock-proxy-net: cdns-user1-net: ipv4_address: \\\${CLIENTDNS_SUBNET_PREFIX}.253 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/wireguard/clientdns/dnsmasq.conf:/etc/dnsmasq.conf environment: - HTTP_USER=$RELAYSERVER_CLIENTDNS_ADMIN_USERNAME - HTTP_PASS=$RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD clientdns-wireguard: image: $IMG_WIREGUARD container_name: clientdns-wireguard hostname: clientdns-wireguard restart: unless-stopped env_file: stack.env cap_add: - NET_ADMIN networks: - dock-ext-net - cdns-user1-net sysctls: - net.ipv4.conf.all.src_valid_mark=1 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/wireguard/clientdns/Corefile:/config/coredns/Corefile:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/wireguard/clientdns/rsClientDNS.conf:/config/wg0.conf networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true cdns-user1-net: name: cdns-user1 external: true EOFCD cat < \$HOME/clientdns.env TZ=\\\${PORTAINER_TZ} CLIENTDNS_SUBNET_PREFIX=\$clientdns_subnet_prefix USE_COREDNS=true EOFCD cat < \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/clientdns/dnsmasq.conf # IP Address of this DNS Server: ${RELAYSERVER_WG_SV_CLIENTDNS_IP}/32 #====================== no-resolv no-hosts domain-needed bogus-priv server=127.0.0.11 cache-size=0 #====================== # Add new entries here EOFCD cat < \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/clientdns/Corefile . { loop reload 15s forward . \${clientdns_subnet_prefix}.253 } EOFCF sudo tee \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/clientdns/rsClientDNS.conf >/dev/null < \$HOME/filebrowser-compose.yml $STACK_VERSION_PREFIX filebrowser $(getScriptStackVersion filebrowser) services: filebrowser: image: $IMG_FILEBROWSER container_name: filebrowser hostname: filebrowser restart: unless-stopped env_file: stack.env user: "\\\${PORTAINER_UID}:\\\${PORTAINER_GID}" security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/filebrowser/filebrowser.json:/.filebrowser.json - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/filebrowser/srv:/srv - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/filebrowser/db:/database - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/filebrowser/config:/config networks: dock-proxy-net: name: dock-proxy external: true EOFFB cat < \$HOME/filebrowser.env UID=\$USERID GID=\$GROUPID FB_USERNAME=$RELAYSERVER_FILEBROWSER_ADMIN_USERNAME FB_PASSWORD='\$FILEBROWSER_PASSWORD_HASH' EOFFB cat < \$RELAYSERVER_HSHQ_STACKS_DIR/filebrowser/filebrowser.json { "port": 80, "baseURL": "", "address": "", "log": "stdout", "database": "/database/filebrowser.db", "root": "/srv" } EOFFB } function installCaddy() { mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/caddy mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/dns mkdir \$RELAYSERVER_HSHQ_NONBACKUP_DIR/caddy mkdir \$RELAYSERVER_HSHQ_NONBACKUP_DIR/caddy/data mkdir \$RELAYSERVER_HSHQ_NONBACKUP_DIR/caddy/config outputConfigCaddy installStack caddy caddy "serving initial configuration" \$HOME/caddy.env } function outputConfigCaddy() { cat < \$HOME/caddy-compose.yml $STACK_VERSION_PREFIX caddy-rs $(getScriptStackVersion caddy-rs) services: caddy: image: $IMG_CADDY container_name: caddy hostname: caddy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true extra_hosts: - host.docker.internal:host-gateway networks: - dock-proxy-net - dock-ext-net ports: - "80:80" - "443:443" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/caddy/Caddyfile:/etc/caddy/Caddyfile - \\\${PORTAINER_RELAYSERVER_HSHQ_NONBACKUP_DIR}/caddy/data:/data - \\\${PORTAINER_RELAYSERVER_HSHQ_NONBACKUP_DIR}/caddy/config:/config - caddy-certs:/data/caddy/certificates caddy-dns: image: $IMG_DNSMASQ container_name: caddy-dns hostname: caddy-dns restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: dock-proxy-net: ipv4_address: \\\${NET_WEBPROXY_SUBNET_PREFIX}.253 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/caddy/dns/dnsmasq.conf:/etc/dnsmasq.conf volumes: caddy-certs: external: true networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFCC connIP=\$(getConnectingIPAddress) privSubnets="${RELAYSERVER_WG_HS_IP}/32 ${RELAYSERVER_WG_USER_IP}/32" if ! [ -z "\$connIP" ]; then privSubnets=\$privSubnets" \${connIP}/32" fi cat < \$HOME/caddy.env CERT_RENEW_INTERVAL=1h PRIVATE_SUBNETS=\$privSubnets INTERNAL_SUBNETS=10.0.0.0/8 NET_WEBPROXY_SUBNET_PREFIX=\$NET_WEBPROXY_SUBNET_PREFIX HTTP_USER=$RELAYSERVER_CADDYDNS_ADMIN_USERNAME HTTP_PASS=$RELAYSERVER_CADDYDNS_ADMIN_PASSWORD EOFCC cat < \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/dns/dnsmasq.conf no-resolv address=/.$HOMESERVER_DOMAIN/$RELAYSERVER_WG_HS_IP address=/.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/$RELAYSERVER_WG_SV_IP EOFDM cat < \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile { email $CERTS_EMAIL_ADDRESS acme_ca https://acme.zerossl.com/v2/DV90 renew_interval {\\\$CERT_RENEW_INTERVAL} } (rip-private) { @subnet remote_ip {\\\$PRIVATE_SUBNETS} } (rip-internal) { @subnet remote_ip {\\\$INTERNAL_SUBNETS} } (tls-$HOMESERVER_ABBREV) { tls { ca https://$SUB_CADDY.$HOMESERVER_DOMAIN/acme/$HOMESERVER_ABBREV/directory } } (rip-private-tls-$HOMESERVER_ABBREV) { import rip-private import tls-$HOMESERVER_ABBREV } (rip-internal-tls-$HOMESERVER_ABBREV) { import rip-internal import tls-$HOMESERVER_ABBREV } (base-header) { header { Referrer-Policy "strict-origin-when-cross-origin" Strict-Transport-Security "max-age=31536000;" X-XSS-Protection "1; mode=block" X-Content-Type-Options "nosniff" X-Robots-Tag "noindex, nofollow" -Server Cross-Origin-Opener-Policy "same-origin" Permissions-Policy "camera=(self), microphone=(self), geolocation=(self), payment=(), usb=()" } } ($CADDY_SNIPPET_DEFAULTCSP) { header Content-Security-Policy "default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' img.shields.io secure.gravatar.com cdn.libravatar.org seccdn.libravatar.org *.${HOMESERVER_DOMAIN} data:; frame-src 'self' *.${HOMESERVER_DOMAIN} data: blob:; connect-src 'self' *.${HOMESERVER_DOMAIN} wss://*.${HOMESERVER_DOMAIN} data:; object-src 'none'; frame-ancestors 'self' *.${HOMESERVER_DOMAIN}; upgrade-insecure-requests;" } ($CADDY_SNIPPET_RELAXEDCSP) { header Content-Security-Policy "default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' img.shields.io secure.gravatar.com cdn.libravatar.org seccdn.libravatar.org i.ytimg.com *.${HOMESERVER_DOMAIN} data:; frame-src 'self' www.youtube-nocookie.com www.youtube.com *.${HOMESERVER_DOMAIN} data: blob:; connect-src 'self' *.${HOMESERVER_DOMAIN} wss://*.${HOMESERVER_DOMAIN} data:; object-src 'none'; frame-ancestors 'self' *.${HOMESERVER_DOMAIN}; upgrade-insecure-requests;" } (safe-header) { import base-header import $CADDY_SNIPPET_DEFAULTCSP } ($CADDY_SNIPPET_TRUSTEDPROXIES) { trusted_proxies $TRUSTED_PROXIES header_up Host {hostport} } (sn-resolver) { transport http { resolvers {\\\$NET_WEBPROXY_SUBNET_PREFIX}.253:53 } } # This obtains/maintains the external ZeroSSL certificate for the postfix mail relay https://$RELAYSERVER_EXT_EMAIL_HOSTNAME { templates header Content-Type text/plain respond "Your IP Address is {{.RemoteIP}}" } # This provides a way to share files externally # The service is disabled by default. #https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { # import safe-header # reverse_proxy http://filebrowser { # import trusted-proxy-list # } #} # Sample for providing public internet access to a service. # Remove the # from the beginning of each line in the block # below and restart caddy to enable. If adding Mastodon or # Matrix, you must also add a route for the base domain. #https://$SUB_FILEBROWSER.$HOMESERVER_DOMAIN { # import safe-header # reverse_proxy https://$SUB_FILEBROWSER.$HOMESERVER_DOMAIN { # import sn-resolver # } #} # Internal admin services only available on VPN to specific IPs. https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { import rip-private-tls-$HOMESERVER_ABBREV handle @subnet { reverse_proxy https://portainer:9443 { import $CADDY_SNIPPET_TRUSTEDPROXIES } } respond 404 } https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { import rip-private-tls-$HOMESERVER_ABBREV handle @subnet { reverse_proxy https://adguard { import $CADDY_SNIPPET_TRUSTEDPROXIES } } respond 404 } https://$SUB_CADDYDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { import rip-private-tls-$HOMESERVER_ABBREV handle @subnet { reverse_proxy http://caddy-dns:8080 { import $CADDY_SNIPPET_TRUSTEDPROXIES } } respond 404 } https://$SUB_CLIENTDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { import rip-private-tls-$HOMESERVER_ABBREV handle @subnet { reverse_proxy http://clientdns-dnsmasq:8080 { import $CADDY_SNIPPET_TRUSTEDPROXIES } } respond 404 } https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { import rip-private-tls-$HOMESERVER_ABBREV handle @subnet { reverse_proxy http://mail-relay-rspamd { import $CADDY_SNIPPET_TRUSTEDPROXIES } } respond 404 } https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { import rip-private-tls-$HOMESERVER_ABBREV handle @subnet { reverse_proxy https://syncthing:$SYNCTHING_LOCAL_WEB_PORT { import $CADDY_SNIPPET_TRUSTEDPROXIES } } respond 404 } https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN { import rip-private-tls-$HOMESERVER_ABBREV handle @subnet { reverse_proxy http://host.docker.internal:$RELAYSERVER_WG_PORTAL_PORT { import $CADDY_SNIPPET_TRUSTEDPROXIES } } respond 404 } # Add any internal User services here # LetsEncrypt paths for HTTP challenge EOFCA leCertsArr=(\$(echo "$RELAYSERVER_LE_CERT_DOMAINS" | tr "," "\n")) for leCert in "\${leCertsArr[@]}" do echo -e "# LE certs path \${leCert} BEGIN\nhttp://\${leCert} {\n handle /.well-known/acme-challenge/* {\n reverse_proxy \${leCert} {\n import sn-resolver\n }\n }\n}\n# LE certs path \${leCert} END\n" >> \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile done } function installOfelia() { outputConfigOfelia installStack ofelia ofelia " " \$HOME/ofelia.env } function outputConfigOfelia() { cat < \$HOME/ofelia-compose.yml $STACK_VERSION_PREFIX ofelia $(getScriptStackVersion ofelia) services: ofelia: image: $IMG_OFELIA container_name: ofelia hostname: ofelia restart: unless-stopped command: daemon --docker security_opt: - no-new-privileges:true networks: - dock-ext-net volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro labels: - "ofelia.job-local.my-test-job.schedule=@every 5m" - "ofelia.job-local.my-test-job.command=date" networks: dock-ext-net: name: dock-ext external: true EOFOF cat < \$HOME/ofelia.env TZ=\\\${PORTAINER_TZ} EOFFB } function installSyncthing() { mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/syncthing mkdir \$RELAYSERVER_HSHQ_STACKS_DIR/syncthing/config mkdir \$RELAYSERVER_HSHQ_NONBACKUP_DIR/syncthing mkdir \$RELAYSERVER_HSHQ_NONBACKUP_DIR/syncthing/data generateCert syncthing syncthing outputConfigSyncthing cd ~ docker compose -f \$HOME/syncthing-compose-tmp.yml up -d search="Access the GUI via the following URL" isFound="F" i=0 set +e while [ \$i -le 60 ] do findtext=\$(docker logs syncthing 2>&1 | grep "\$search") if ! [ -z "\$findtext" ]; then isFound="T" break fi echo "Container not ready, sleeping 1 second, total wait=\$i seconds..." sleep 1 i=\$((i+1)) done if [ "\$isFound" = "F" ]; then echo "There was a problem installing syncthing" exit fi set -e cd ~ docker compose -f \$HOME/syncthing-compose-tmp.yml down -v sleep 5 rm -f \$HOME/syncthing-compose-tmp.yml alltext=\$(sudo cat \$RELAYSERVER_HSHQ_STACKS_DIR/syncthing/config/config.xml) pwhash=\$(htpasswd -bnBC 10 "" $RELAYSERVER_SYNCTHING_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2a/') replacetext='\n
127.0.0.1:'$SYNCTHING_LOCAL_WEB_PORT'
\n '$RELAYSERVER_SYNCTHING_ADMIN_USERNAME'\n '\$pwhash'\n '$RELAYSERVER_SYNCTHING_API_KEY'\n dark\n
' echo -e "\${alltext%%}" > \$HOME/st_config.xml alltext=\$(cat \$HOME/st_config.xml) replacetext='false' echo -e "\${alltext%%}" > \$HOME/st_config.xml alltext=\$(cat \$HOME/st_config.xml) replacetext='false' echo -e "\${alltext%%}" > \$HOME/st_config.xml alltext=\$(cat \$HOME/st_config.xml) replacetext='false' echo -e "\${alltext%%}" > \$HOME/st_config.xml alltext=\$(cat \$HOME/st_config.xml) replacetext='false' echo -e "\${alltext%%}" > \$HOME/st_config.xml alltext=\$(cat \$HOME/st_config.xml) replacetext='false' echo -e "\${alltext%%}" > \$HOME/st_config.xml chmod 600 \$HOME/st_config.xml sudo chown root:root \$HOME/st_config.xml sudo mv \$HOME/st_config.xml \$RELAYSERVER_HSHQ_STACKS_DIR/syncthing/config/config.xml installStack syncthing syncthing "Access the GUI via the following URL" \$HOME/syncthing.env sleep 3 curl -s -H "X-API-Key: $RELAYSERVER_SYNCTHING_API_KEY" -X DELETE -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/folders/default } function outputConfigSyncthing() { cat < \$HOME/syncthing-compose-tmp.yml services: syncthing: image: $IMG_SYNCTHING container_name: syncthing hostname: syncthing restart: unless-stopped security_opt: - no-new-privileges:true networks: - dock-ext-net ports: - "$SYNCTHING_SYNC_PORT:$SYNCTHING_SYNC_PORT/tcp" - "$SYNCTHING_SYNC_PORT:$SYNCTHING_SYNC_PORT/udp" - "$SYNCTHING_DISC_PORT:$SYNCTHING_DISC_PORT/udp" - "127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT:$SYNCTHING_LOCAL_WEB_PORT" environment: - PUID=0 - PGID=0 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${RELAYSERVER_HSHQ_SSL_DIR}/syncthing.crt:/var/syncthing/config/cert.pem:ro - \${RELAYSERVER_HSHQ_SSL_DIR}/syncthing.key:/var/syncthing/config/key.pem:ro - \${RELAYSERVER_HSHQ_SSL_DIR}/syncthing.crt:/var/syncthing/config/https-cert.pem:ro - \${RELAYSERVER_HSHQ_SSL_DIR}/syncthing.key:/var/syncthing/config/https-key.pem:ro - \${RELAYSERVER_HSHQ_STACKS_DIR}/syncthing/config:/var/syncthing/config - \${RELAYSERVER_HSHQ_NONBACKUP_DIR}/syncthing/data:/var/syncthing/data - \${RELAYSERVER_HSHQ_DATA_DIR}:/relayserver networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFST cat < \$HOME/syncthing-compose.yml $STACK_VERSION_PREFIX syncthing $(getScriptStackVersion syncthing) services: syncthing: image: $IMG_SYNCTHING container_name: syncthing hostname: syncthing restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net ports: - "$SYNCTHING_SYNC_PORT:$SYNCTHING_SYNC_PORT/tcp" - "$SYNCTHING_SYNC_PORT:$SYNCTHING_SYNC_PORT/udp" - "$SYNCTHING_DISC_PORT:$SYNCTHING_DISC_PORT/udp" - "127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT:$SYNCTHING_LOCAL_WEB_PORT" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_SSL_DIR}/syncthing.crt:/var/syncthing/config/cert.pem:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_SSL_DIR}/syncthing.key:/var/syncthing/config/key.pem:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_SSL_DIR}/syncthing.crt:/var/syncthing/config/https-cert.pem:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_SSL_DIR}/syncthing.key:/var/syncthing/config/https-key.pem:ro - \\\${PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR}/syncthing/config:/var/syncthing/config - \\\${PORTAINER_RELAYSERVER_HSHQ_NONBACKUP_DIR}/syncthing/data:/var/syncthing/data - \\\${PORTAINER_RELAYSERVER_HSHQ_DATA_DIR}:/relayserver networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFST cat < \$HOME/syncthing.env PUID=0 PGID=0 STCONFDIR=/var/syncthing/config STDATADIR=/var/syncthing/data STHOMEDIR= EOFST } main "\$@" EOFRS chmod 0600 $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME } function uploadVPNInstallScripts() { if [ -z "$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME" ]; then echo "Generating keys, please wait..." RELAYSERVER_SSH_PRIVATE_KEY_FILENAME=$HOMESERVER_ABBREV".key" updateConfigVar RELAYSERVER_SSH_PRIVATE_KEY_FILENAME $RELAYSERVER_SSH_PRIVATE_KEY_FILENAME rm -f $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME rm -f $HSHQ_CONFIG_DIR/${RELAYSERVER_SSH_PRIVATE_KEY_FILENAME}.pub RELAYSERVER_SSH_PRIVATE_KEY_PHRASE=$(pwgen -c -n 64 1) updateConfigVar RELAYSERVER_SSH_PRIVATE_KEY_PHRASE $RELAYSERVER_SSH_PRIVATE_KEY_PHRASE ssh-keygen -f $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME -N $RELAYSERVER_SSH_PRIVATE_KEY_PHRASE chmod 0400 $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME chmod 0400 $HSHQ_CONFIG_DIR/${RELAYSERVER_SSH_PRIVATE_KEY_FILENAME}.pub fi set +e echo "Checking known_hosts..." if ! [ -z "$RELAYSERVER_SSH_PORT" ]; then echo "Removing old known_hosts entry: [$RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$RELAYSERVER_SSH_PORT" ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1 if ! [ -z "$RELAYSERVER_SERVER_IP" ] && [ "$(checkValidIPAddress $RELAYSERVER_SERVER_IP)" = "true" ]; then echo "Removing old known_hosts entry: [$RELAYSERVER_SERVER_IP]:$RELAYSERVER_SSH_PORT" ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SERVER_IP]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1 fi fi is_err=0 err_message="unknown" while true; do if [ $is_err -ne 0 ]; then errmenu=$(cat << EOF $(getLogo) There is a problem with the RelayServer host: ----------------------------------------------------------------- $err_message ----------------------------------------------------------------- Please address this issue, then press Retry to proceed or Cancel to exit. EOF ) if ! (whiptail --title "Login Error" --yesno "$errmenu" $MENU_HEIGHT $MENU_WIDTH --yes-button "Retry" --no-button "Cancel"); then return 1 fi fi is_err=0 err_message="unknown" rs_new_username="" rs_cur_password="" trUsername=$RELAYSERVER_REMOTE_USERNAME rs_cur_username=$(promptUserInputMenu "root" "Enter Username" "Enter the CURRENT Linux OS username for the RelayServer host. A fresh installation will typically default to root: ") if [ $? -ne 0 ]; then return 1 fi if [ $(checkValidString "$rs_cur_username" "-") = "false" ]; then showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or hyphens" rs_cur_username="" continue fi if [ "$rs_cur_username" = "root" ]; then rs_cur_password=$(promptPasswordMenu "Enter Password" "Enter the password for your RelayServer Linux OS root account: ") if [ $(checkValidPassword "$rs_cur_password") = "false" ]; then showMessageBox "ERROR" "The password is invalid . It must consist of uppercase/lowercase letters, numbers, or the following symbols: ~!@#%^&*()-_+=<>?., Please change your root password on the remote host to fit this set in order to continue." continue fi while [ -z "$rs_new_username" ] do rs_new_username=$(promptUserInputMenu "$USERNAME" "Enter New Username" "Enter a NEW Linux OS username to add (in place of root): ") if [ $? -ne 0 ]; then return 1 fi if [ $(checkValidString "$rs_new_username" "-") = "false" ]; then showMessageBox "Invalid Character(s)" "The name contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or hyphens" rs_new_username="" fi done RELAYSERVER_REMOTE_USERNAME="$rs_new_username" else RELAYSERVER_REMOTE_USERNAME="$rs_cur_username" fi tmp_pw1="" tmp_pw2="" while [ -z "$tmp_pw1" ] || ! [ "$tmp_pw1" = "$tmp_pw2" ] do if ! [ -z "$rs_new_username" ]; then tmp_pw1=$(promptPasswordMenu "Create Password" "Create a password for your RelayServer Linux OS user ($RELAYSERVER_REMOTE_USERNAME) account: ") if [ $? -ne 0 ]; then return 1 fi if [ -z "$tmp_pw1" ]; then showMessageBox "Password Empty" "The password cannot be empty, please try again." continue fi if [ $(checkValidPassword "$tmp_pw1" 16) = "false" ]; then showMessageBox "Weak Password" "The password is invalid or is too weak. It must contain at least 16 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.," tmp_pw1="" tmp_pw2="" continue fi tmp_pw2=$(promptPasswordMenu "Confirm Password" "Enter the password again to confirm: ") if [ $? -ne 0 ]; then return 1 fi if ! [ "$tmp_pw1" = "$tmp_pw2" ]; then showMessageBox "Password Mismatch" "The passwords do not match, please try again." fi else tmp_pw1=$(promptPasswordMenu "Enter Password" "Enter the password for your RelayServer Linux OS user ($RELAYSERVER_REMOTE_USERNAME) account: ") if [ $? -ne 0 ]; then return 1 fi if [ -z "$tmp_pw1" ]; then showMessageBox "Password Empty" "The password cannot be empty, please try again." continue fi tmp_pw2=$tmp_pw1 fi done USER_RELAY_SUDO_PW="$tmp_pw1" tmp_pw1="" tmp_pw2="" domain_ip_guess=$(getIPFromHostname ip.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN) if [ -z "$domain_ip_guess" ]; then domain_ip_guess="0.0.0.0" fi RELAYSERVER_SERVER_IP="" while [ -z "$RELAYSERVER_SERVER_IP" ] do RELAYSERVER_SERVER_IP=$(promptUserInputMenu $domain_ip_guess "Enter IP Address" "Enter the IP Address of the RelayServer. It is IMPORTANT that you enter this value correctly. If you have already pointed your DNS of your HomeServer domain to your RelayServer, then ensure the guessed IP is correct.") if [ $? -ne 0 ]; then return 1 fi if [ -z "$RELAYSERVER_SERVER_IP" ]; then showMessageBox "IP Address Empty" "The IP Address cannot be empty" elif [ "$(checkValidIPAddress $RELAYSERVER_SERVER_IP)" = "false" ] || [ "$RELAYSERVER_SERVER_IP" = "0.0.0.0" ]; then showMessageBox "Invalid Character(s)" "The IP address contains invalid character(s)." RELAYSERVER_SERVER_IP="" fi done RELAYSERVER_CURRENT_SSH_PORT="" while [ -z "$RELAYSERVER_CURRENT_SSH_PORT" ] do RELAYSERVER_CURRENT_SSH_PORT=$(promptUserInputMenu "22" "Enter CURRENT SSH Port" "Enter the CURRENT SSH port for the RelayServer host, (a fresh installation defaults to port 22): ") if [ -z "$RELAYSERVER_CURRENT_SSH_PORT" ]; then showMessageBox "SSH Port Empty" "The SSH port cannot be empty" fi done set +e if [ -z "$rs_new_username" ]; then rs_new_username="$RELAYSERVER_REMOTE_USERNAME" rs_cur_password="$USER_RELAY_SUDO_PW" fi echo "Logging into RelayServer..." perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -o "-T -o 'StrictHostKeyChecking accept-new' -o ConnectTimeout=10" -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c "echo hello >/dev/null" -f is_err=$? if [ $is_err -ne 0 ]; then err_message="Could not log in to RelayServer. It could be wrong IP address/port and/or incorrect credentials. Please check your records and try again." continue fi # 2. Upload check script outputRelayServerValidationScript echo "Uploading validation script..." perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -a $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c ":~" -f is_err=$? rm -f $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME if [ $is_err -ne 0 ]; then err_message="There was an error uploading the validation script to the RelayServer." continue fi echo "Uploading validation setup script..." perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c ":~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME" -f is_err=$? if [ $is_err -ne 0 ]; then err_message="There was an error uploading the setup script to the RelayServer." continue fi echo "Performing pre-installation checks, please wait..." set -o pipefail perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -o "-T -o 'StrictHostKeyChecking accept-new' -o ConnectTimeout=10" -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c "bash ~/$RS_INSTALL_VALIDATION_SCRIPT_NAME -s" -f -i "$USER_RELAY_SUDO_PW" | tee /tmp/rsValidationOutput.txt is_err=$? set +o pipefail if [ $is_err -ne 0 ]; then err_message="$(cat /tmp/rsValidationOutput.txt)" rm -f /tmp/rsValidationOutput.txt continue fi if [ $is_err -eq 0 ]; then break fi done if [ -f /tmp/rsValidationOutput.txt ]; then cat /tmp/rsValidationOutput.txt fi rm -f /tmp/rsValidationOutput.txt refreshSudo rs_cur_password="" updateConfigVar RELAYSERVER_CURRENT_SSH_PORT $RELAYSERVER_CURRENT_SSH_PORT updateConfigVar RELAYSERVER_SERVER_IP $RELAYSERVER_SERVER_IP addHomeNetIP ${RELAYSERVER_SERVER_IP}/32 true if [ "$IS_INSTALLED" = "true" ]; then addDomainAdguardHS "*.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "$RELAYSERVER_SERVER_IP" fi updateConfigVar RELAYSERVER_REMOTE_USERNAME $RELAYSERVER_REMOTE_USERNAME RELAYSERVER_HSHQ_BASE_DIR=/home/$RELAYSERVER_REMOTE_USERNAME/hshq RELAYSERVER_HSHQ_DATA_DIR=$RELAYSERVER_HSHQ_BASE_DIR/data RELAYSERVER_HSHQ_NONBACKUP_DIR=$RELAYSERVER_HSHQ_BASE_DIR/nonbackup RELAYSERVER_HSHQ_SCRIPTS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/scripts RELAYSERVER_HSHQ_SECRETS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/secrets RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/stacks RELAYSERVER_HSHQ_SSL_DIR=$RELAYSERVER_HSHQ_DATA_DIR/ssl updateConfigVar RELAYSERVER_HSHQ_BASE_DIR $RELAYSERVER_HSHQ_BASE_DIR updateConfigVar RELAYSERVER_HSHQ_DATA_DIR $RELAYSERVER_HSHQ_DATA_DIR updateConfigVar RELAYSERVER_HSHQ_NONBACKUP_DIR $RELAYSERVER_HSHQ_NONBACKUP_DIR updateConfigVar RELAYSERVER_HSHQ_SCRIPTS_DIR $RELAYSERVER_HSHQ_SCRIPTS_DIR updateConfigVar RELAYSERVER_HSHQ_SECRETS_DIR $RELAYSERVER_HSHQ_SECRETS_DIR updateConfigVar RELAYSERVER_HSHQ_STACKS_DIR $RELAYSERVER_HSHQ_STACKS_DIR updateConfigVar RELAYSERVER_HSHQ_SSL_DIR $RELAYSERVER_HSHQ_SSL_DIR perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "touch ~/$RELAYSERVER_SCRIPTS_UPLOADED_FILE" -f } function loadSSHKey() { set +e ssh-add -L > /dev/null 2>&1 if [ $? -eq 2 ]; then eval "$(ssh-agent)" fi set -e cat < $HSHQ_CONFIG_DIR/askpass #!/bin/bash echo $RELAYSERVER_SSH_PRIVATE_KEY_PHRASE EOFAP chmod u+x $HSHQ_CONFIG_DIR/askpass DISPLAY=1 SSH_ASKPASS="$HSHQ_CONFIG_DIR/askpass" ssh-add $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME < /dev/null rm $HSHQ_CONFIG_DIR/askpass } function unloadSSHKey() { ssh-add -D 2> /dev/null } function connectPrimaryVPN() { is_prompt_user="$1" db_id=$(getPrimaryVPN_DBID) if ! [ -z "$db_id" ]; then connectVPN $db_id "$is_prompt_user" fi } function getPrimaryVPN_DBID() { echo $(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='primary';") } function getPrimaryVPN_IP() { prim_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ConnectionType='homeserver_vpn' and NetworkType='primary';") } function connectPrimaryInternet() { db_id=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_internet' and NetworkType='primary';") if [ -z "$db_id" ]; then return fi ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") dockerSubnet=$(getConfigVarFromFile \#DOCKER_NETWORK_SUBNET $HSHQ_WIREGUARD_DIR/internet/${ifaceName}.conf root) if [ -z "$dockerSubnet" ]; then dockerNetworkName=$(getConfigVarFromFile \#DOCKER_NETWORK_NAME $HSHQ_WIREGUARD_DIR/internet/${ifaceName}.conf root) conn_mtu=$(getConfigVarFromFile \#MTU $HSHQ_WIREGUARD_DIR/internet/${ifaceName}.conf root) dockerSubnet=$(getNextAvailableWGDockerNetwork $dockerNetworkName $conn_mtu) sudo sed -i "s|^#DOCKER_NETWORK_SUBNET.*|#DOCKER_NETWORK_SUBNET=$dockerSubnet|g" $HSHQ_WIREGUARD_DIR/internet/${ifaceName}.conf fi connectInternet $db_id } function connectVPN() { echo "Connecting to RelayServer via WireGuard..." # Configuration files of this type are expected to be located in /etc/wireguard db_id=$1 is_prompt_user="$2" if [ -z "$db_id" ]; then echo "ERROR: No database ID provided." return fi client_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$db_id;") ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") hs_name=$(sqlite3 $HSHQ_DB "select HomeServerName from hsvpn_connections where ID=$db_id;") ext_prefix=$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$db_id;") int_prefix=$(sqlite3 $HSHQ_DB "select InternalPrefix from hsvpn_connections where ID=$db_id;") ca_abbrev=$(sqlite3 $HSHQ_DB "select CA_Abbrev from hsvpn_connections where ID=$db_id;") domain_name=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$db_id;") ca_ip=$(sqlite3 $HSHQ_DB "select CA_IP from hsvpn_connections where ID=$db_id;") ca_subdomain=$(sqlite3 $HSHQ_DB "select CA_Subdomain from hsvpn_connections where ID=$db_id;") ca_url=$(sqlite3 $HSHQ_DB "select CA_URL from hsvpn_connections where ID=$db_id;") vpn_subnet=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections where ID=$db_id;") rs_vpn_ip=$(sqlite3 $HSHQ_DB "select RS_VPN_IP from hsvpn_connections where ID=$db_id;") is_primary=$(sqlite3 $HSHQ_DB "select IsPrimary from hsvpn_connections where ID=$db_id;") if [ $is_primary = 1 ] && [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then set +e while true; do max_attempts=20 total_attempts=1 isBreak=false while [ $total_attempts -le $max_attempts ] do perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "echo \"Logged in to RelayServer...\"; if test -f ~/$RELAYSERVER_INSTALL_FAILED_FILE; then exit 23; fi; test -f ~/hshq/$RELAYSERVER_INSTALL_COMPLETE_FILE && rm -f ~/hshq/$RELAYSERVER_INSTALL_COMPLETE_FILE > /dev/null 2>&1" -r 1 -b 1 rtVal=$? if [ $rtVal -eq 0 ]; then isBreak=true break elif [ $rtVal -eq 23 ]; then echo "************************************************************************************" echo " ERROR: The RelayServer installation failed to complete. Please remove it and retry." echo "************************************************************************************" return fi if [ $total_attempts -lt 10 ]; then echo "($total_attempts of $max_attempts) RelayServer installation has not completed, retrying in 30 seconds..." else echo "($total_attempts of $max_attempts) RelayServer installation has not completed, it could be a firewall issue with the VPS provider. Ensure that the port $RELAYSERVER_SSH_PORT (SSH) is open and accessible. Retrying in 30 seconds..." fi refreshSudo sleep 30 total_attempts=$((total_attempts + 1)) done if ! [ "$isBreak" = "true" ]; then if [ "$is_prompt_user" = "true" ]; then echo "ERROR: Could not log in to RelayServer to setup Syncthing. It could be an external firewall by the VPS provider or some other unknown network connectivity issue. If this step is skipped, you will have to manually set up the RelayServer backup in Syncthing." while true; do read -r -p "Enter 'retry' or 'cancel': " isRetryConnect case "$isRetryConnect" in "retry") break ;; "cancel") isBreak=true break ;; *) echo "Unknown response..." ;; esac done else echo "ERROR: Could not log in to RelayServer to setup Syncthing. Something likely went wrong with the RelayServer installation. You might have to entirely remove the Primary VPN and start over (In Script-server: 06 My Network -> 15 Remove Primary VPN)." isBreak=true fi fi if [ "$isBreak" = "true" ]; then break fi done fi enableWGInterfaceQuick $ifaceName # If not hosting or non-primary VPN, then add CA domain. if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] || ! [ $is_primary = 1 ]; then addDomainAndWildcardAdguardNoReplaceHS "$domain_name" "$ca_ip" addDomainAdguardHS "*.$ext_prefix.$domain_name" "A" addDomainAndWildcardIgnoreQuerylogAndStatsHS "$domain_name" insertEnableSvcUptimeKuma uptimekuma "${hs_name}" homeservers "https://$SUB_HSHQSTATUS.$domain_name" true fi addDomainAdguardHS "*.$int_prefix.$domain_name" "$rs_vpn_ip" primary_string=other if [ $is_primary = 1 ]; then primary_string=primary if [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then restartAllCaddyContainers fi fi updateSvcsIPChanges # Add new Caddy container. installCaddy $ifaceName $primary_string $ifaceName $client_ip $ca_abbrev $ca_url $ca_subdomain $ca_ip if [ $is_primary = 1 ] && [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then updateMailuStackRelayHost fi # If hosting VPN, add ClientDNS stack and setup Syncthing if [ $is_primary = 1 ] && [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then set +e echo "Installing ClientDNS..." installClientDNS user1 $RELAYSERVER_WG_HS_CLIENTDNS_IP "$CLIENTDNS_USER1_ADMIN_USERNAME" "$CLIENTDNS_USER1_ADMIN_PASSWORD" echo "Sleeping for 5 seconds..." sleep 5 # Setup syncthing link echo "Setting up Syncthing..." SYNCTHING_DEVICE_ID=$(curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X GET -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices | jq '.[0]' | jq -r '.deviceID') updateConfigVar SYNCTHING_DEVICE_ID $SYNCTHING_DEVICE_ID # Remote Syncthing echo "Getting RelayServer syncthing device ID..." rsCaptureResults=$(perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "curl -s -H \"X-API-Key: $RELAYSERVER_SYNCTHING_API_KEY\" -X GET -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices | jq '.[0]' | jq -r '.deviceID'" -f -d) if [ $? -ne 0 ]; then echo "There was an error obtaining the remote RelayServer Synchthing device ID. This is likely due to an issue with your internet connection. You can either manually set up Syncthing to backup your RelayServer's data or remove the VPN and retry." set -e updateHeimdallUptimeKumaRelayServer return fi RELAYSERVER_SYNCTHING_DEVICE_ID="$rsCaptureResults" updateConfigVar RELAYSERVER_SYNCTHING_DEVICE_ID $RELAYSERVER_SYNCTHING_DEVICE_ID jsonbody="{\\\"deviceID\\\": \\\"$SYNCTHING_DEVICE_ID\\\", \\\"name\\\": \\\"$HOMESERVER_NAME HomeServer\\\",\\\"addresses\\\": [\\\"tcp://$SUB_SYNCTHING.$HOMESERVER_DOMAIN:$SYNCTHING_SYNC_PORT\\\"], \\\"compression\\\": \\\"metadata\\\", \\\"certName\\\": \\\"\\\", \\\"skipIntroductionRemovals\\\": false,\\\"introducedBy\\\": \\\"\\\",\\\"paused\\\": false,\\\"allowedNetworks\\\": [],\\\"autoAcceptFolders\\\": false,\\\"maxSendKbps\\\": 0,\\\"maxRecvKbps\\\": 0,\\\"ignoredFolders\\\": [],\\\"maxRequestKiB\\\": 0,\\\"untrusted\\\": false,\\\"remoteGUIPort\\\": 0}" echo "Adding HomeServer device to RelayServer syncthing..." perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "curl -s -H \"X-API-Key: $RELAYSERVER_SYNCTHING_API_KEY\" -X POST -d \"$jsonbody\" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices" -f if [ $? -ne 0 ]; then echo "There was an error setting up Syncthing on the remote RelayServer instance. This is likely due to an issue with your internet connection. You can either manually set up Syncthing to backup your RelayServer's data or remove the VPN and retry." set -e updateHeimdallUptimeKumaRelayServer return fi jsonbody="{\\\"id\\\": \\\"$RELAYSERVER_SYNCTHING_FOLDER_ID\\\", \\\"label\\\": \\\"RelayServer Backup\\\",\\\"filesystemType\\\": \\\"basic\\\", \\\"path\\\": \\\"/relayserver/\\\", \\\"type\\\": \\\"sendonly\\\",\\\"devices\\\": [{\\\"deviceID\\\": \\\"$RELAYSERVER_SYNCTHING_DEVICE_ID\\\",\\\"introducedBy\\\": \\\"\\\",\\\"encryptionPassword\\\": \\\"\\\"},{\\\"deviceID\\\": \\\"$SYNCTHING_DEVICE_ID\\\",\\\"introducedBy\\\": \\\"\\\",\\\"encryptionPassword\\\": \\\"\\\"}], \\\"rescanIntervalS\\\": 3600,\\\"fsWatcherEnabled\\\": true,\\\"fsWatcherDelayS\\\": 10,\\\"ignorePerms\\\": false,\\\"autoNormalize\\\": true,\\\"minDiskFree\\\": {\\\"value\\\": 1,\\\"unit\\\": \\\"%\\\"},\\\"versioning\\\": {\\\"type\\\": \\\"\\\",\\\"params\\\": {},\\\"cleanupIntervalS\\\": 3600,\\\"fsPath\\\": \\\"\\\",\\\"fsType\\\": \\\"basic\\\"},\\\"copiers\\\": 0,\\\"pullerMaxPendingKiB\\\": 0,\\\"hashers\\\": 0,\\\"order\\\": \\\"random\\\",\\\"ignoreDelete\\\": false,\\\"scanProgressIntervalS\\\": 0,\\\"pullerPauseS\\\": 0,\\\"maxConflicts\\\": 10,\\\"disableSparseFiles\\\": false,\\\"disableTempIndexes\\\": false,\\\"paused\\\": false,\\\"weakHashThresholdPct\\\": 25,\\\"markerName\\\": \\\".stfolder\\\",\\\"copyOwnershipFromParent\\\": false,\\\"modTimeWindowS\\\": 0,\\\"maxConcurrentWrites\\\": 2,\\\"disableFsync\\\": false,\\\"blockPullOrder\\\": \\\"standard\\\",\\\"copyRangeMethod\\\": \\\"standard\\\",\\\"caseSensitiveFS\\\": false,\\\"junctionsAsDirs\\\": false,\\\"syncOwnership\\\": false,\\\"sendOwnership\\\": true,\\\"syncXattrs\\\": false,\\\"sendXattrs\\\": false,\\\"xattrFilter\\\": {\\\"entries\\\": [],\\\"maxSingleEntrySize\\\": 1024,\\\"maxTotalSize\\\": 4096}}" echo "Setting up backup directory on RelayServer syncthing..." perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "curl -s -H \"X-API-Key: $RELAYSERVER_SYNCTHING_API_KEY\" -X POST -d \"$jsonbody\" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/folders" -f if [ $? -ne 0 ]; then echo "There was an error configuring Syncthing on the remote RelayServer instance. This is likely due to an issue with your internet connection. You can either manually set up Syncthing to backup your RelayServer's data or remove the VPN and retry." set -e updateHeimdallUptimeKumaRelayServer return fi # Local Syncthing jsonbody="{\"deviceID\": \"$RELAYSERVER_SYNCTHING_DEVICE_ID\", \"name\": \"$RELAYSERVER_NAME\",\"addresses\": [\"tcp://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN:$SYNCTHING_SYNC_PORT\"], \"compression\": \"metadata\", \"certName\": \"\", \"skipIntroductionRemovals\": false,\"introducedBy\": \"\",\"paused\": false,\"allowedNetworks\": [],\"autoAcceptFolders\": false,\"maxSendKbps\": 0,\"maxRecvKbps\": 0,\"ignoredFolders\": [],\"maxRequestKiB\": 0,\"untrusted\": false,\"remoteGUIPort\": 0}" echo "Setting up RelayServer device on HomeServer syncthing..." if [ "$LOG_LEVEL" = "debug" ]; then echo -e "Running this command on HomeServer:" echo -e "++++++++++++++++++++++++++++++++++++++++++++++++++\n" echo "curl -s -H \"X-API-Key: $SYNCTHING_API_KEY\" -X POST -d \"$jsonbody\" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices" echo -e " \n++++++++++++++++++++++++++++++++++++++++++++++++++\n" fi curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X POST -d "$jsonbody" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices jsonbody="{\"id\": \"$RELAYSERVER_SYNCTHING_FOLDER_ID\", \"label\": \"RelayServer Backup\",\"filesystemType\": \"basic\", \"path\": \"/relayserver/\", \"type\": \"receiveonly\",\"devices\": [{\"deviceID\": \"$SYNCTHING_DEVICE_ID\",\"introducedBy\": \"\",\"encryptionPassword\": \"\"},{\"deviceID\": \"$RELAYSERVER_SYNCTHING_DEVICE_ID\",\"introducedBy\": \"\",\"encryptionPassword\": \"\"}], \"rescanIntervalS\": 86400,\"fsWatcherEnabled\": false,\"fsWatcherDelayS\": 10,\"ignorePerms\": false,\"autoNormalize\": true,\"minDiskFree\": {\"value\": 1,\"unit\": \"%\"},\"versioning\": {\"type\": \"\",\"params\": {},\"cleanupIntervalS\": 3600,\"fsPath\": \"\",\"fsType\": \"basic\"},\"copiers\": 0,\"pullerMaxPendingKiB\": 0,\"hashers\": 0,\"order\": \"random\",\"ignoreDelete\": false,\"scanProgressIntervalS\": 0,\"pullerPauseS\": 0,\"maxConflicts\": 10,\"disableSparseFiles\": false,\"disableTempIndexes\": false,\"paused\": false,\"weakHashThresholdPct\": 25,\"markerName\": \".stfolder\",\"copyOwnershipFromParent\": false,\"modTimeWindowS\": 0,\"maxConcurrentWrites\": 2,\"disableFsync\": false,\"blockPullOrder\": \"standard\",\"copyRangeMethod\": \"standard\",\"caseSensitiveFS\": false,\"junctionsAsDirs\": false,\"syncOwnership\": true,\"sendOwnership\": false,\"syncXattrs\": false,\"sendXattrs\": false,\"xattrFilter\": {\"entries\": [],\"maxSingleEntrySize\": 1024,\"maxTotalSize\": 4096}}" echo "Setting up backup directory on HomeServer syncthing..." if [ "$LOG_LEVEL" = "debug" ]; then echo -e "Running this command on HomeServer:" echo -e "++++++++++++++++++++++++++++++++++++++++++++++++++\n" echo "curl -s -H \"X-API-Key: $SYNCTHING_API_KEY\" -X POST -d \"$jsonbody\" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/folders" echo -e " \n++++++++++++++++++++++++++++++++++++++++++++++++++\n" fi curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X POST -d "$jsonbody" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/folders echo "Restarting caddy instance on RelayServer..." perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "docker container restart caddy" -f if [ $? -ne 0 ]; then echo "There was an error restarting caddy on the remote RelayServer instance. This is likely due to an issue with your internet connection. This is not a breaking issue, it will likely resolve itself automatically." set -e updateHeimdallUptimeKumaRelayServer return fi echo "Finished setting up syncthing..." set -e curdt=$(getCurrentDate) sudo sqlite3 $HSHQ_DB "update connections set LastUpdated='$curdt' where ID=$db_id;" updateHeimdallUptimeKumaRelayServer fi } function connectInternet() { db_id=$1 ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${ifaceName}.conf up } function resetRelayServerData() { sudo rm -fr $HSHQ_RELAYSERVER_DIR/backup/* sudo rm -fr $HSHQ_RELAYSERVER_DIR/scripts/* sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from connections where NetworkType in ('relayserver','primary','mynetwork');" updateDeviceIPMappingFile } function createOrJoinPrimaryVPN() { if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] || [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then is_remove=$(promptUserInputMenu "" "Existing RelayServer" "There is already a RelayServer set up. If you wish to remove all connections/data, enter the word 'remove' below:") if ! [ "$is_remove" = "remove" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return 0 fi set +e disconnect_reason=$(promptUserInputMenu "" "Enter Reason" "Enter a reason for removal/disconnect: ") removeMyNetworkPrimaryVPN "$disconnect_reason" fi vpnmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option for your PRIMARY VPN connection" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Host a VPN" \ "2" "Join a VPN" \ "3" "Exit" 3>&1 1>&2 2>&3) case $menures in 1) confirmHost=$(promptUserInputMenu "" "Confirmation" "This function will set up a hosted VPN. Ensure you have the RelayServer ready and you can readily log into it. Enter the word 'confirm' below:") if ! [ "$confirmHost" = "confirm" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return 0 fi PRIMARY_VPN_SETUP_TYPE=host updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE setupHostedVPN if [ $? -ne 0 ]; then return 0 fi rs_wg_user_conf=$(sudo cat $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf) # Set env vars in mailu stack set +e updateMailuStackRelayHost email_msg="Your new RelayServer has been configured. Below is your first WireGuard User Connection Info:\n\n================ WireGuard Configuration ================\n${rs_wg_user_conf}\n=======================================================\n" sendEmail -s "New RelayServer" -b "$email_msg" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" clear echo -e "\n\n$(getDNSRecordsInfo $HOMESERVER_DOMAIN)\n\n" echo -e "============= User WireGuard Configuration ============\n" echo -e """""$rs_wg_user_conf""""\n" echo -e "=======================================================\n" echo -e "_______________________________________________________" echo -e "The installation script has been uploaded. Please log in to" echo -e "the RelayServer with your newly created username ($RELAYSERVER_REMOTE_USERNAME)," echo -e "and perform the installation by entering the command" echo -e "'bash $RS_INSTALL_FRESH_SCRIPT_NAME', and follow the prompts.\n" echo -e "Ensure to copy your new WireGuard configuration above for your first user.\n" echo -e "AFTER the RelayServer has completed the installation, and AFTER the server" echo -e "has FULLY rebooted, enter 'integrate' to finish the integration. When this" echo -e "process completes, and you are returned to the main menu, then you can" echo -e "safely activate your new WireGuard connection to access your HomeServer" echo -e "network and masquerade your IP address.\n" echo -e "Check your admin email ($EMAIL_ADMIN_EMAIL_ADDRESS) for a copy of the WireGuard config info." while true; do read -r -p "Enter 'integrate' to complete the integration (no quotes, all lowercase): " is_continue if [ "$is_continue" = "integrate" ]; then break fi done connectPrimaryInternet connectPrimaryVPN true ;; 2) confirmJoin=$(promptUserInputMenu "" "Confirmation" "This function will prepare you server to join another hosted VPN as your primary. Enter the word 'confirm' below:") if ! [ "$confirmJoin" = "confirm" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return 0 fi PRIMARY_VPN_SETUP_TYPE=join updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE setupJoinPrimaryVPN join_res=$? if [ $join_res -ne 0 ]; then return fi connectPrimaryVPN true email_msg="Your joined RelayServer has been configured, please update your DNS MX record with your domain name provider to the new mail server: $RELAYSERVER_EXT_EMAIL_HOSTNAME\n\n" # Set env vars in mailu stack set +e sendEmail -s "New RelayServer" -b "$email_msg" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" showMessageBox "Joined RelayServer" "The RelayServer has been joined. Check your email ($EMAIL_ADMIN_EMAIL_ADDRESS) for further details." ;; 3) return ;; esac } function showRemovePrimaryVPN() { if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] || [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then is_remove=$(promptUserInputMenu "" "Remove RelayServer" "If you wish to remove all connections/data, enter the word 'remove' below:") if [ -z "$is_remove" ] || ! [ "$is_remove" = "remove" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return 0 fi set +e disconnect_reason=$(promptUserInputMenu "" "Enter Reason" "Enter a reason for removal/disconnect: ") removeMyNetworkPrimaryVPN "$disconnect_reason" else showMessageBox "Invalid Selection" "You are not currently connected to a RelayServer, returning..." fi } # VPN Menus function showNetworkMenu() { networkmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$networkmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "My Network" \ "2" "Other Networks" \ "3" "RelayServer Utils" \ "4" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then showMessageBox "No RelayServer" "You are not hosting a RelayServer, returning..." return fi showMyNetworkMenu ;; 2) refreshSudo showOtherNetworksMenu ;; 3) showRelayServerUtilsMenu ;; *) return ;; esac } function showMyNetworkMenu() { networkmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$networkmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Invite to My Network" \ "2" "Remove from My Network" \ "3" "My Network Utils" \ "4" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) createMyNetworkInvite ;; 2) showMyNetworkRemoveMenu ;; 3) showMyNetworkUtilsMenu ;; *) return ;; esac } function showOtherNetworksMenu() { networkmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$networkmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Apply to Another Network" \ "2" "Join Another Network" \ "3" "Disconnect From Another Network" \ "4" "Other Network Utils" \ "5" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) showOtherNetworkApplyMenu ;; 2) showOtherNetworkJoinMenu ;; 3) showOtherNetworkDisconnectMenu ;; 4) showOtherNetworkUtilsMenu ;; *) return ;; esac } function showRelayServerUtilsMenu() { netutilmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$netutilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Add Domain(s) To RelayServer" \ "2" "Remove Domain(s) From RelayServer" \ "3" "Add Subdomain to be managed by LetsEncrypt" \ "4" "Create or Join a VPN RelayServer" \ "5" "Transfer RelayServer" \ "6" "Remove VPN RelayServer" \ "7" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) addDomainsToRelayServer ;; 2) removeDomainsFromRelayServer ;; 3) addLECertPathsToRelayServerMsgbox ;; 4) #createOrJoinPrimaryVPN showMessageBox "Deprecated" "This function is now only accessible via Script-server (https://$SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN)." ;; 5) #transferHostedVPN showMessageBox "Deprecated" "This function is outdated and unsupported at the moment. If you need to transfer your RelayServer, then make a feature request on the forum (https://forum.homeserverhq.com)." ;; 6) showRemovePrimaryVPN ;; *) return ;; esac } function showMyNetworkRemoveMenu() { vpnmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "HomeServer VPN" \ "2" "HomeServer Internet" \ "3" "User" \ "4" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) showMyNetworkRemoveVPNMenu ;; 2) showMyNetworkRemoveInternetMenu ;; 3) showMyNetworkRemoveUserMenu ;; *) return ;; esac } function showMyNetworkUtilsMenu() { netutilmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$netutilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Email RemoteServers DNS List" \ "2" "Email Users DNS List" \ "3" "Create ClientDNS Server" \ "4" "Remove ClientDNS Server" \ "5" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) showEmailMyNetworkHomeServerDNSListMenu ;; 2) showEmailMyNetworkHomeServerDNSListClientDNSMenu ;; 3) showMyNetworkCreateClientDNSMenu ;; 4) showMyNetworkRemoveClientDNSMenu ;; *) return ;; esac } function showOtherNetworkApplyMenu() { vpnmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "HomeServer VPN (Producer)" \ "2" "HomeServer Internet (Consumer)" \ "3" "User (Consumer)" \ "4" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) createOtherNetworkApplyHomeServerVPNConfig ;; 2) createOtherNetworkApplyHomeServerInternetConfig ;; 3) createOtherNetworkApplyUserConfig ;; *) return ;; esac } function showOtherNetworkJoinMenu() { vpnmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "VPN" \ "2" "Internet" \ "3" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) createNetworkJoin true if [ $? -ne 0 ]; then return fi ;; 2) createNetworkJoin true if [ $? -ne 0 ]; then return fi ;; *) return ;; esac } function showOtherNetworkDisconnectMenu() { vpnmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "VPN" \ "2" "Internet" \ "3" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) showOtherNetworkDisconnectVPNMenu ;; 2) showOtherNetworkDisconnectInternetMenu ;; *) return ;; esac } function showOtherNetworkUtilsMenu() { utilsmenu=$(cat << EOF $(getLogo) EOF ) menures=$(whiptail --title "Select an option" --menu "$utilsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \ "1" "Update HomeServer DNS" \ "2" "Sync Adguard DNS Server from DB" \ "3" "Exit" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then menures=0 fi case $menures in 1) showUpdateHomeServerDNSMenu ;; 2) syncAdguardDNSFromDB ;; *) return ;; esac } function showMyNetworkRemoveVPNMenu() { vpnremovemenu=$(cat << EOF $(getLogo) Select the network connections that you wish to remove: EOF ) vpn_arr=($(sqlite3 $HSHQ_DB "select ID,Name from connections where ConnectionType='homeserver_vpn' and NetworkType='mynetwork';")) if [ -z "$vpn_arr" ]; then showMessageBox "Empty" "There are no eligible connections for this selection, returning..." return fi menu_items="" for curvpn in "${vpn_arr[@]}" do menu_items=${menu_items}"$(echo "("$(echo $curvpn | sed 's/|/)/1')) | OFF " done sel_vpn=($(whiptail --title "Select Network Connections" --checklist "$vpnremovemenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) is_remove=$(promptUserInputMenu "" "Confirm Removal" "This will remove all selected network connections. To confirm, enter the word 'remove' below:") if [ $? -ne 0 ] || [ -z "$is_remove" ] || ! [ "$is_remove" = "remove" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi removal_reason=$(promptUserInputMenu "" "Removal Reason" "Enter a reason for removal: ") for curvpn in "${sel_vpn[@]}" do curvpnID=$(echo $curvpn | cut -d ")" -f1 | sed 's/(//' | sed 's/"//g') removeMyNetworkHomeServerVPNConnection $curvpnID "$removal_reason" done } function showMyNetworkRemoveInternetMenu() { vpnremovemenu=$(cat << EOF $(getLogo) Select the network connections that you wish to remove: EOF ) vpn_arr=($(sqlite3 $HSHQ_DB "select ID,Name from connections where ConnectionType='homeserver_internet' and NetworkType='mynetwork';")) if [ -z "$vpn_arr" ]; then showMessageBox "Empty" "There are no eligible connections for this selection, returning..." return fi menu_items="" for curvpn in "${vpn_arr[@]}" do menu_items=${menu_items}"$(echo "("$(echo $curvpn | sed 's/|/)/1')) | OFF " done sel_vpn=($(whiptail --title "Select Network Connections" --checklist "$vpnremovemenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) is_remove=$(promptUserInputMenu "" "Confirm Removal" "This will remove all selected network connections. To confirm, enter the word 'remove' below:") if [ -z "$is_remove" ] || ! [ "$is_remove" = "remove" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi removal_reason=$(promptUserInputMenu "" "Removal Reason" "Enter a reason for removal: ") for curvpn in "${sel_vpn[@]}" do curvpnID=$(echo $curvpn | cut -d ")" -f1 | sed 's/(//' | sed 's/"//g') peer_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$curvpnID;") removeMyNetworkNonHomeServerConnection $curvpnID # Notify peer that you have removed them. notifyHomeServerInternetNetworkRemoval "$peer_email" "$removal_reason" done } function notifyHomeServerInternetNetworkRemoval() { peer_email="$1" removal_reason="$2" email_subj="HomeServer Removal Notice from $HOMESERVER_NAME" email_body="" email_body=${email_body}"HomeServer Removal Notice from $HOMESERVER_NAME\n" email_body=${email_body}"================================================================\n\n" email_body=${email_body}"Your HomeServer internet connection has been removed from the network.\n" email_body=${email_body}"Ensure to disconnect accordingly.\n\n" email_body=${email_body}"Host Domain: $HOMESERVER_DOMAIN\n" email_body=${email_body}"Reason Provided: $removal_reason\n\n" sendEmail -s "$email_subj" -b "$email_body" -t "$peer_email" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function showMyNetworkRemoveUserMenu() { vpnremovemenu=$(cat << EOF $(getLogo) Select the network connections that you wish to remove: EOF ) vpn_arr=($(sqlite3 $HSHQ_DB "select ID,Name from connections where ConnectionType='user' and NetworkType='mynetwork';")) if [ -z "$vpn_arr" ]; then showMessageBox "Empty" "There are no eligible connections for this selection, returning..." return fi menu_items="" for curvpn in "${vpn_arr[@]}" do menu_items=${menu_items}"$(echo "("$(echo $curvpn | sed 's/|/)/1')) | OFF " done sel_vpn=($(whiptail --title "Select Network Connections" --checklist "$vpnremovemenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) removal_reason=$(promptUserInputMenu "" "Removal Reason" "Enter a reason for removal: ") is_remove=$(promptUserInputMenu "" "Confirm Removal" "This will remove all selected network connections. To confirm, enter the word 'remove' below:") if [ -z "$is_remove" ] || ! [ "$is_remove" = "remove" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi for curvpn in "${sel_vpn[@]}" do curvpnID=$(echo $curvpn | cut -d ")" -f1 | sed 's/(//' | sed 's/"//g') peer_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$curvpnID;") removeMyNetworkNonHomeServerConnection $curvpnID # Notify peer that you have removed them. notifyUserNetworkRemoval "$peer_email" "$removal_reason" done } function notifyUserNetworkRemoval() { peer_email="$1" removal_reason="$2" email_subj="HomeServer Removal Notice from $HOMESERVER_NAME" email_body="" email_body=${email_body}"HomeServer Removal Notice from $HOMESERVER_NAME\n" email_body=${email_body}"================================================================\n\n" email_body=${email_body}"Your client connection has been removed from the network.\n" email_body=${email_body}"Ensure to disconnect accordingly.\n\n" email_body=${email_body}"Host Domain: $HOMESERVER_DOMAIN\n" email_body=${email_body}"Reason Provided: $removal_reason\n\n" sendEmail -s "$email_subj" -b "$email_body" -t "$peer_email" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function showMyNetworkCreateClientDNSMenu() { clientdns_stack_name=$(promptUserInputMenu "" "ClientDNS Stack Name" "Enter a unique name for this stack, 3-10 alpha-numeric lowercase characters: ") if [ -z "$clientdns_stack_name" ]; then showMessageBox "Stack Name Empty" "The name cannot be empty." fi if [ -z "$clientdns_stack_name" ]; then return fi performMyNetworkCreateClientDNS "$clientdns_stack_name" } function performMyNetworkCreateClientDNS() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi clientdns_stack_name="$1" if [ $(checkValidString "$clientdns_stack_name") = "false" ] || [ ${#clientdns_stack_name} -lt 3 ] || [ ${#clientdns_stack_name} -gt 10 ]; then echo "ERROR: Invalid ClientDNS stack name - must contain 3-10 lowercase alpha-numeric characters, no spaces or special characters." return fi # Check if name exists in DB cdns_stack_name=$(sqlite3 $HSHQ_DB "select Name from connections where Name='clientdns-$clientdns_stack_name';") if ! [ -z "$cdns_stack_name" ]; then echo "ERROR: A stack with this name already exists." return fi # Check if stack exists in Portainer checkID=$(getStackID clientdns-${clientdns_stack_name}) if ! [ -z "$checkID" ]; then echo "ERROR: A stack with this name already exists." return fi # Generate WireGuard config wg_ip=$(getRandomVPNIP) if [ -z "$wg_ip" ]; then echo "ERROR: Could not allocate an IP address, your network is full." return fi wg_priv_key=$(wg genkey) wg_pre_key=$(wg genpsk) wg_pub_key=$(echo $wg_priv_key | wg pubkey) wgPortalAuth="$(getWGPortalAuth)" perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh \"ClientDNS-$clientdns_stack_name\" \"$EMAIL_ADMIN_EMAIL_ADDRESS\" \"$wg_pub_key\" \"$wg_pre_key\" \"$wg_ip\" \"false\" \"true\" \"$wgPortalAuth\"" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an error adding the peer, returning..." return fi sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated) values('clientdns-$clientdns_stack_name','$EMAIL_ADMIN_EMAIL_ADDRESS','clientdns','mynetwork','$wg_pub_key','$wg_pre_key','$wg_ip',false,'wg0','$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$(getCurrentDate)');" updateDeviceIPMappingFile sudo rm -f $HSHQ_WIREGUARD_DIR/users/clientdns-${clientdns_stack_name}.conf sudo tee $HSHQ_WIREGUARD_DIR/users/clientdns-${clientdns_stack_name}.conf >/dev/null <&1 1>&2 2>&3)) is_remove=$(promptUserInputMenu "" "Confirm Removal" "This will remove all selected network connections. To confirm, enter the word 'remove' below:") if [ -z "$is_remove" ] || ! [ "$is_remove" = "remove" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi for curvpn in "${sel_vpn[@]}" do curvpnID=$(echo $curvpn | cut -d ")" -f1 | sed 's/(//' | sed 's/"//g') cdns_stack_name=$(sqlite3 $HSHQ_DB "select Name from connections where ID=$curvpnID;") performMyNetworkRemoveClientDNS "$curvpnID" "$cdns_stack_name" true true done } function performMyNetworkRemoveClientDNS() { dbID="$1" cdns_stack_name="$2" is_remove_network_conn="$3" is_manage_heimdall="$4" removeCaddySnippetAllImports sn-sub-${cdns_stack_name} removeCaddySnippet "# sn-sub-${cdns_stack_name} BEGIN" "# sn-sub-${cdns_stack_name} END" removeServiceFromConfig "${cdns_stack_name}" checkDeleteStackAndDirectory $cdns_stack_name "$cdns_stack_name" true true dock_net_name_suffix=$(echo "$cdns_stack_name" | cut -d"-" -f2-) docker network rm cdns-$dock_net_name_suffix > /dev/null 2>&1 deleteSvcHeimdall admin "https://${cdns_stack_name}.$HOMESERVER_DOMAIN" "$is_manage_heimdall" if [ "$is_remove_network_conn" = "true" ]; then removeMyNetworkNonHomeServerConnection $dbID true else sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from connections where ID=$dbID;" updateDeviceIPMappingFile fi } function showOtherNetworkDisconnectVPNMenu() { vpndisconnectmenu=$(cat << EOF $(getLogo) Select the network connections that you wish to disconnect: EOF ) vpn_arr=($(sqlite3 $HSHQ_DB "select ID,Name from connections where ConnectionType='homeserver_vpn' and NetworkType='other';")) if [ -z "$vpn_arr" ]; then showMessageBox "Empty" "There are no eligible connections for this selection, returning..." return fi menu_items="" for curvpn in "${vpn_arr[@]}" do menu_items=${menu_items}"$(echo "("$(echo $curvpn | sed 's/|/)/1')) | OFF " done sel_vpn=($(whiptail --title "Select Network Connections" --checklist "$vpndisconnectmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) disconnect_reason=$(promptUserInputMenu "" "Disconnect Reason" "Enter a reason for disconnecting: ") is_remove=$(promptUserInputMenu "" "Confirm Disconnect" "This will disconnect all selected network connections. To confirm, enter the word 'disconnect' below:") if [ $? -ne 0 ] || [ -z "$is_remove" ] || ! [ "$is_remove" = "disconnect" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi for curvpn in "${sel_vpn[@]}" do curvpnID=$(echo $curvpn | cut -d ")" -f1 | sed 's/(//' | sed 's/"//g') disconnectOtherNetworkHomeServerVPNConnection $curvpnID "$disconnect_reason" done } function showOtherNetworkDisconnectInternetMenu() { vpndisconnectmenu=$(cat << EOF $(getLogo) Select the network connections that you wish to disconnect: EOF ) vpn_arr=($(sqlite3 $HSHQ_DB "select ID,Name from connections where ConnectionType='homeserver_internet' and NetworkType='other';")) if [ -z "$vpn_arr" ]; then showMessageBox "Empty" "There are no eligible connections for this selection, returning..." return fi menu_items="" for curvpn in "${vpn_arr[@]}" do menu_items=${menu_items}"$(echo "("$(echo $curvpn | sed 's/|/)/1')) | OFF " done sel_vpn=($(whiptail --title "Select Network Connections" --checklist "$vpndisconnectmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) disconnect_reason=$(promptUserInputMenu "" "Disconnect Reason" "Enter a reason for disconnecting: ") is_remove=$(promptUserInputMenu "" "Confirm Disconnect" "This will disconnect all selected network connections. To confirm, enter the word 'disconnect' below:") if [ $? -ne 0 ] || [ -z "$is_remove" ] || ! [ "$is_remove" = "disconnect" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi for curvpn in "${sel_vpn[@]}" do curvpnID=$(echo $curvpn | cut -d ")" -f1 | sed 's/(//' | sed 's/"//g') disconnectOtherNetworkHomeServerInternetConnection "$curvpnID" "$disconnect_reason" done } # VPN Apply functions function applyHomeServerPrimaryVPNConfig() { is_primary=$1 set +e le_cert_domains=unset while [ "$le_cert_domains" = "unset" ] do le_cert_domains=$(promptUserInputMenu "$(getLetsEncryptCertsDefault)" "Enter LE Cert Subdomains" "Enter the subdomains for which the certificates will be managed by LetsEncrypt (comma-separated):") if [ $? -ne 0 ]; then return 1 fi if [ $(checkValidString "$le_cert_domains" ",.-") = "false" ]; then showMessageBox "Invalid Character(s)" "The domain list contains invalid character(s). It must consist of a-z (lowercase), 0-9, -, and/or ." le_cert_domains=unset fi done createOtherNetworkApplyHomeServerVPNConfig true "$le_cert_domains" } function createOtherNetworkApplyHomeServerVPNConfig() { is_primary=$1 le_cert_domains="$2" recipient_email=$(promptUserInputMenu "" "Enter Recipient" "Enter the administrator email address of the network to which you are applying:") if [ $? -ne 0 ]; then return 1 fi sendOtherNetworkApplyHomeServerVPNConfig "$recipient_email" "$is_primary" "$le_cert_domains" if [ "$IS_INSTALLED" = "true" ]; then showMessageBox "Check Email" "Check your email ($EMAIL_ADMIN_EMAIL_ADDRESS) for the configuration application. You will need to forward the application to the manager of the network to which you are applying." fi } function sendOtherNetworkApplyHomeServerVPNConfig() { recipient_email="$1" is_primary="$2" le_cert_domains="$3" set +e if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] && ! [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ] && ! [ "$is_primary" = "true" ]; then echo "ERROR: Cannot host on another network without a primary VPN connection." return 1 fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] && [ "$is_primary" = "true" ]; then echo "ERROR: You are already hosting a VPN. You must remove this first." return 1 fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ] && [ "$is_primary" = "true" ]; then echo "ERROR: You already have a primary connection. You must disconnect from your primary network first." return 1 fi request_id=$(getRandomRequestID) priv_key=$(wg genkey) pub_key=$(echo $priv_key | wg pubkey) echo "RequestID = $request_id" > $HSHQ_WIREGUARD_DIR/requestkeys/$request_id echo "PrivateKey = $priv_key" >> $HSHQ_WIREGUARD_DIR/requestkeys/$request_id echo "ConnectionType = HomeServer VPN" >> $HSHQ_WIREGUARD_DIR/requestkeys/$request_id echo "IsPrimary = $is_primary" >> $HSHQ_WIREGUARD_DIR/requestkeys/$request_id chmod 0400 $HSHQ_WIREGUARD_DIR/requestkeys/$request_id domain_name=$HOMESERVER_DOMAIN msg_body="$APPLICATION_FIRST_LINE\n" msg_body=$msg_body"RequestID = $request_id\n" msg_body=$msg_body"ConnectionType = HomeServer VPN\n" msg_body=$msg_body"EmailAddress = $EMAIL_ADMIN_EMAIL_ADDRESS\n" msg_body=$msg_body"PublicKey = $pub_key\n" msg_body=$msg_body"PresharedKey = \n" msg_body=$msg_body"DomainName = $domain_name\n" msg_body=$msg_body"HomeServerName = $HOMESERVER_NAME\n" msg_body=$msg_body"ExternalPrefix = $EXT_DOMAIN_PREFIX\n" if [ "$is_primary" = "true" ]; then RELAYSERVER_LE_CERT_DOMAINS="$le_cert_domains" updateConfigVar RELAYSERVER_LE_CERT_DOMAINS $RELAYSERVER_LE_CERT_DOMAINS leCertsArr=($(echo $RELAYSERVER_LE_CERT_DOMAINS | tr "," "\n")) isReload=false for leCert in "${leCertsArr[@]}" do curBaseDomain=$(getBaseDomain $leCert) curSubDomain=$(getSubDomain $leCert) if [ "$curBaseDomain" = "$HOMESERVER_DOMAIN" ]; then for curSVC in "${SVCS_ARR[@]}" do var_base=$(echo $curSVC | cut -d"=" -f1 | cut -d"_" -f 2-) subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6) subdom="${subdom//\"}" if [ "$subdom" = "$curSubDomain" ]; then var_name="SVCD_"$var_base var_value=$(getConfigVar $var_name) var_valueArr=($(echo $var_value | tr "," "\n")) var_valueArr[6]=le printf -v new_value '%s,' "${var_valueArr[@]}" new_value="$(echo "${new_value%,}")" updateConfigVar $var_name $new_value isReload=true fi done fi done if [ "$isReload" = "true" ]; then loadSvcVars fi msg_body=$msg_body"IsPrimary = true\n" msg_body=$msg_body"MailSubdomain = $SUB_POSTFIX.$HOMESERVER_DOMAIN\n" msg_body=$msg_body"LetsEncryptDomains = $RELAYSERVER_LE_CERT_DOMAINS\n" else msg_body=$msg_body"IsPrimary = false\n" fi msg_body=$msg_body"$APPLICATION_LAST_LINE\n" set -e if ! [ "$IS_INSTALLED" = "true" ]; then clear echo -e "\n" echo -e "$msg_body" echo -e "\n" echo -e "Copy the above section and submit it to your RelayServer administrator." else msg_header="HomeServer VPN Application from $HOMESERVER_NAME ($request_id)\n\n" msg_header=${msg_header}"For the receiving manager: \n" msg_header=${msg_header}"1) Go to the Script-server app, and navigate to 06 My Network > 01 Invite to Network.\n" msg_header=${msg_header}"2) Copy everything BELOW the following line and paste the contents where appropriate.\n\n" msg_header=${msg_header}"_______________________________________________________________________\n\n" msg_body=${msg_header}"$msg_body" if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] || [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then sendEmail -s "HomeServer VPN Application from $HOMESERVER_NAME ($request_id)" -b "$msg_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$recipient_email" echo "Application successfully sent!" else echo "You do not have a primary network, you will only receive a manager (MGR) copy of the application. Ensure to transfer this to the manager of the network to which you are applying." fi sendEmail -s "(MGR COPY)HomeServer VPN Application from $HOMESERVER_NAME ($request_id)" -b "$msg_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" fi } function createOtherNetworkApplyHomeServerInternetConfig() { recipient_email=$(promptUserInputMenu "" "Enter Recipient" "Enter the administrator email address of the network to which you are applying:") if [ $? -ne 0 ]; then return fi sendOtherNetworkApplyHomeServerInternetConfig "$recipient_email" } function sendOtherNetworkApplyHomeServerInternetConfig() { recipient_email="$1" request_id=$(getRandomRequestID) priv_key=$(wg genkey) pub_key=$(echo $priv_key | wg pubkey) echo "RequestID = $request_id" > $HSHQ_WIREGUARD_DIR/requestkeys/$request_id echo "PrivateKey = $priv_key" >> $HSHQ_WIREGUARD_DIR/requestkeys/$request_id echo "ConnectionType = HomeServer Internet" >> $HSHQ_WIREGUARD_DIR/requestkeys/$request_id echo "IsPrimary = false" >> $HSHQ_WIREGUARD_DIR/requestkeys/$request_id chmod 0400 $HSHQ_WIREGUARD_DIR/requestkeys/$request_id msg_body="" msg_body=$msg_body"$APPLICATION_FIRST_LINE\n" msg_body=$msg_body"RequestID = $request_id\n" msg_body=$msg_body"ConnectionType = HomeServer Internet\n" msg_body=$msg_body"EmailAddress = $EMAIL_ADMIN_EMAIL_ADDRESS\n" msg_body=$msg_body"PublicKey = $pub_key\n" msg_body=$msg_body"DomainName = $HOMESERVER_DOMAIN\n" msg_body=$msg_body"IsInternet = true\n" msg_body=$msg_body"$APPLICATION_LAST_LINE\n" msg_header="Copy everything BELOW the following line:\n\n" msg_header=$msg_header"_______________________________________________________________________\n\n" msg_body=${msg_header}"$msg_body" sendEmail -s "HomeServer Internet Application from $HOMESERVER_NAME ($request_id)" -b "$msg_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$recipient_email" echo "Application successfully sent!" } function createOtherNetworkApplyUserConfig() { email_address=$(promptUserInputMenu "$EMAIL_ADMIN_EMAIL_ADDRESS" "Enter Email Address" "Enter the email address that you wish to associate with this request: ") if [ $? -ne 0 ]; then return fi showYesNoMessageBox "Internet Access" "Request public internet IP masquerade for this connection?" if [ $? -eq 0 ]; then isInternetAccess=true else isInternetAccess=false fi ip_address=$(promptUserInputMenu "" "Enter IP Address" "Enter your client device IP address. Leave blank if you do not have one and want it generated for you: ") if [ $? -ne 0 ]; then return fi if ! [ -z "$ip_address" ]; then if [ "$(checkValidIPAddress $ip_address)" = "false" ] || [ "$(isIPInSubnet $ip_address 10.0.0.0/8)" = "false" ]; then showMessageBox "Invalid IP" "Invalid IP address, returning..." return fi ip_address=$(echo $ip_address | cut -d "/" -f1) pub_key=$(promptUserInputMenu "" "Enter Public Key" "Enter your client device public key: ") if [ $? -ne 0 ]; then return fi if [ -z "$pub_key" ]; then showMessageBox "Invalid Public Key" "Invalid Public Key, returning..." return fi fi recipient_email=$(promptUserInputMenu "" "Enter Recipient" "Enter the administrator email address of the network to which you are applying:") if [ $? -ne 0 ]; then return fi description=$(promptUserInputMenu "" "Enter Description" "Enter a description of what this connection will be used for:") if [ $? -ne 0 ]; then return fi sendOtherNetworkApplyUserConfig "$recipient_email" "$email_address" "$isInternetAccess" "$ip_address" "$pub_key" "$description" } function sendOtherNetworkApplyUserConfig() { recipient_email="$1" email_address="$2" is_internet="$3" ip_address="$4" pub_key="$5" description="$6" request_id=$(getRandomRequestID) if [ -z "$pub_key" ] && ! [ -z "$ip_address" ]; then echo "ERROR: IP address provided without public key." return 2 fi if [ -z "$ip_address" ]; then ip_address="New" fi if [ -z "$pub_key" ]; then priv_key=$(wg genkey) pub_key=$(echo $priv_key | wg pubkey) sendEmail -s "Private Key ($request_id)" -b "DO NOT SEND THIS TO ANYONE!!!\n\nKeep it secret, keep it safe.\n\nRequestID: $request_id\nDescription: $description\nPrivate Key: $priv_key\n" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t $email_address elif [ "$(checkValidWireGuardKey $client_public_key)" = "false" ]; then echo "ERROR: Invalid public key." return 2 elif ! [ -z "$(getWGNameFromPubkey $pub_key)" ]; then echo "ERROR: Duplicate public key." return 2 fi msg_body="" msg_body=$msg_body"$APPLICATION_FIRST_LINE\n" msg_body=$msg_body"RequestID = $request_id\n" msg_body=$msg_body"ConnectionType = User\n" msg_body=$msg_body"EmailAddress = $email_address\n" msg_body=$msg_body"PublicKey = $pub_key\n" msg_body=$msg_body"IsInternet = $is_internet\n" msg_body=$msg_body"IPAddress = $ip_address\n" msg_body=$msg_body"$APPLICATION_LAST_LINE\n" msg_header="" msg_header=$msg_header"Description: $description\n\n" msg_header=$msg_header"Copy everything BELOW the following line:\n" msg_header=$msg_header"_______________________________________________________________________\n\n" msg_body=${msg_header}"$msg_body" sendEmail -s "User Application from $HOMESERVER_NAME ($request_id)" -b "$msg_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$recipient_email" } # VPN Invite functions function getValueFromConfig() { get_val="$1" apply_file="$2" retVal=$(grep "^$get_val = " $apply_file) if [ $? -eq 0 ]; then echo "$(echo $retVal | sed 's/^[^=]*=//' | sed 's/ *$//g' | sed 's/^[ \t]*//;s/[ \t]*$//')" fi } function createMyNetworkInvite() { set +e config_name=$(promptUserInputMenu "" "Enter Config Name" "Enter a name for this configuration: ") if [ $? -ne 0 ]; then return fi apply_file=$HOME/apply_hsv.cnf showMessageBox "Paste Configuration" "In this next step, you will need to paste the application that you received via email. After pasting the contents, press CTRL-o to save, then Enter to confirm, then CTRL-x to exit. Please remember these key combinations. Press OK to proceed." nano $apply_file if ! [ -f $apply_file ]; then showMessageBox "Empty Config File" "The application is empty, returning..." return fi performNetworkInvite "$apply_file" "$config_name" rm -f $apply_file if [ $? -ne 0 ]; then showMessageBox "Config File Error" "There was an error with the config file, returning..." return fi } function createMyNetworkUserInvite() { email_address="$1" is_internet="$2" ip_address="$3" pub_key="$4" pre_key="$5" apply_file="$6" priv_key="" request_id=$(getRandomRequestID) if [ -z "$pub_key" ] && ! [ -z "$ip_address" ]; then echo "ERROR: IP address provided without public key." return 1 fi if [ -z "$ip_address" ]; then ip_address="New" fi if [ -z "$pub_key" ]; then priv_key=$(wg genkey) pub_key=$(echo $priv_key | wg pubkey) fi msg_body="" msg_body=$msg_body"$APPLICATION_FIRST_LINE\n" msg_body=$msg_body"RequestID = $request_id\n" msg_body=$msg_body"ConnectionType = User\n" msg_body=$msg_body"EmailAddress = $email_address\n" if ! [ -z "$priv_key" ]; then msg_body=$msg_body"PrivateKey = $priv_key\n" fi msg_body=$msg_body"PresharedKey = $pre_key\n" msg_body=$msg_body"PublicKey = $pub_key\n" msg_body=$msg_body"IsInternet = $is_internet\n" msg_body=$msg_body"IPAddress = $ip_address\n" msg_body=$msg_body"$APPLICATION_LAST_LINE\n" echo -e "$msg_body" > $apply_file } function performNetworkInvite() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi apply_file="$1" config_name="$2" # First check if there are any shell expansions in the file. # If there is, throw up a big red flag. check_file=$(checkFileShellExpansion $apply_file) if ! [ -z "$check_file" ]; then echo -e "$check_file" return 2 fi # Do basic checks removeSpecialChars "$apply_file" first_line="$(awk 'NF{print;exit}' $apply_file)" last_line="$(awk 'NF{s=$0}END{print s}' $apply_file)" if ! [ "$first_line" = "$APPLICATION_FIRST_LINE" ] || ! [ "$last_line" = "$APPLICATION_LAST_LINE" ]; then echo "ERROR: Incomplete application." echo "first_line: $first_line" echo "last_line: $last_line" return 7 fi request_id=$(getValueFromConfig "RequestID" $apply_file) if [ -z "$request_id" ]; then echo "ERROR: Invalid request ID." return 7 fi conn_type=$(getValueFromConfig "ConnectionType" $apply_file) email_address=$(getValueFromConfig "EmailAddress" $apply_file) if [ "$(checkValidEmail $email_address)" = "false" ]; then echo "ERROR: Invalid email address." return 7 fi pub_key=$(getValueFromConfig "PublicKey" $apply_file) if [ -z "$pub_key" ] || [ "$(checkValidWireGuardKey $pub_key)" = "false" ]; then echo "ERROR: Invalid public key." return 7 fi if ! [ -z "$(getWGNameFromPubkey $pub_key)" ]; then echo "ERROR: Duplicate public key." return 7 fi preshared_key=$(getValueFromConfig "PresharedKey" $apply_file) if ! [ -z "$preshared_key" ] && [ "$(checkValidWireGuardKey $preshared_key)" = "false" ]; then echo "ERROR: Invalid preshared key." return 7 fi case "$conn_type" in "HomeServer VPN") domain_name=$(getValueFromConfig "DomainName" $apply_file) if [ -z "$domain_name" ] || [ $(checkValidBaseDomain "$domain_name") = "false" ]; then echo "ERROR: Invalid domain name." return 7 fi cur_hs_name=$(getValueFromConfig "HomeServerName" $apply_file) if [ -z "$cur_hs_name" ]; then echo "ERROR: Invalid HomeServer name." return 7 fi external_prefix=$(getValueFromConfig "ExternalPrefix" $apply_file) if [ -z "$external_prefix" ] || [ $(checkValidString "$external_prefix" "-") = "false" ]; then echo "ERROR: Invalid external prefix." return 7 fi is_primary=$(getValueFromConfig "IsPrimary" $apply_file) if [ -z "$is_primary" ] || ! ( [ "$is_primary" = "true" ] || [ "$is_primary" = "false" ] ); then echo "ERROR: Invalid primary VPN selection." return 7 fi if [ "$is_primary" = "true" ]; then mail_subdomain=$(getValueFromConfig "MailSubdomain" $apply_file) if [ -z "$mail_subdomain" ] || [ $(checkValidString "$mail_subdomain" ".-") = "false" ]; then echo "ERROR: Invalid mail subdomain." return 7 fi le_domains=$(getValueFromConfig "LetsEncryptDomains" $apply_file) if [ -z "$le_domains" ] || [ $(checkValidString "$external_prefix" ",.-") = "false" ]; then echo "ERROR: Invalid LetsEncrypt subdomains." return 7 fi fi ;; "HomeServer Internet") domain_name=$(getValueFromConfig "DomainName" $apply_file) if [ -z "$domain_name" ] || [ $(checkValidBaseDomain "$domain_name") = "false" ]; then echo "ERROR: Invalid domain name." return 7 fi is_internet=$(getValueFromConfig "IsInternet" $apply_file) if [ -z "$is_internet" ] || ! ( [ "$is_internet" = "true" ] || [ "$is_internet" = "false" ] ); then echo "ERROR: Invalid IsInternet selection." return 7 fi ;; "User") if [ -z "$config_name" ] || [ $(checkValidStringUpperLowerNumbers "$config_name" "-") = "false" ]; then echo "ERROR: Invalid config name." return 7 fi is_internet=$(getValueFromConfig "IsInternet" $apply_file) if [ -z "$is_internet" ] || ! ( [ "$is_internet" = "true" ] || [ "$is_internet" = "false" ] ); then echo "ERROR: Invalid IsInternet selection." return 7 fi ip_address=$(getValueFromConfig "IPAddress" $apply_file) if [ -z "$pub_key" ] && ! [ "$ip_address" = "New" ]; then echo "ERROR: IP address provided without public key." return 7 fi if [ "$ip_address" = "New" ]; then ip_address=$(getRandomWireGuardIP) is_ip_provided=false else is_ip_provided=true fi if [ -z "$ip_address" ] || [ "$(checkValidIPAddress $ip_address)" = "false" ] || [ "$(isIPInSubnet $ip_address 10.0.0.0/8)" = "false" ]; then echo "ERROR: Invalid IP address." return 7 fi ;; *) echo "ERROR: Connection type not found." return 7 ;; esac # Do logical checks case "$conn_type" in "HomeServer VPN") if [ "$domain_name" = "$HOMESERVER_DOMAIN" ]; then echo "ERROR: This is your domain, dummy!" return 7 fi config_name="HS-VPN-$domain_name" checkname_db=$(sqlite3 $HSHQ_DB "select Name from connections where Name='$config_name';") if ! [ -z "$checkname_db" ]; then echo "ERROR: There is already a connection with this name." return 7 fi check_dom=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections join connections on hsvpn_connections.ID = connections.ID where hsvpn_connections.DomainName='$domain_name' and connections.NetworkType='mynetwork';") if ! [ -z "$check_dom" ]; then echo "ERROR: There is already a connection with this domain ($domain_name)." return 7 fi if ! [ "$(isEmailMatchBaseDomain $email_address $domain_name)" = "true" ]; then echo "ERROR: Requesting email is not from the same base domain. Email: $email_address, Domain: $domain_name" return 7 fi new_ip=$(getRandomVPNIP) if [ -z "$new_ip" ]; then echo "ERROR: The VPN network is full." return 7 fi isPrimary=0 if [ "$is_primary" = "true" ]; then isPrimary=1 fi ;; "HomeServer Internet") if [ "$domain_name" = "$HOMESERVER_DOMAIN" ]; then echo "ERROR: This is your domain, dummy!" return 7 fi if ! [ "$(isEmailMatchBaseDomain $email_address $domain_name)" = "true" ]; then echo "ERROR: Requesting email is not from the same base domain. Email: $email_address, Domain: $domain_name" return 7 fi config_name="HS-Internet-$domain_name" checkname_db=$(sqlite3 $HSHQ_DB "select Name from connections where Name='$config_name';") if ! [ -z "$checkname_db" ]; then echo "ERROR: There is already a connection with this name." return 7 fi ;; "User") is_db=$(getWGNameFromIP $ip_address) if ! [ -z "$is_db" ]; then echo "ERROR: This IP address is already being used (Connection Name: $is_db)." return 7 fi is_in_subnet=$(isIPInSubnet $ip_address $PRIMARY_VPN_SUBNET) if [ "$is_in_subnet" = "true" ]; then echo "ERROR: This IP address is inside of your VPN range designated for hosting RemoteServers." return 7 fi ;; esac # Perform invitation steps tmp_preshared_key=$(wg genpsk) if [ -z "$preshared_key" ]; then preshared_key="$tmp_preshared_key" fi wgPortalAuth="$(getWGPortalAuth)" curdt=$(getCurrentDate) case "$conn_type" in "HomeServer VPN") echo "Adding to RelayServer..." perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh \"HS-$domain_name\" \"$email_address\" \"$pub_key\" \"$preshared_key\" \"$new_ip\" \"false\" \"true\" \"$wgPortalAuth\"" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an error adding the peer." return 7 fi db_id=$(sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated) values('$config_name','$email_address','homeserver_vpn','mynetwork','$pub_key','$preshared_key','$new_ip',false,'','$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$curdt');select last_insert_rowid();") mail_host_id=NULL if [ "$is_primary" = "true" ]; then mail_host_id=$(sudo sqlite3 $HSHQ_DB "insert into mailhosts(MailHost) values('$mail_subdomain');select last_insert_rowid();") fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into hsvpn_connections(ID,HomeServerName,IsPrimary,DomainName,ExternalPrefix,MailHostID) values($db_id,'$cur_hs_name','$isPrimary','$domain_name','$external_prefix',$mail_host_id);" echo "Adding DNS entries to AdguardHome..." addDomainAndWildcardAdguardHS "$domain_name" "$new_ip" addDomainAndWildcardAdguardRS "$domain_name" "$new_ip" addDomainAdguardHS "*.$external_prefix.$domain_name" "A" addDomainAdguardRS "*.$external_prefix.$domain_name" "A" addDomainAndWildcardIgnoreQuerylogAndStatsHS "$domain_name" sudo sqlite3 $HSHQ_DB "update hsvpn_dns set IsActive=0 where PeerDomain='$domain_name';" sudo sqlite3 $HSHQ_DB "insert into hsvpn_dns(HostDomain,PeerDomain,PeerDomainExtPrefix,IPAddress,DateAdded,IsActive) values('$HOMESERVER_DOMAIN','$domain_name','$external_prefix','$new_ip','$curdt',1);" if [ "$is_primary" = "true" ]; then echo "Adding mail domain to RelayServer..." sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into mailhostmap(MailHostID,Domain,IsFirstDomain) values ($mail_host_id,'$domain_name',true);" mail_relay_password=$(pwgen -c -n 32 1) perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "docker exec mail-relay-postfix /etc/postfix/scripts/addMailUser.sh $domain_name $mail_relay_password" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was another unknown error." return 7 fi perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addRelayedDomains.sh $domain_name $mail_subdomain $new_ip" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was another unknown error." return 7 fi if ! [ -z "$le_domains" ]; then echo "Primary Adding LECertDomains: $le_domains" addLECertPathsToRelayServer "$le_domains" "$domain_name" fi fi ;; "HomeServer Internet") new_ip=$(getRandomWireGuardIP) if [ -z "$new_ip" ]; then echo "ERROR: The HomeServer Internet network is full." return 7 fi perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh \"$config_name\" \"$email_address\" \"$pub_key\" \"$preshared_key\" \"$new_ip\" \"true\" \"false\" \"$wgPortalAuth\"" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an error adding the peer." return 7 fi sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,LastUpdated) values('$config_name','$email_address','homeserver_internet','mynetwork','$pub_key','$preshared_key','$new_ip',true,'$curdt');" ;; "User") perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh \"$config_name\" \"$email_address\" \"$pub_key\" \"$preshared_key\" \"$ip_address\" \"$is_internet\" \"false\" \"$wgPortalAuth\"" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an error adding the peer." return 7 fi isInternet=false if [ "$is_internet" = "true" ]; then isInternet=true fi sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,LastUpdated) values('$config_name','$email_address','user','mynetwork','$pub_key','$preshared_key','$ip_address',$isInternet,'$curdt');" priv_key=$(getValueFromConfig "PrivateKey" $apply_file) ;; esac updateDeviceIPMappingFile echo "Emailing the invitation..." # Finally, email the invitation. case "$conn_type" in "HomeServer VPN") mail_subj="HomeServer VPN Invitation from $HOMESERVER_NAME, RequestID: $request_id" mail_body="" mail_body=${mail_body}"HomeServer VPN Invitation from $HOMESERVER_NAME\n" mail_body=${mail_body}"================================================================\n\n" if [ "$is_primary" = "true" ]; then mail_body=${mail_body}"$(getDNSRecordsInfo $domain_name)\n\n" fi mail_body=$mail_body"1) Go to the Script-server app, and navigate to 07 Other Networks > 05 Join Network.\n" mail_body=$mail_body"2) Copy everything BELOW the following line and paste the contents where appropriate.\n" mail_body=$mail_body"_______________________________________________________________________\n" mail_body=$mail_body"\n$INVITATION_FIRST_LINE\n" mail_body=$mail_body"\n###################### Base Config Begin #######################\n\n" mail_body=$mail_body"RequestID = $request_id\n" mail_body=$mail_body"ConnectionType = $conn_type\n" mail_body=$mail_body"InterfaceName = $RELAYSERVER_WG_VPN_NETNAME\n" mail_body=$mail_body"EmailAddress = $EMAIL_ADMIN_EMAIL_ADDRESS\n" mail_body=$mail_body"DomainName = $HOMESERVER_DOMAIN\n" mail_body=$mail_body"EndpointHostname = $RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN\n" mail_body=$mail_body"EndpointPort = $RELAYSERVER_WG_PORT\n" mail_body=$mail_body"ClientIP = $new_ip\n" mail_body=$mail_body"ClientPublicKey = $pub_key\n" mail_body=$mail_body"RelayServerPublicKey = $RELAYSERVER_WG_SV_PUBLICKEY\n" mail_body=$mail_body"PresharedKey = $tmp_preshared_key\n" mail_body=$mail_body"HomeServerName = $HOMESERVER_NAME\n" mail_body=$mail_body"ExternalPrefix = $EXT_DOMAIN_PREFIX\n" mail_body=$mail_body"InternalPrefix = $INT_DOMAIN_PREFIX\n" mail_body=$mail_body"CertificateAuthorityAbbrev = $HOMESERVER_ABBREV\n" mail_body=$mail_body"CertificateAuthorityIP = $RELAYSERVER_WG_HS_IP\n" mail_body=$mail_body"CertificateAuthoritySubdomain = $SUB_CADDY.$HOMESERVER_DOMAIN\n" mail_body=$mail_body"CertificateAuthorityURL = https://$SUB_CADDY.$HOMESERVER_DOMAIN/acme/$HOMESERVER_ABBREV/directory\n" mail_body=$mail_body"VPNSubnet = $PRIMARY_VPN_SUBNET\n" mail_body=$mail_body"RelayServerExternalIP = $RELAYSERVER_SERVER_IP\n" mail_body=$mail_body"RelayServerVPNIP = $RELAYSERVER_WG_SV_IP\n" mail_body=$mail_body"IsPrimary = $is_primary\n" if [ "$is_primary" = "true" ]; then mail_body=$mail_body"SMTPRelayHost = $SMTP_RELAY_HOST\n" mail_body=$mail_body"SMTPRelayUsername = $domain_name\n" mail_body=$mail_body"SMTPRelayPassword = $mail_relay_password\n" mail_body=$mail_body"RelayServerExtEmailHostname = $RELAYSERVER_EXT_EMAIL_HOSTNAME\n" fi mail_body=$mail_body"\n####################### Base Config End ########################\n" mail_body=$mail_body"\n#################### RemoteServers DNS Begin #####################\n\n" mail_body=$mail_body"$(getMyNetworkHomeServerDNSList)" mail_body=$mail_body"\n##################### RemoteServers DNS End ######################\n" mail_body=$mail_body"\n######################### Root CA Begin #########################\n\n" mail_body=$mail_body"""$(cat $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt)""" mail_body=$mail_body"\n\n########################## Root CA End ##########################\n" if [ "$is_primary" = "true" ]; then generateCert $mail_subdomain "$SMTP_HOSTNAME,$mail_subdomain" mail_body=$mail_body"\n######################## mail.crt Begin ########################\n\n" mail_body=$mail_body"""$(cat $HSHQ_SSL_DIR/${mail_subdomain}.crt)""" mail_body=$mail_body"\n\n######################### mail.crt End #########################\n" mail_body=$mail_body"\n######################## mail.key Begin ########################\n\n" mail_body=$mail_body"""$(cat $HSHQ_SSL_DIR/${mail_subdomain}.key)""" mail_body=$mail_body"\n\n######################### mail.key End #########################\n" rm -f $RELAYSERVER_SSH_PORT $HSHQ_SSL_DIR/$mail_subdomain* fi mail_body=$mail_body"\n$INVITATION_LAST_LINE\n\n" if [ "$is_primary" = "true" ]; then echo -e "\n\n\n########################################" echo -e " This is a primary VPN request." echo -e " No email will be sent to $email_address." echo -e " You will have to transfer your MGR copy" echo -e " to them through alternative means." echo -e "########################################\n" outputDNSZoneFile $domain_name /tmp/${DNS_ZONE_FILENAME_BASE}_${domain_name}.txt # Send ourself a copy sendEmail -s "(MGR COPY)$mail_subj" -b "$mail_body" -a /tmp/${DNS_ZONE_FILENAME_BASE}_${domain_name}.txt else sendEmail -s "$mail_subj" -b "$mail_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$email_address" # Send ourself a copy sendEmail -s "(MGR COPY)$mail_subj" -b "$mail_body" fi insertEnableSvcUptimeKuma uptimekuma "${cur_hs_name}" homeservers "https://$SUB_HSHQSTATUS.$domain_name" true insertEnableSvcHeimdall heimdall "$cur_hs_name" homeservers "https://$SUB_HSHQHOME.$domain_name" "hs2.png" true "$(getHeimdallOrderFromSub $SUB_HSHQHOME homeservers)" # Send update email to other RemoteServers on our network notifyMyNetworkHomeServersDNSUpdate add "$cur_hs_name" "$domain_name" # Send update email to other users on our network notifyMyNetworkUsersDNSUpdate add "$cur_hs_name" "$domain_name" "$external_prefix" "$new_ip" rm -f /tmp/${DNS_ZONE_FILENAME_BASE}_${domain_name}.txt ;; "HomeServer Internet") mail_subj="HomeServer Internet Invitation from $HOMESERVER_NAME, RequestID: $request_id" mail_body="" mail_body=${mail_body}"HomeServer Internet Invitation from $HOMESERVER_NAME\n" mail_body=${mail_body}"================================================================\n\n" mail_body=$mail_body"1) Copy everything BELOW the following line.\n" mail_body=$mail_body"2) Go to the Script-server app, and navigate to 07 Other Networks > 05 Join Network.\n" mail_body=$mail_body"3) Paste the contents where appropriate. \n" mail_body=$mail_body"_______________________________________________________________________\n\n" mail_body=$mail_body"\n$INVITATION_FIRST_LINE\n" mail_body=$mail_body"\n###################### Base Config Begin #######################\n\n" mail_body=$mail_body"RequestID = $request_id\n" mail_body=$mail_body"ConnectionType = $conn_type\n" mail_body=$mail_body"InterfaceName = $RELAYSERVER_WG_INTERNET_NETNAME\n" mail_body=$mail_body"EmailAddress = $EMAIL_ADMIN_EMAIL_ADDRESS\n" mail_body=$mail_body"DomainName = $HOMESERVER_DOMAIN\n" mail_body=$mail_body"EndpointHostname = $RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN\n" mail_body=$mail_body"EndpointPort = $RELAYSERVER_WG_PORT\n" mail_body=$mail_body"ClientIP = $new_ip\n" mail_body=$mail_body"ClientPublicKey = $pub_key\n" mail_body=$mail_body"EndpointPublicKey = $RELAYSERVER_WG_SV_PUBLICKEY\n" mail_body=$mail_body"PresharedKey = $tmp_preshared_key\n" mail_body=$mail_body"MTU = $RELAYSERVER_CLIENT_DEFAULT_MTU\n" mail_body=$mail_body"\n####################### Base Config End ########################\n" mail_body=$mail_body"\n$INVITATION_LAST_LINE\n\n" # Send ourself a copy sendEmail -s "(MGR COPY)$mail_subj" -b "$mail_body" sendEmail -s "$mail_subj" -b "$mail_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$email_address" ;; "User") mail_subj="User Invitation from $HOMESERVER_NAME, RequestID: $request_id" mail_body="" mail_body=${mail_body}"User Invitation from $HOMESERVER_NAME\n" mail_body=${mail_body}"================================================================\n\n" mail_body=$mail_body"The public root certificate is attached to this email or can be downloaded via the following links (must be connected to network first). There is also an attached QR code with the URL (CertificateURL-qr.png).\n\n" mail_body=$mail_body"Root CA (PEM): http://$SUB_FILES.$HOMESERVER_DOMAIN/ca.crt\n" mail_body=$mail_body"Root CA (DER): http://$SUB_FILES.$HOMESERVER_DOMAIN/ca.der\n" mail_body=$mail_body"VPN Owner Home Page: https://$SUB_HSHQHOME.$HOMESERVER_DOMAIN\n\n" if [ "$is_ip_provided" = "false" ]; then mail_body=$mail_body"The configuration can be loaded using one of the three following ways: \n" mail_body=$mail_body" 1. Copy and paste the configuration INSIDE the ### borders below, or\n" mail_body=$mail_body" 2. Scan the attached QR image (${config_name}-WireGuard-qr.png) from within the WireGuard client, or\n" mail_body=$mail_body" 3. Load the attached config file (${config_name}.conf).\n\n" if [ -z "$priv_key" ]; then mail_body=$mail_body"Ensure to replace your private key in the config before activating.\n" fi else mail_body=$mail_body"Append the config section below to your existing configuration.\n" fi mail_body=$mail_body"_______________________________________________________________________\n" if [ "$is_internet" = "true" ]; then allowed_ips=$(getAllowedPublicIPs $PRIMARY_VPN_SUBNET) else allowed_ips=$PRIMARY_VPN_SUBNET fi wg_config="" if [ -z "$priv_key" ]; then priv_key=$(wg genkey) fi if [ "$is_ip_provided" = "false" ]; then wg_config=$wg_config"[Interface]\n" wg_config=$wg_config"PrivateKey = $priv_key\n" wg_config=$wg_config"Address = ${ip_address}/32\n" wg_config=$wg_config"MTU = $RELAYSERVER_CLIENT_DEFAULT_MTU\n" wg_config=$wg_config"DNS = $RELAYSERVER_WG_SV_CLIENTDNS_IP\n\n" fi wg_config=$wg_config"[Peer]\n" wg_config=$wg_config"PublicKey = $RELAYSERVER_WG_SV_PUBLICKEY\n" wg_config=$wg_config"PresharedKey = $tmp_preshared_key\n" wg_config=$wg_config"AllowedIPs = $allowed_ips\n" wg_config=$wg_config"Endpoint = $RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN:$RELAYSERVER_WG_PORT\n" wg_config=$wg_config"PersistentKeepalive = $RELAYSERVER_PERSISTENT_KEEPALIVE\n" mail_body=$mail_body"\n#################### WireGuard Config Begin ####################\n\n" mail_body=$mail_body"$wg_config" mail_body=$mail_body"\n##################### WireGuard Config End #####################\n\n" mail_body=$mail_body"If this is not your primary VPN, add/update the entries below into your ClientDNS server.\n" mail_body=$mail_body"_______________________________________________________________________\n" mail_body=$mail_body"\n#################### RemoteServers DNS Begin #####################\n\n" mail_body=$mail_body"$(getMyNetworkHomeServerDNSListForClientDNS)" mail_body=$mail_body"##################### RemoteServers DNS End ######################\n" qrencode -o $HOME/CertificateURL-qr.png "http://$SUB_FILES.$HOMESERVER_DOMAIN/ca.crt" if [ "$is_ip_provided" = "false" ]; then echo -e "$wg_config" > $HOME/${config_name}.conf qrencode -t png -o $HOME/${config_name}-WireGuard-qr.png -r $HOME/${config_name}.conf mail_attachments="-a $HOME/${config_name}.conf -a $HOME/${config_name}-WireGuard-qr.png -a $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -a $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der -a $HOME/CertificateURL-qr.png" else mail_attachments="-a $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -a $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der -a $HOME/CertificateURL-qr.png" fi sendEmail -s "$mail_subj" -b "$mail_body" $mail_attachments -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$email_address" # Send ourself a copy sendEmail -s "(MGR COPY)$mail_subj" -b "$mail_body" $mail_attachments -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" rm -f $HOME/${config_name}.conf rm -f $HOME/${config_name}-WireGuard-qr.png rm -f $HOME/CertificateURL-qr.png ;; esac echo "Invite complete." } # VPN Join functions function createNetworkJoin() { is_connect="$1" refreshSudo set +e join_file=$HOME/joinvpn_hs.cnf showMessageBox "Paste Configuration" "In this next step, you will need to paste your configuration file contents that you received via email. After pasting the contents, press CTRL-o to save, then Enter to confirm, then CTRL-x to exit. Please remember these key combinations. Press OK to proceed." nano $join_file if ! [ -f $join_file ]; then showMessageBox "Empty Config File" "The configuration file is empty, returning..." return 1 fi performNetworkJoin "$is_connect" "$join_file" rm -f $join_file if [ $? -ne 0 ]; then showMessageBox "Config File Error" "There was an error with the config file, returning..." return 1 fi } function performNetworkJoin() { is_connect="$1" join_file="$2" echo "Joining network..." # First check if there are any shell expansions in the file. # If there is, throw up a big red flag. check_file=$(checkFileShellExpansion $join_file) if ! [ -z "$check_file" ]; then echo -e "$check_file" return 2 fi # Do basic checks removeSpecialChars "$join_file" first_line="$(awk 'NF{print;exit}' $join_file)" last_line="$(awk 'NF{s=$0}END{print s}' $join_file)" if ! [ "$first_line" = "$INVITATION_FIRST_LINE" ] || ! [ "$last_line" = "$INVITATION_LAST_LINE" ]; then echo "ERROR: Incomplete invitation." return 8 fi # Check right away for matching RequestID request_id=$(getValueFromConfig "RequestID" $join_file) if [ -z "$request_id" ]; then echo "ERROR: Invalid request ID." return 8 fi if ! [ -f $HSHQ_WIREGUARD_DIR/requestkeys/$request_id ]; then echo "ERROR: No matching key for this request." return 8 fi base_config_section=$(getTextBetweenStrings $join_file "Base Config Begin" "Base Config End") wireguard_config_section=$(getTextBetweenStrings $join_file "WireGuard Config Begin" "WireGuard Config End") dns_section=$(getTextBetweenStrings $join_file "RemoteServers DNS Begin" "RemoteServers DNS End") root_ca_section=$(getTextBetweenStrings $join_file "Root CA Begin" "Root CA End") mail_crt_section=$(getTextBetweenStrings $join_file "mail.crt Begin" "mail.crt End") mail_key_section=$(getTextBetweenStrings $join_file "mail.key Begin" "mail.key End") join_base_config_file=$HOME/joinvpn_base_config.cnf echo -e "$base_config_section" > $join_base_config_file priv_key=$(getValueFromConfig "PrivateKey" $HSHQ_WIREGUARD_DIR/requestkeys/$request_id) req_conn_type=$(getValueFromConfig "ConnectionType" $HSHQ_WIREGUARD_DIR/requestkeys/$request_id) req_is_primary=$(getValueFromConfig "IsPrimary" $HSHQ_WIREGUARD_DIR/requestkeys/$request_id) conn_type=$(getValueFromConfig "ConnectionType" $join_base_config_file) if ! [ "$conn_type" = "$req_conn_type" ]; then echo "ERROR: Apply/join connection type mismatch. You applied for $req_conn_type, but invitation has $conn_type." rm -f $join_base_config_file return 8 fi interface_name=$(getValueFromConfig "InterfaceName" $join_base_config_file) if [ -z "$interface_name" ] || [ $(checkValidString "$interface_name" "-") = "false" ]; then echo "ERROR: Invalid interface name." rm -f $join_base_config_file return 8 fi # Version 121 - Put vpn- and ext- at beginning of interface name rather than the end if [ "${interface_name: -4}" = "-vpn" ] && ! [ "${interface_name:0:4}" = "vpn-" ]; then interface_name="vpn-${interface_name%????}" fi if [ "${interface_name: -4}" = "-ext" ] && ! [ "${interface_name:0:4}" = "ext-" ]; then interface_name="ext-${interface_name%????}" fi email_address=$(getValueFromConfig "EmailAddress" $join_base_config_file) if [ -z "$email_address" ] || [ $(checkValidEmail "$email_address") = "false" ]; then echo "ERROR: Invalid email address." rm -f $join_base_config_file return 8 fi domain_name=$(getValueFromConfig "DomainName" $join_base_config_file) if [ -z "$domain_name" ] || [ $(checkValidBaseDomain "$domain_name") = "false" ]; then echo "ERROR: Invalid domain name." rm -f $join_base_config_file return 8 fi endpoint_hostname=$(getValueFromConfig "EndpointHostname" $join_base_config_file) if [ -z "$endpoint_hostname" ] || [ $(checkValidString "$endpoint_hostname" ".-") = "false" ]; then echo "ERROR: Invalid endpoint hostname." rm -f $join_base_config_file return 8 fi endpoint_port=$(getValueFromConfig "EndpointPort" $join_base_config_file) if [ -z "$endpoint_port" ] || [ $(checkValidNumber "$endpoint_port") = "false" ]; then echo "ERROR: Invalid endpoint port." rm -f $join_base_config_file return 8 fi client_ip=$(getValueFromConfig "ClientIP" $join_base_config_file) if [ -z "$client_ip" ] || [ $(checkValidIPAddress "$client_ip") = "false" ]; then echo "ERROR: Invalid client IP address." rm -f $join_base_config_file return 8 fi client_public_key=$(getValueFromConfig "ClientPublicKey" $join_base_config_file) if [ -z "$client_public_key" ] || [ "$(checkValidWireGuardKey $client_public_key)" = "false" ]; then echo "ERROR: Invalid client public key." rm -f $join_base_config_file return 8 fi relayserver_public_key=$(getValueFromConfig "RelayServerPublicKey" $join_base_config_file) if [ -z "$relayserver_public_key" ] || [ "$(checkValidWireGuardKey $relayserver_public_key)" = "false" ]; then echo "ERROR: Invalid RelayServer public key." rm -f $join_base_config_file return 8 fi if ! [ -z "$(getWGNameFromPubkey $client_public_key)" ]; then echo "ERROR: Duplicate client public key." rm -f $join_base_config_file return 8 fi preshared_key=$(getValueFromConfig "PresharedKey" $join_base_config_file) if [ -z "$preshared_key" ]; then echo "ERROR: Invalid preshared key." rm -f $join_base_config_file return 8 fi case "$conn_type" in "HomeServer VPN") cur_hs_name=$(getValueFromConfig "HomeServerName" $join_base_config_file) if [ -z "$cur_hs_name" ]; then echo "ERROR: Invalid HomeServer name." rm -f $join_base_config_file return 8 fi ext_prefix=$(getValueFromConfig "ExternalPrefix" $join_base_config_file) if [ -z "$ext_prefix" ]; then echo "ERROR: Invalid external prefix." rm -f $join_base_config_file return 8 fi int_prefix=$(getValueFromConfig "InternalPrefix" $join_base_config_file) if [ -z "$int_prefix" ]; then echo "ERROR: Invalid internal prefix." rm -f $join_base_config_file return 8 fi ca_abbrev=$(getValueFromConfig "CertificateAuthorityAbbrev" $join_base_config_file) if [ -z "$ca_abbrev" ]; then echo "ERROR: Invalid CA abbrev." rm -f $join_base_config_file return 8 fi ca_ip=$(getValueFromConfig "CertificateAuthorityIP" $join_base_config_file) if [ -z "$ca_ip" ] || [ $(checkValidIPAddress "$ca_ip") = "false" ]; then echo "ERROR: Invalid CA IP address." rm -f $join_base_config_file return 8 fi ca_subdomain=$(getValueFromConfig "CertificateAuthoritySubdomain" $join_base_config_file) if [ -z "$ca_subdomain" ] || [ $(checkValidString "$ca_subdomain" ".-") = "false" ]; then echo "ERROR: Invalid CA subdomain." rm -f $join_base_config_file return 8 fi ca_url=$(getValueFromConfig "CertificateAuthorityURL" $join_base_config_file) if [ -z "$ca_url" ]; then echo "ERROR: Invalid CA URL." rm -f $join_base_config_file return 8 fi vpn_subnet=$(getValueFromConfig "VPNSubnet" $join_base_config_file) if [ -z "$vpn_subnet" ]; then echo "ERROR: Invalid VPN subnet." rm -f $join_base_config_file return 8 fi rs_ext_ip=$(getValueFromConfig "RelayServerExternalIP" $join_base_config_file) if [ -z "$rs_ext_ip" ] || [ $(checkValidIPAddress "$rs_ext_ip") = "false" ]; then echo "ERROR: Invalid RelayServer external IP address." rm -f $join_base_config_file return 8 fi rs_vpn_ip=$(getValueFromConfig "RelayServerVPNIP" $join_base_config_file) if [ -z "$rs_vpn_ip" ] || [ $(checkValidIPAddress "$rs_vpn_ip") = "false" ]; then echo "ERROR: Invalid RelayServer VPN IP address." rm -f $join_base_config_file return 8 fi is_primary=$(getValueFromConfig "IsPrimary" $join_base_config_file) if [ -z "$is_primary" ] || ! ( [ "$is_primary" = "true" ] || [ "$is_primary" = "false" ] ); then echo "ERROR: Invalid primary VPN selection." rm -f $join_base_config_file return 8 fi conn_type=$(getValueFromConfig "ConnectionType" $join_base_config_file) if ! [ "$is_primary" = "$req_is_primary" ]; then if [ "$is_primary" = "true" ]; then echo "ERROR: Apply/join IsPrimary mismatch. You applied for a non-primary connection, but the invitation indicates a primary connection." else echo "ERROR: Apply/join IsPrimary mismatch. You applied for a primary connection, but the invitation indicates a non-primary connection." fi rm -f $join_base_config_file return 8 fi if [ "$is_primary" = "true" ]; then if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are already hosting a VPN, cannot join another network as primary." rm -f $join_base_config_file return 8 fi smtp_relay_host=$(getValueFromConfig "SMTPRelayHost" $join_base_config_file) if [ -z "$smtp_relay_host" ]; then echo "ERROR: Invalid SMTP relay host." rm -f $join_base_config_file return 8 fi smtp_relay_username=$(getValueFromConfig "SMTPRelayUsername" $join_base_config_file) if [ -z "$smtp_relay_username" ]; then echo "ERROR: Invalid SMTP username." rm -f $join_base_config_file return 8 fi smtp_relay_password=$(getValueFromConfig "SMTPRelayPassword" $join_base_config_file) if [ -z "$smtp_relay_password" ]; then echo "ERROR: Invalid SMTP password." rm -f $join_base_config_file return 8 fi rs_ext_email_hostname=$(getValueFromConfig "RelayServerExtEmailHostname" $join_base_config_file) if [ -z "$rs_ext_email_hostname" ]; then echo "ERROR: Invalid RelayServer external email hostname." rm -f $join_base_config_file return 8 fi fi ;; "HomeServer Internet") endpoint_pub_key=$(getValueFromConfig "EndpointPublicKey" $join_base_config_file) if [ -z "$endpoint_pub_key" ]; then echo "ERROR: Invalid endpoint public key." rm -f $join_base_config_file return 8 fi mtu=$(getValueFromConfig "MTU" $join_base_config_file) if [ -z "$mtu" ]; then echo "ERROR: Invalid MTU." rm -f $join_base_config_file return 8 fi ;; *) echo "ERROR: Connection type not found." rm -f $join_base_config_file return 8 ;; esac # Do logical checks if [ "$domain_name" = "$HOMESERVER_DOMAIN" ]; then echo "ERROR: This is your domain, dummy!" rm -f $join_base_config_file return 8 fi if ! [ "$(isEmailMatchBaseDomain $email_address $domain_name)" = "true" ]; then echo "ERROR: Join email is not from the same base domain. Email: $email_address, Domain: $domain_name" return 8 fi if [ "$(checkInterfaceNameExists $interface_name)" = "true" ]; then echo "ERROR: An interface with this name already exists. Change the InterfaceName value to something unique (using 10 characters or less)." rm -f $join_base_config_file return 8 fi if [ ${#interface_name} -gt 10 ]; then echo "ERROR: Change the InterfaceName value to something shorter, no more than 10 characters, exiting..." rm -f $join_base_config_file return 8 fi my_pub_key=$(echo $priv_key | wg pubkey) if ! [ "$client_public_key" = "$my_pub_key" ]; then echo "ERROR: The private key does not align with the public key submitted in the application." rm -f $join_base_config_file return 8 fi is_db=$(getWGNameFromIP $client_ip) if ! [ -z "$is_db" ]; then echo "ERROR: This IP address is already being used (Connection Name: $is_db). Please request a new one." rm -f $join_base_config_file return 8 fi case "$conn_type" in "HomeServer VPN") check_ca_dom=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections join connections on hsvpn_connections.ID = connections.ID where hsvpn_connections.DomainName='$domain_name' and connections.NetworkType='other';") if ! [ -z "$check_ca_dom" ]; then echo "ERROR: There is already an existing connection with this domain ($domain_name), please disconnect that existing connection and try again." rm -f $join_base_config_file return 8 fi vpn_subnet=$(getValueFromConfig VPNSubnet $join_base_config_file) # Check for intersection with our networks echo "Checking for network collision..." check_intersect="$(isNetworkIntersectOurNetworks $vpn_subnet false true)" if ! [ -z "$check_intersect" ]; then echo "ERROR: Network collision: $check_intersect" rm -f $join_base_config_file return 8 fi echo "No collision." join_rootca_name="$(getCACertificateNameFromDomain $domain_name)" join_rootca_file=$HOME/$join_rootca_name echo -e "$root_ca_section" > $join_rootca_file openssl x509 -in $join_rootca_file -noout -text 1> /dev/null 2>&1 check_cert=$? if [ $check_cert -ne 0 ]; then echo "ERROR: There was an unknown error parsing the provided root certificate, exiting..." rm -f $join_base_config_file rm -f $join_rootca_file return 8 fi join_wireguard_config_file=$HOME/${interface_name}.conf if [ -z "$relayserver_public_key" ]; then # The one var missing, so the ensures compatibility with older versions echo -e "$wireguard_config_section" > /tmp/wgtemp.conf relayserver_public_key=$(awk -F '= ' '/^PublicKey/ {print $2}' /tmp/wgtemp.conf) rm -f /tmp/wgtemp.conf fi nextVPNTableID=$(getNextHSVPNRoutingTable) sudo tee $join_wireguard_config_file >/dev/null < $HOME/mail-cert.crt openssl x509 -in $HOME/mail-cert.crt -noout -text 1> /dev/null 2>&1 check_cert=$? if [ $check_cert -ne 0 ]; then echo "ERROR: There was an unknown error parsing the provided mail certificate." rm -f $join_base_config_file rm -f $join_rootca_file rm -f $join_wireguard_config_file rm -f $HOME/mail-cert.crt return 8 fi echo -e "$mail_key_section" > $HOME/mail-key.key openssl rsa -in $HOME/mail-key.key -check 1> /dev/null 2>&1 check_cert=$? if [ $check_cert -ne 0 ]; then echo "ERROR: There was an unknown error parsing the provided mail certificate key, exiting..." rm -f $join_base_config_file rm -f $join_rootca_file rm -f $join_wireguard_config_file rm -f $HOME/mail-cert.crt rm -f $HOME/mail-key.key return 8 fi mv -f $HOME/mail-cert.crt $HSHQ_SSL_DIR/mail.crt mv -f $HOME/mail-key.key $HSHQ_SSL_DIR/mail.key chmod 0444 $HSHQ_SSL_DIR/mail.crt chmod 0444 $HSHQ_SSL_DIR/mail.key SMTP_RELAY_HOST="$smtp_relay_host" updateConfigVar SMTP_RELAY_HOST $SMTP_RELAY_HOST SMTP_RELAY_USERNAME="$smtp_relay_username" updateConfigVar SMTP_RELAY_USERNAME $SMTP_RELAY_USERNAME SMTP_RELAY_PASSWORD="$smtp_relay_password" updateConfigVar SMTP_RELAY_PASSWORD $SMTP_RELAY_PASSWORD RELAYSERVER_SERVER_IP="$rs_ext_ip" updateConfigVar RELAYSERVER_SERVER_IP $RELAYSERVER_SERVER_IP RELAYSERVER_EXT_EMAIL_HOSTNAME=$rs_ext_email_hostname updateConfigVar RELAYSERVER_EXT_EMAIL_HOSTNAME $RELAYSERVER_EXT_EMAIL_HOSTNAME PRIMARY_VPN_SETUP_TYPE=join updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE fi dns_file=$HOME/dns.tmp echo -e "$dns_section" > $dns_file curdt=$(getCurrentDate) db_id=$(sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated,Network_Subnet,IsExposeToNetwork,InputAllowPorts,DockerUserAllowPorts) values('$config_name','$email_address','homeserver_vpn','$net_type','$my_pub_key','$preshared_key','$client_ip',false,'$interface_name','$endpoint_hostname','$curdt','$vpn_subnet',true,'$input_ports_list','$docker_user_ports_list');select last_insert_rowid();") updateDeviceIPMappingFile sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into hsvpn_connections(ID,HomeServerName,IsPrimary,DomainName,ExternalPrefix,InternalPrefix,CA_Abbrev,CA_IP,CA_Subdomain,CA_URL,RS_VPN_IP,VPNRoutingTable) values($db_id,'$cur_hs_name','$isPrimary','$domain_name','$ext_prefix','$int_prefix','$ca_abbrev','$ca_ip','$ca_subdomain','$ca_url','$rs_vpn_ip',$nextVPNTableID);" checkUpdateAllIPTables performNetworkJoin if [ "$IS_INSTALLED" = "true" ]; then echo "Updating HomeServer DNS and restarting Heimdall..." docker container stop heimdall >/dev/null updateHomeServerDNS $dns_file docker container start heimdall >/dev/null rm -f $dns_file fi echo "Creating WireGuard config..." JOINED_DB_ID=$db_id rm -f $join_base_config_file sudo mv $join_wireguard_config_file $HSHQ_WIREGUARD_DIR/vpn/${interface_name}.conf sudo chown root:root $HSHQ_WIREGUARD_DIR/vpn/${interface_name}.conf sudo chmod 0400 $HSHQ_WIREGUARD_DIR/vpn/${interface_name}.conf sudo cp $HSHQ_WIREGUARD_DIR/vpn/${interface_name}.conf /etc/wireguard/${interface_name}.conf echo "Installing CA certificate..." sudo mv $join_rootca_file $HSHQ_SSL_DIR/$join_rootca_name sudo chmod 0444 $HSHQ_SSL_DIR/$join_rootca_name sudo cp $HSHQ_SSL_DIR/$join_rootca_name /usr/local/share/ca-certificates/ sudo update-ca-certificates echo "Updating services due to new CA certificate..." if [ "$IS_INSTALLED" = "true" ]; then docker ps | grep nextcloud-app > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "Updating CA certificates in Nextcloud..." docker exec -u www-data nextcloud-app php occ --no-warnings security:certificates:import /usr/local/share/ca-certificates/$join_rootca_name fi #echo "Restarting HomeAssistant (if running) due to new CA certificate..." #restartStackIfRunning homeassistant 10 fi set -e if [ "$is_connect" = "true" ]; then connectVPN $db_id true fi ;; "HomeServer Internet") dockerNetworkName="dwg-${interface_name}" config_name="Other-Internet-$domain_name" checkname_db=$(sqlite3 $HSHQ_DB "select Name from connections where Name='$config_name';") if ! [ -z "$checkname_db" ]; then echo "ERROR: There is already a connection with this name." rm -f $join_base_config_file return 8 fi # Check for intersection with our networks echo "Checking for network collision..." check_intersect="$(isNetworkIntersectOurNetworks ${client_ip}/32 false true)" if ! [ -z "$check_intersect" ]; then echo "ERROR: Network collision: $check_intersect" rm -f $join_base_config_file return 8 fi echo "No collision." join_wireguard_config_file=$HOME/${interface_name}.conf tableid=$(getNextWGRoutingTableID) allowed_ips=$(getAllowedPublicIPs) dockerSubnet=$(getNextAvailableWGDockerNetwork $dockerNetworkName $mtu) echo -e "#NETNAME=$interface_name" > $join_wireguard_config_file echo -e "#ROUTING_TABLE_ID=$tableid" >> $join_wireguard_config_file echo -e "#DOCKER_NETWORK_NAME=$dockerNetworkName" >> $join_wireguard_config_file echo -e "#DOCKER_NETWORK_SUBNET=$dockerSubnet" >> $join_wireguard_config_file echo -e "#CLIENT_ADDRESS=$client_ip" >> $join_wireguard_config_file echo -e "#MTU=$mtu" >> $join_wireguard_config_file echo -e "#IS_ENABLED=true" >> $join_wireguard_config_file echo -e "#EXT_DOMAIN=$endpoint_hostname" >> $join_wireguard_config_file echo -e "\n[Interface]" >> $join_wireguard_config_file echo -e "PrivateKey = $priv_key" >> $join_wireguard_config_file echo -e "\n[Peer]" >> $join_wireguard_config_file echo -e "PublicKey = $endpoint_pub_key" >> $join_wireguard_config_file echo -e "PresharedKey = $preshared_key" >> $join_wireguard_config_file echo -e "AllowedIPs = $allowed_ips" >> $join_wireguard_config_file echo -e "Endpoint = ${endpoint_hostname}:${endpoint_port}" >> $join_wireguard_config_file echo -e "PersistentKeepalive = $RELAYSERVER_PERSISTENT_KEEPALIVE" >> $join_wireguard_config_file chmod 0400 $join_wireguard_config_file sudo chown root:root $join_wireguard_config_file sudo mv $join_wireguard_config_file $HSHQ_WIREGUARD_DIR/internet/${interface_name}.conf rm -f $join_base_config_file curdt=$(getCurrentDate) db_id=$(sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated) values('$config_name','$email_address','homeserver_internet','other','$my_pub_key','$preshared_key','$client_ip',true,'$interface_name','$endpoint_hostname','$curdt');select last_insert_rowid();") JOINED_DB_ID=$db_id updateDeviceIPMappingFile set -e if [ "$is_connect" = "true" ]; then connectInternet $db_id fi ;; esac rm -f $HSHQ_WIREGUARD_DIR/requestkeys/$request_id } # VPN Remove functions function removeMyNetworkPrimaryVPN() { disconnect_reason="$1" db_id=$(getPrimaryVPN_DBID) if ! [ -z "$db_id" ]; then client_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$db_id;") ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") ca_abbrev=$(sqlite3 $HSHQ_DB "select CA_Abbrev from hsvpn_connections where ID=$db_id;") domain_name=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$db_id;") ext_prefix=$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$db_id;") int_prefix=$(sqlite3 $HSHQ_DB "select InternalPrefix from hsvpn_connections where ID=$db_id;") ca_ip=$(sqlite3 $HSHQ_DB "select CA_IP from hsvpn_connections where ID=$db_id;") ca_subdomain=$(sqlite3 $HSHQ_DB "select CA_Subdomain from hsvpn_connections where ID=$db_id;") ca_url=$(sqlite3 $HSHQ_DB "select CA_URL from hsvpn_connections where ID=$db_id;") vpn_subnet=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections where ID=$db_id;") rs_vpn_ip=$(sqlite3 $HSHQ_DB "select RS_VPN_IP from hsvpn_connections where ID=$db_id;") # Do notifications first, since this will tear down our network. if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then # Notify everyone that they have been removed notifyMyNetworkFullRemoval echo "Waiting 30 seconds to proceed so that Disconnect Notice email(s) can get sent..." sleep 30 elif [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then host_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$db_id;") # Notify host that you have disconnected. email_subj="HomeServer Disconnect Notice from $HOMESERVER_NAME" email_body="" email_body=${email_body}"HomeServer Disconnect Notice from $HOMESERVER_NAME\n" email_body=${email_body}"================================================================\n\n" email_body=${email_body}"Domain: $HOMESERVER_DOMAIN\n" email_body=${email_body}"Reason Provided: $disconnect_reason\n\n" sendEmail -s "$email_subj" -b "$email_body" -t "$host_email" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" echo "Waiting 30 seconds to proceed so that Disconnect Notice email(s) can get sent..." sleep 30 fi fi refreshSudo if sudo test -f /etc/wireguard/${ifaceName}.conf; then echo "Removing WireGuard interface: $ifaceName" removeWGInterfaceQuick $ifaceName fi sudo rm -f $HSHQ_WIREGUARD_DIR/vpn/${ifaceName}.conf deleteDomainAdguardHS "*.$int_prefix.$domain_name" deleteDomainAdguardHS "*.$ext_prefix.$domain_name" addDomainAdguardHS "*.$ext_prefix.$domain_name" "A" checkDeleteStackAndDirectory caddy-$ifaceName "Caddy" true true removeHomeNetIP ${RELAYSERVER_SERVER_IP}/32 true docker container stop uptimekuma >/dev/null docker container stop heimdall >/dev/null if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "Removing Wazuh agent..." sudo sqlite3 $HSHQ_DB "drop table portforwarding;" removeRelayServerAgentFromWazuhManager echo "Removing Syncthing backup..." curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X DELETE -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/folders/$RELAYSERVER_SYNCTHING_FOLDER_ID > /dev/null 2>&1 curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X DELETE -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices/$RELAYSERVER_SYNCTHING_DEVICE_ID > /dev/null 2>&1 echo "Removing ClientDNS instances..." cdns_arr=($(sqlite3 $HSHQ_DB "select ID,Name from connections where ConnectionType='clientdns' and NetworkType in ('primary','mynetwork');")) for cur_cdns in "${cdns_arr[@]}" do cur_cdns_id=$(echo "$cur_cdns" | cut -d"|" -f1) cur_cdns_name=$(echo "$cur_cdns" | cut -d"|" -f2) performMyNetworkRemoveClientDNS $cur_cdns_id "$cur_cdns_name" false false done sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${RELAYSERVER_WG_INTERNET_NETNAME}.conf down sudo rm -f $HSHQ_WIREGUARD_DIR/internet/${RELAYSERVER_WG_INTERNET_NETNAME}.conf docker container restart syncthing # Remove any DNS entries resulting from our network. sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from hsvpn_dns where HostDomain='$HOMESERVER_DOMAIN';" vpn_arr=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='mynetwork';")) for curID in "${vpn_arr[@]}" do curDomain=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$curID;") curDomainExtPrefix=$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$curID;") curMailHostID=$(sqlite3 $HSHQ_DB "select MailHostID from hsvpn_connections where ID=$curID;") if ! [ -z "$curMailHostID" ]; then sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhostmap where MailHostID=$curMailHostID;" sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhosts where ID=$curMailHostID;" fi removeRevertDNS $curDomain $curDomainExtPrefix done curMailHostID=$(sqlite3 $HSHQ_DB "select MailHostID from hsvpn_connections join connections on connections.ID = hsvpn_connections.ID where ConnectionType = 'homeserver_vpn' and NetworkType='primary';") if ! [ -z "$curMailHostID" ]; then sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhostmap where MailHostID=$curMailHostID;" sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhosts where ID=$curMailHostID;" fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains;" sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from exposedomains;" deleteSvcAll relayserver "https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false deleteSvcAll relayserver "https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false deleteSvcAll relayserver "https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false deleteSvcAll relayserver "https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false deleteSvcAll relayserver "https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false deleteSvcAll relayserver "https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false deleteSvcHeimdall relayserver "https://$SUB_CLIENTDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false deleteSvcHeimdall relayserver "https://$SUB_CADDYDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false elif [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then sudo rm -f $HSHQ_SSL_DIR/"$(getCACertificateNameFromDomain $domain_name)" sudo rm -f /usr/local/share/ca-certificates/"$(getCACertificateNameFromDomain $domain_name)" sudo update-ca-certificates dns_arr=($(sqlite3 $HSHQ_DB "select PeerDomain,PeerDomainExtPrefix from hsvpn_dns where HostDomain='$domain_name';")) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from hsvpn_dns where HostDomain='$domain_name';" for cur_dns in "${dns_arr[@]}" do curPeerDomain=$(echo "$cur_dns" | cut -d "|" -f1) curPeerDomainExtPrefix=$(echo "$cur_dns" | cut -d "|" -f2) removeRevertDNS $curPeerDomain $curPeerDomainExtPrefix deleteSvcHeimdall homeservers https://$SUB_HSHQHOME.$curPeerDomain false done fi docker container start uptimekuma >/dev/null docker container start heimdall >/dev/null PRIMARY_VPN_SETUP_TYPE=none updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE resetRelayServerData updateSvcsIPChanges checkUpdateAllIPTables removeMyNetworkPrimaryVPN outputStackListsScriptServer } function removeMyNetworkHomeServerVPNConnection() { db_id=$1 removal_reason="$2" # Multiple queries is wasteful...but easier... pub_key=$(sqlite3 $HSHQ_DB "select PublicKey from connections where ID=$db_id;") ip_addr=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$db_id;") peer_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$db_id;") domain_name=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$db_id;") hs_name=$(sqlite3 $HSHQ_DB "select HomeServerName from hsvpn_connections where ID=$db_id;") is_primary=$(sqlite3 $HSHQ_DB "select IsPrimary from hsvpn_connections where ID=$db_id;") mail_host_id=$(sqlite3 $HSHQ_DB "select MailHostID from hsvpn_connections where ID=$db_id;") domain_ext_prefix=$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$db_id;") if [ -z "$pub_key" ]; then echo "ERROR: No matching DB entry, returning..." return fi # Notify peer that you are removing them. email_subj="HomeServer Removal Notice from $HOMESERVER_NAME" email_body="" email_body=${email_body}"HomeServer Removal Notice from $HOMESERVER_NAME\n" email_body=${email_body}"================================================================\n\n" email_body=${email_body}"Your HomeServer connection has been removed from the network.\n" email_body=${email_body}"Ensure to disconnect accordingly.\n\n" email_body=${email_body}"Host Domain: $HOMESERVER_DOMAIN\n" email_body=${email_body}"Reason Provided: $removal_reason\n\n" sendEmail -s "$email_subj" -b "$email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$peer_email" if [ $is_primary = 1 ]; then echo "Waiting 30 seconds to proceed so that Removal Notice email(s) can get sent..." sleep 30 fi wgPortalAuth="$(getWGPortalAuth)" perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/removePeer.sh \"$pub_key\" \"$ip_addr\" \"false\" \"true\" \"$wgPortalAuth\"" -f if [ $? -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an error adding the peer, returning..." return fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from hsvpn_dns where HostDomain='$HOMESERVER_DOMAIN' and PeerDomain='$domain_name';" removeRevertDNS $domain_name $domain_ext_prefix deleteDomainAndWildcardAdguardRS $domain_name deleteDomainAdguardRS "*.$domain_ext_prefix.$domain_name" deleteSvcHeimdall homeservers "https://$SUB_HSHQHOME.$domain_name" true if [ $is_primary = 1 ]; then domains_to_remove=$(sqlite3 $HSHQ_DB "select Domain from mailhostmap where MailHostID=$mail_host_id and IsFirstDomain=true;") domains_to_rem_qry=($(sqlite3 $HSHQ_DB "select Domain from mailhostmap where MailHostID=$mail_host_id and IsFirstDomain=false;")) for curdom in "${domains_to_rem_qry[@]}" do domains_to_remove="${domains_to_remove},$curdom" done deliver_to_host=$(sqlite3 $HSHQ_DB "select MailHost from mailhosts where ID=$mail_host_id;") perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeRelayedDomains.sh $domains_to_remove $deliver_to_host $domain_name" -f if [ $? -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an error removing the peer, returning..." return fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhostmap where MailHostID=$mail_host_id;" sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhosts where ID=$mail_host_id;" removeSecondaryDomainFromRelayServer "$domain_name" fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from connections where ID=$db_id;" updateDeviceIPMappingFile # Send update email to other RemoteServers on our network notifyMyNetworkHomeServersDNSUpdate remove "$hs_name" "$domain_name" # Send update email to other users on our network notifyMyNetworkUsersDNSUpdate remove "$hs_name" "$domain_name" "$domain_ext_prefix" "$ip_addr" } function removeMyNetworkHomeServerInternetConnection() { db_id=$1 removal_reason="$2" echo "Removing $db_id" peer_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$db_id;") removeMyNetworkNonHomeServerConnection $db_id echo "Sending email to $peer_email" notifyHomeServerInternetNetworkRemoval "$peer_email" "$removal_reason" } function removeMyNetworkUserConnection() { db_id=$1 removal_reason="$2" peer_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$db_id;") removeMyNetworkNonHomeServerConnection $db_id echo "Sending email to $peer_email, reason: $removal_reason" notifyUserNetworkRemoval "$peer_email" "$removal_reason" } function removeMyNetworkNonHomeServerConnection() { db_id=$1 is_vpn=$2 pub_key=$(sqlite3 $HSHQ_DB "select PublicKey from connections where ID=$db_id;") ip_addr=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$db_id;") is_int=$(sqlite3 $HSHQ_DB "select IsInternet from connections where ID=$db_id;") is_internet="false" if [ $is_int = 1 ]; then is_internet="true" fi is_in_vpn=false if ! [ -z "$is_vpn" ] && [ "$is_vpn" = "true" ]; then is_in_vpn=true fi wgPortalAuth="$(getWGPortalAuth)" perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/removePeer.sh \"$pub_key\" \"$ip_addr\" \"$is_internet\" \"$is_in_vpn\" \"$wgPortalAuth\"" -f if [ $? -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an error adding the peer, returning..." return fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from connections where ID=$db_id;" updateDeviceIPMappingFile } # VPN Disconnect functions function disconnectOtherNetworkHomeServerVPNConnection() { db_id=$1 disconnect_reason="$2" ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") client_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$db_id;") host_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$db_id;") domain_name=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$db_id;") ext_prefix=$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$db_id;") int_prefix=$(sqlite3 $HSHQ_DB "select InternalPrefix from hsvpn_connections where ID=$db_id;") ca_ip=$(sqlite3 $HSHQ_DB "select CA_IP from hsvpn_connections where ID=$db_id;") if sudo test -f /etc/wireguard/${ifaceName}.conf; then removeWGInterfaceQuick $ifaceName else echo "ERROR: WireGuard config file not found: /etc/wireguard/${ifaceName}.conf" fi sudo rm -f $HSHQ_WIREGUARD_DIR/vpn/${ifaceName}.conf sudo rm -f $HSHQ_SSL_DIR/"$(getCACertificateNameFromDomain $domain_name)" sudo rm -f /usr/local/share/ca-certificates/"$(getCACertificateNameFromDomain $domain_name)" sudo update-ca-certificates checkDeleteStackAndDirectory caddy-$ifaceName "Caddy" true true rm -f $HSHQ_STACKS_DIR/caddy-common/caddyfiles/CaddyfileBody-caddy-$ifaceName deleteDomainAdguardHS "*.$int_prefix.$domain_name" sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from connections where ID=$db_id;" updateDeviceIPMappingFile peer_list=($(sqlite3 $HSHQ_DB "select PeerDomain,PeerDomainExtPrefix,IsActive from hsvpn_dns where HostDomain='$domain_name';")) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from hsvpn_dns where HostDomain='$domain_name';" for cur_peer in "${peer_list[@]}" do curPeerDomain=$(echo "$cur_peer" | cut -d "|" -f1) curPeerDomainExtPrefix=$(echo "$cur_peer" | cut -d "|" -f2) curIsActive=$(echo "$cur_peer" | cut -d "|" -f3) if [ $curIsActive = 1 ]; then removeRevertDNS $curPeerDomain $curPeerDomainExtPrefix fi done updateSvcsIPChanges checkUpdateAllIPTables disconnectOtherNetworkHomeServerVPNConnection # Notify host that you have disconnected. email_subj="HomeServer VPN Disconnect Notice from $HOMESERVER_NAME" email_body="" email_body=${email_body}"HomeServer VPN Disconnect Notice from $HOMESERVER_NAME\n" email_body=${email_body}"================================================================\n\n" email_body=${email_body}"Domain: $HOMESERVER_DOMAIN\n" email_body=${email_body}"Reason Provided: $disconnect_reason\n\n" sendEmail -s "$email_subj" -b "$email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$host_email" } function disconnectOtherNetworkHomeServerInternetConnection() { db_id=$1 disconnect_reason="$2" set +e wg_config=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") host_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$db_id;") sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${wg_config}.conf down del_network=$(sudo grep ^\#DOCKER_NETWORK_NAME= $HSHQ_WIREGUARD_DIR/internet/${wg_config}.conf | sed 's/^[^=]*=//') docker network rm $del_network > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ERROR: Could not remove the docker network associated with this connection. There is likely one or more containers still attached to it. Ensure to shut down the container(s) and manually delete the network ($del_network)." fi sudo rm -f $HSHQ_WIREGUARD_DIR/internet/${wg_config}.conf sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from connections where ID=$db_id;" updateDeviceIPMappingFile # Notify host that you have disconnected. email_subj="HomeServer Internet Disconnect Notice from $HOMESERVER_NAME" email_body="" email_body=${email_body}"HomeServer Internet Disconnect Notice from $HOMESERVER_NAME\n" email_body=${email_body}"================================================================\n\n" email_body=${email_body}"Domain: $HOMESERVER_DOMAIN\n" email_body=${email_body}"Reason Provided: $disconnect_reason\n\n" sendEmail -s "$email_subj" -b "$email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$host_email" } function changeHSInternetPrimaryIPAddress() { # Just in case there's a network collision, allow # user to change the IP address rather than having # to tear down the entire hosted VPN structure. if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi new_ip=$1 echo "Checking for network collision..." check_intersect="$(isNetworkIntersectOurNetworks ${new_ip}/32 false true)" if ! [ -z "$check_intersect" ]; then echo "ERROR: Network collision: $check_intersect" return 2 fi echo "No collision." if [ "$(isIPInSubnet $new_ip 10.0.0.0/8)" = "false" ]; then echo "ERROR: IP not in the 10.0.0.0/8 range." return 3 fi db_id=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_internet' and NetworkType='primary';") pub_key=$(sqlite3 $HSHQ_DB "select PublicKey from connections where ID=$db_id;") cur_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$db_id;") interface_name=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;") wgPortalAuth="$(getWGPortalAuth)" perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/removePeer.sh \"$pub_key\" \"$cur_ip\" \"true\" \"false\" \"$wgPortalAuth\"" -f if [ $? -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an unknown error, returning..." return 4 fi perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh \"Primary-Internet-$HOMESERVER_DOMAIN\" \"$EMAIL_ADMIN_EMAIL_ADDRESS\" \"$pub_key\" \"$RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY\" \"$new_ip\" \"true\" \"false\" \"$wgPortalAuth\"" -f if [ $? -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an unknown error, returning..." return 5 fi sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${interface_name}.conf down sudo sed -i "s/^#CLIENT_ADDRESS=.*/#CLIENT_ADDRESS=$new_ip\/32/g" $HSHQ_WIREGUARD_DIR/internet/${interface_name}.conf sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${interface_name}.conf up sudo sqlite3 $HSHQ_DB "update connections set IPAddress='$new_ip' where ID=$db_id;" } function changeDeviceIPAddress() { # Just in case there's a network collision, allow # user to change the IP address rather than having # to redo the apply/invite. if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi db_id=$1 new_ip=$2 is_db=$(getWGNameFromIP $new_ip) if ! [ -z "$is_db" ]; then echo "ERROR: This IP address is already being used (Connection Name: $is_db)." return 2 fi is_in_subnet=$(isIPInSubnet $new_ip $PRIMARY_VPN_SUBNET) if [ "$is_in_subnet" = "true" ]; then echo "ERROR: This IP address is inside of your VPN range designated for hosting RemoteServers." return 3 fi cur_name=$(sqlite3 $HSHQ_DB "select Name from connections where ID=$db_id;") email_address=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$db_id;") pub_key=$(sqlite3 $HSHQ_DB "select PublicKey from connections where ID=$db_id;") pre_key=$(sqlite3 $HSHQ_DB "select PresharedKey from connections where ID=$db_id;") cur_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$db_id;") isInt=$(sqlite3 $HSHQ_DB "select IsInternet from connections where ID=$db_id;") is_internet="false" if [ "$isInt" = "1" ]; then is_internet="true" fi wgPortalAuth="$(getWGPortalAuth)" perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/removePeer.sh \"$pub_key\" \"$cur_ip\" \"$is_internet\" \"false\" \"$wgPortalAuth\"" -f if [ $? -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an unknown error, returning..." return 4 fi perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/addPeer.sh \"$cur_name\" \"$email_address\" \"$pub_key\" \"$pre_key\" \"$new_ip\" \"$is_internet\" \"false\" \"$wgPortalAuth\"" -f if [ $? -ne 0 ]; then echo "ERROR: Could not connect to RelayServer host or there was an unknown error, returning..." return 5 fi sudo sqlite3 $HSHQ_DB "update connections set IPAddress='$new_ip' where ID=$db_id;" email_subj="User Interface IP Address Change Notice from $HOMESERVER_NAME" email_body="" email_body=$email_body"User Interface IP Address Change Notice from $HOMESERVER_NAME\n" email_body=$email_body"================================================================\n\n" email_body=$email_body"IP address successfully changed.\n\n" email_body=$email_body"Old: $cur_ip\n" email_body=$email_body"New: $new_ip\n" sendEmail -s "$email_subj" -b "$email_body" -t "$email_address" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function outputLockUtilsScript() { rm -f $HSHQ_LIB_DIR/$LOCK_UTILS_FILENAME cat < $HSHQ_LIB_DIR/$LOCK_UTILS_FILENAME #!/bin/bash isDebug=false LOGF=$HSHQ_LOG_FILE LOCKHOLDER_FILENAME=$LOCKHOLDER_FILENAME ALL_LOCKS="$ALL_LOCKS" function tryGetLock() { lockName="\$1" callerName="\$2" cohs_curE=\${-//[^e]/} set +e if [ -z "\$callerName" ]; then callerName=UnknownCaller fi isLockFound=false for curLock in \$ALL_LOCKS do if [ "\$lockName" = "\$curLock" ]; then isLockFound=true break fi done if [ "\$isLockFound" = "false" ]; then if ! [ -z "\$cohs_curE" ]; then set -e fi # Unknown lock name echo "false" return fi # BeforeNetwork is always the first one called during boot, # so this will clean up any mess prior to the last reboot. if [ "\$callerName" = "BeforeNetwork" ]; then releaseLock "\$lockName" "BeforeNetwork" false fi # mkdir is an atomic operation, so its the best mechanism # for performing locking operations if mkdir -- "/tmp/\$lockName" > /dev/null 2>&1; then echo \$callerName > /tmp/locktmp echo \$(date '+%s') >> /tmp/locktmp echo 0 >> /tmp/locktmp chmod 755 /tmp/locktmp chmod 644 /tmp/locktmp mv /tmp/locktmp /tmp/\$lockName/\$LOCKHOLDER_FILENAME if [ "\$isDebug" = "true" ]; then echo "\$(date '+%Y-%m-%d %H:%M:%S.%3N') [debug] tryGetLock - \$lockName lock obtained by \$callerName" >> \$LOGF fi echo "true" else if [ "\$isDebug" = "true" ]; then echo "\$(date '+%Y-%m-%d %H:%M:%S.%3N') [debug] tryGetLock - \$lockName lock denied from \$callerName" >> \$LOGF fi echo "false" fi if ! [ -z "\$cohs_curE" ]; then set -e fi } function forceGetLock() { lockName="\$1" callerName="\$2" if [ -d /tmp/\$lockName ]; then sudo rm -f /tmp/\$lockName/\$LOCKHOLDER_FILENAME else mkdir -- "/tmp/\$lockName" fi echo \$callerName > /tmp/locktmp echo \$(date '+%s') >> /tmp/locktmp echo 0 >> /tmp/locktmp chmod 755 /tmp/locktmp chmod 644 /tmp/locktmp mv /tmp/locktmp /tmp/\$lockName/\$LOCKHOLDER_FILENAME if [ "\$isDebug" = "true" ]; then echo "\$(date '+%Y-%m-%d %H:%M:%S.%3N') [debug] forceGetLock - \$lockName lock obtained by \$callerName" >> \$LOGF fi } function getLockOpenMsg() { lockName="\$1" if test -f /tmp/\$lockName/\$LOCKHOLDER_FILENAME; then lastCaller=\$(sed -n 1p /tmp/\$lockName/\$LOCKHOLDER_FILENAME) lastExecute=\$(sed -n 2p /tmp/\$lockName/\$LOCKHOLDER_FILENAME) lastAttempts=\$(sed -n 3p /tmp/\$lockName/\$LOCKHOLDER_FILENAME) lastExecuteDate=\$(date -d @\${lastExecute} '+%Y-%m-%d %H:%M:%S') dtNow=\$(date '+%s') secondsDiff=\$((\$dtNow - \$lastExecute)) strMessage="" if [ \$secondsDiff -lt 60 ]; then strMessage="Lock obtained \$secondsDiff second(s) ago by" else totMinutes=\$((\$secondsDiff / 60)) strMessage="Lock obtained \$totMinutes minute(s) ago by" fi strMessage="\$strMessage \$lastCaller @ \$lastExecuteDate" else strMessage="Unknown-User" fi echo "\$strMessage" } function getIncrementLockAttempts() { lockName="\$1" if test -f /tmp/\$lockName/\$LOCKHOLDER_FILENAME; then lastAttempts=\$(sed -n 3p /tmp/\$lockName/\$LOCKHOLDER_FILENAME) totAttempts=\$((\$lastAttempts + 1)) sed -i '\$d' /tmp/\$lockName/\$LOCKHOLDER_FILENAME > /dev/null 2>&1 if [ \$? -eq 0 ]; then echo "\$totAttempts" >> /tmp/\$lockName/\$LOCKHOLDER_FILENAME fi echo "\$totAttempts" fi } function checkIsLockEnabled() { lockName="\$1" if [ -d "/tmp/\$lockName" ]; then echo "\$(getLockOpenMsg \$lockName)" fi } function releaseLock() { lockName="\$1" callerName="\$2" isForceRelease="\$3" if [ "\$isForceRelease" = "true" ]; then sudo rm -fr /tmp/\$lockName else rm -fr /tmp/\$lockName fi if [ "\$isDebug" = "true" ]; then echo "\$(date '+%Y-%m-%d %H:%M:%S.%3N') [debug] releaseLock - \$lockName lock released by \$callerName" >> \$LOGF fi } EOFUS chmod 0444 $HSHQ_LIB_DIR/$LOCK_UTILS_FILENAME } function getHSHQScriptOpenMsg() { getLockOpenMsg hshqopen } function checkHSHQScriptOpen() { checkIsLockEnabled hshqopen } function closeHSHQScript() { releaseLock hshqopen "$1" false } function acquireAllLocks() { tgLock="$(tryGetLock networkchecks acquireAllLocks)" if ! [ "$tgLock" = "true" ]; then checkRes="$(getLockOpenMsg networkchecks)" echo "networkchecks: $checkRes" return 23 fi tgLock="$(tryGetLock hshqopen acquireAllLocks)" if ! [ "$tgLock" = "true" ]; then checkRes="$(getLockOpenMsg hshqopen)" releaseLock networkchecks acquireAllLocks false echo "hshqopen: $checkRes" return 24 fi } function releaseAllLocks() { isForce="$1" releaseLock hshqopen "releaseAllLocks" "$isForce" releaseLock networkchecks "releaseAllLocks" "$isForce" } # Util Functions function isConfigVar() { isVar=$(grep ^$1= $CONFIG_FILE) if [ -z "$isVar" ]; then echo "false" else echo "true" fi } function getConfigVar() { echo $(getConfigVarFromFile $1 $CONFIG_FILE) } function getPlaintextRootConfigVar() { echo $(getConfigVarFromFile $1 $HSHQ_PLAINTEXT_ROOT_CONFIG root) } function getConfigVarFromFile() { if [ "$3" = "root" ]; then echo $(sudo grep ^$1= $2 | sed 's/^[^=]*=//' | sed 's/ *$//g' | sed 's/"//g') else echo $(grep ^$1= $2 | sed 's/^[^=]*=//' | sed 's/ *$//g' | sed 's/"//g') fi } function updateConfigVar() { updateConfigVarInFile "$1" "$2" $CONFIG_FILE } function updatePlaintextRootConfigVar() { updateConfigVarInFile "$1" "$2" $HSHQ_PLAINTEXT_ROOT_CONFIG root } function removePlaintextRootConfigVar() { if [ -z "$1" ]; then echo "FATAL: removePlaintextRootConfigVar - the variable name cannot be empty" exit 11 fi sudo sed -i "/$1/d" $HSHQ_PLAINTEXT_ROOT_CONFIG } function updatePlaintextUserConfigVar() { updateConfigVarInFile "$1" "$2" $HSHQ_PLAINTEXT_USER_CONFIG } function removePlaintextUserConfigVar() { if [ -z "$1" ]; then echo "FATAL: removePlaintextUserConfigVar - the variable name cannot be empty" exit 11 fi sed -i "/$1/d" $HSHQ_PLAINTEXT_USER_CONFIG } function updateConfigVarInFile() { ucv_curE=${-//[^e]/} if [ -z "$3" ]; then return fi if [ -z "$1" ]; then echo "FATAL: updateConfigVarInFile - the variable name cannot be empty" exit 11 fi if ! [ "$(echo $2 | head -n 1)" = "$2" ]; then echo "FATAL: updateConfigVarInFile - the value cannot have move than one line" exit 11 fi set +e is_ds=false if [ "$4" = "root" ]; then if sudo grep -q "[$]" <<< "$2"; then is_ds=true fi sudo grep -q "${1}=" "$3" else if grep -q "[$]" <<< "$2"; then is_ds=true fi grep -q "${1}=" "$3" fi if [ $? -ne 0 ]; then echo "Variable not found (${1}), exiting..." exit 3 fi local escaped_val="${2//|/\\|}" escaped_val="${escaped_val//&/\\&}" if [ "$is_ds" = "true" ]; then if [ "$4" = "root" ]; then sudo sed -i "s|^${1}=.*|${1}=\'${escaped_val}\'|g" "$3" else sed -i "s|^${1}=.*|${1}=\'${escaped_val}\'|g" "$3" fi else if [ "$4" = "root" ]; then sudo sed -i "s|^${1}=.*|${1}=\"${escaped_val}\"|g" "$3" else sed -i "s|^${1}=.*|${1}=\"${escaped_val}\"|g" "$3" fi fi if ! [ -z "$ucv_curE" ]; then set -e fi } function getCurrentDate() { date '+%Y-%m-%d %H:%M:%S' } function getDomainTLD() { domain_name=$1 echo $domain_name | rev | cut -d"." -f1 | rev } function getBaseDomain() { domain_name=$1 echo $domain_name | cut -d"." -f2- } function getSubDomain() { domain_name=$1 echo $domain_name | cut -d"." -f1 } function getAdminEmailName() { #echo "${HOMESERVER_ABBREV^^} Admin" echo "$HSHQ_ADMIN_NAME" } function removeRelayServerAgentFromWazuhManager() { removeAgentFromWazuhManager "RelayServer" } function removeHomeServerAgentFromWazuhManager() { removeAgentFromWazuhManager "HomeServer" } function removeAgentFromWazuhManager() { strMatch="$1" rrswa_curE=${-//[^e]/} set +e docker ps | grep "wazuh.manager" > /dev/null 2>&1 if [ $? -eq 0 ]; then rsID=$(docker exec -it wazuh.manager bash -c "/var/ossec/bin/manage_agents -l" | grep "$strMatch" | xargs | cut -d"," -f1 | cut -d" " -f2| xargs) if ! [ -z "$rsID" ]; then docker exec wazuh.manager bash -c "/var/ossec/bin/manage_agents -r $rsID" > /dev/null 2>&1 fi fi if ! [ -z "$rrswa_curE" ]; then set -e fi } function checkDeleteStackAndDirectory() { cdsad_curE=${-//[^e]/} stack_name="$1" formal_name="$2" is_check_stack="$3" is_force_delete="$4" is_delete_stack=false is_delete_dirs=false refreshSudo set +e if ! [ "$is_check_stack" = "false" ]; then stackID=$(getStackID $stack_name) if ! [ -z "$stackID" ]; then if ! [ "$ENABLE_STACK_DELETE" = "true" ] && ! [ "$is_force_delete" = "true" ]; then echo "ERROR: Stack deletion is disabled(1), exiting..." exit 5 fi if [ "$is_force_delete" = "true" ]; then is_delete_stack=true else showYesNoMessageBox "Stack Exists" "The stack '$stack_name' exists. Do you wish to delete it?" mbres=$? if [ $mbres -ne 0 ]; then return 1 else is_delete_stack=true fi fi fi fi if [ -d "$HSHQ_STACKS_DIR/$stack_name" ] || [ -d "$HSHQ_NONBACKUP_DIR/$stack_name" ]; then if ! [ "$ENABLE_STACK_DELETE" = "true" ] && ! [ "$is_force_delete" = "true" ]; then echo "ERROR: Stack deletion is disabled(2), exiting..." exit 5 fi if [ "$is_force_delete" = "true" ]; then is_delete_dirs=true else showYesNoMessageBox "$formal_name Directory Exists" "The $formal_name directory already exists. Do you wish to delete it?" mbres=$? if [ $mbres -eq 0 ]; then is_delete_dirs=true fi fi fi set -e if [ "$is_delete_stack" = "true" ]; then deleteStack $stack_name fi set +e if [ "$is_delete_dirs" = "true" ]; then sudo rm -fr $HSHQ_STACKS_DIR/$stack_name sudo rm -fr $HSHQ_NONBACKUP_DIR/$stack_name # Delete any volumes OLDIFS=$IFS IFS=$(echo -en "\n\b") vol_list=($(docker volume ls | grep v-${stack_name})) for curVol in "${vol_list[@]}" do curVolName=$(echo $curVol | xargs | cut -d" " -f2) docker volume rm $curVolName done IFS=$OLDIFS fi performPostStackRemoval $stack_name set +e if ! [ -z "$cdsad_curE" ]; then set -e fi } function checkAvailableHomeServerPort() { check_port=$1 if [ $check_port -le 1024 ]; then return 1 fi allPortsArr=($(echo "$(getHomeServerPortsList)" | tr "," "\n")) for curPort in "${allPortsArr[@]}" do echo "$curPort" | grep ":" > /dev/null 2>&1 if [ $? -eq 0 ]; then # Range of ports startPort=$(echo $curPort | cut -d":" -f1) endPort=$(echo $curPort | cut -d":" -f2) if [ $check_port -ge $startPort ] && [ $check_port -le $endPort ]; then return 2 fi else # Single port if [ $check_port -eq $curPort ]; then return 3 fi fi done } function perfRemoteAction() { set +e # m ra_mode="" # h ra_host="" # p ra_port=22 # u ra_username="" # i ra_injectPassword="" # s ra_sshPassword="" # c ra_command="" # a ra_attachfile="" # d ra_isEchoOutput=false # e ra_errorMsg="The remote operation failed, there might be a problem with your network." # o ra_options="" # r ra_numRetries=5 # b ra_secondsBetweenRetries=5 # f ra_isPromptRetryOnFailure=false local OPTIND opt a b c d e f h i m o p r s u while getopts ':a:b:c:de:fh:i:m:o:p:r:s:u:' opt; do case "$opt" in a) ra_attachfile="$OPTARG" ;; b) ra_secondsBetweenRetries="$OPTARG" ;; c) ra_command="$OPTARG" ;; d) ra_isEchoOutput=true ;; e) ra_errorMsg="$OPTARG" ;; f) ra_isPromptRetryOnFailure=true ;; h) ra_host="$OPTARG" ;; i) ra_injectPassword="$OPTARG" ;; m) ra_mode="$OPTARG" ;; o) ra_options="$OPTARG" ;; p) ra_port="$OPTARG" ;; r) ra_numRetries="$OPTARG" ;; s) ra_sshPassword="$OPTARG" ;; u) ra_username="$OPTARG" ;; ?|h) echo "Usage: perfRemoteAction [-a arg] [-b arg] [-c arg] [-d] [-e arg] [-f] [-h arg] [-i arg] [-m arg] [-o arg] [-p arg] [-r arg] [-s arg] [-u arg]" >&2 return ;; esac done shift "$(($OPTIND -1))" if [ -z "$ra_mode" ]; then echo "ERROR: Mode not specified, returning..." >&2 return 1 fi if [ -z "$ra_host" ]; then echo "ERROR: Host not specified($ra_host), returning..." >&2 return 1 fi if [ -z "$ra_port" ]; then echo "ERROR: Port not specified, returning..." >&2 return 1 fi if [ -z "$ra_username" ]; then echo "ERROR: Username not specified, returning..." >&2 return 1 fi if [ -z "$ra_command" ]; then echo "ERROR: Command not specified, returning..." >&2 return 1 fi if [ -z "$ra_numRetries" ]; then echo "ERROR: numRetries not specified, returning..." >&2 return 1 fi if [ -z "$ra_secondsBetweenRetries" ]; then echo "ERROR: secondsBetweenRetries not specified, returning..." >&2 return 1 fi if [ -z "$ra_isPromptRetryOnFailure" ]; then echo "ERROR: isPromptRetryOnFailure not specified, returning..." >&2 return 1 fi if [ "$ra_mode" = "scp" ] && [ -z "$ra_attachfile" ]; then echo "ERROR: No file specified for scp, returning..." >&2 return 1 fi if ! [ "$ra_mode" = "ssh" ] && ! [ "$ra_mode" = "scp" ]; then echo "ERROR: Unknown mode - only ssh and scp are supported, returning..." >&2 return 1 fi cmdString="" if ! [ -z "$ra_sshPassword" ]; then cmdString=${cmdString}"sshpass -p \"$ra_sshPassword\" " fi case "$ra_mode" in ssh) cmdString=${cmdString}"ssh -p $ra_port " ;; scp) cmdString=${cmdString}"scp -P $ra_port " ;; esac if ! [ -z "$ra_options" ]; then cmdString=${cmdString}"$ra_options " fi if [ "$ra_mode" = "scp" ]; then cmdString=${cmdString}" $ra_attachfile " fi cmdString=${cmdString}"$ra_username@$ra_host" case "$ra_mode" in ssh) cmdString=${cmdString}" ${ra_command@Q} " ;; scp) cmdString=${cmdString}"$ra_command " ;; esac if ! [ -z "$ra_injectPassword" ]; then cmdString=${cmdString}"<<< \"$ra_injectPassword\"" fi if [ "$ra_isEchoOutput" = "true" ]; then cmdString="cmdResult=\$(${cmdString})" fi if [ "$LOG_LEVEL" = "debug" ]; then echo "DEBUG: perfRemoteAction - $cmdString" >&2 fi if [ -z "$ra_sshPassword" ]; then loadSSHKey fi set +e ra_result=0 while true; do curRATries=0 while [ $curRATries -lt $ra_numRetries ] do eval "$cmdString" ra_result=$? if [ $ra_result -eq 0 ]; then if [ "$ra_isEchoOutput" = "true" ]; then echo "$cmdResult" fi unloadSSHKey set +e return 0 fi ((curRATries++)) echo "($curRATries of $ra_numRetries) Connection error, retrying in $ra_secondsBetweenRetries seconds..." >&2 sleep $ra_secondsBetweenRetries done if [ "$ra_isPromptRetryOnFailure" = "true" ]; then echo -e "\n\n========================================================================" >&2 echo "$ra_errorMsg" >&2 while true; do read -r -p "Enter retry or cancel: " raRetry if [ "$raRetry" = "retry" ]; then break elif [ "$raRetry" = "cancel" ]; then unloadSSHKey set +e return $ra_result else echo -e "\nUnknown option, please enter either retry or cancel.\n" >&2 fi done else unloadSSHKey set +e return 2 fi done unloadSSHKey set +e return $ra_result } function getPrivateIPRangesCaddy() { if ! [ "$(checkIsIPPrivate $1)" = "true" ]; then str_res="" if ! [ -z "$CONNECTING_IP" ]; then str_res="${CONNECTING_IP}/32" fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then rsip=$(getIPFromHostname ip.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN) if ! [ -z "$rsip" ] && ! [ "$rsip" = "$CONNECTING_IP" ]; then str_res=${str_res}" ${rsip}/32" fi fi echo "$str_res" fi } function getNonPrivateConnectingIP() { if ! [ "$(checkIsIPPrivate $HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" = "true" ]; then str_res="" if ! [ -z "$CONNECTING_IP" ]; then str_res="${CONNECTING_IP}/32" fi rsip=$(getIPFromHostname ip.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN) if ! [ -z "$rsip" ] && ! [ "$rsip" = "$CONNECTING_IP" ]; then str_res=${str_res}" ${rsip}/32" fi set +e echo "$str_res" | grep "$HOMESERVER_HOST_PRIMARY_INTERFACE_IP" > /dev/null 2>&1 if [ $? -ne 0 ] && ! [ -z "$HOMESERVER_HOST_PRIMARY_INTERFACE_IP" ]; then str_res=${str_res}" ${HOMESERVER_HOST_PRIMARY_INTERFACE_IP}/32" fi set -e echo "$str_res" fi } function getConnectingIPAddress() { echo $(echo $SSH_CLIENT | xargs | cut -d" " -f1) } function getDefaultIface() { echo $(ip route | grep -e "^default" | head -n 1 | awk -F'dev ' '{print $2}' | xargs | cut -d" " -f1) } function getIPAddressOfInterface() { interface_name=$1 ip_addr=$(ip route | grep $interface_name | grep / | awk -F'src ' '{print $2}' | xargs | cut -d" " -f1) if ! [ "$(checkValidIPAddress $ip_addr)" = "true" ]; then ip_addr="" fi if [[ $ip_addr =~ ^169\.254\. ]]; then ip_addr="$DEFAULT_UNFOUND_IP_ADDRESS" fi echo $ip_addr } function getCIDRLengthOfInterface() { interface_name=$1 ip route | grep "$interface_name" | grep / | xargs | cut -d" " -f1 | cut -d"/" -f2 } function getSubnetOfInterface() { net_interface=$1 net_ip=$2 is_priv=$3 gsoa_curE=${-//[^e]/} set +e if [ -z "$net_ip" ]; then net_ip=$(getIPAddressOfInterface $net_interface) fi if [ -z "$is_priv" ]; then is_priv=$(checkIsIPPrivate "$net_ip") fi this_subnet="" num_tries=1 max_tries=6 while [ -z "$this_subnet" ] && [ $num_tries -lt $max_tries ] do if [ "$is_priv" = "true" ]; then rpt="$(sipcalc $(ip -o -f inet addr show $net_interface | awk '/scope global/ {print $4}'))" netaddr=$(echo "$rpt" | grep -o "Network address.*" | cut -d"-" -f2 | xargs | cut -d" " -f1) netsize=$(echo "$rpt" | grep -o "Network mask (bits).*" | cut -d"-" -f2 | xargs | cut -d" " -f1) if ! [ -z "$netaddr" ] && ! [ -z "$netsize" ]; then this_subnet="${netaddr}/${netsize}" fi else this_subnet="${net_ip}/32" fi if ! [ -z "$this_subnet" ]; then break fi sleep 3 ((num_tries++)) done if [[ $net_ip =~ ^169\.254\. ]]; then this_subnet="$DEFAULT_UNFOUND_IP_SUBNET" fi if ! [ -z "$gsoa_curE" ]; then set -e fi echo "$this_subnet" } function getGatewayOfInterface() { interface_name=$1 ip route | grep "$interface_name" | grep -e "^default" | awk '{print $3}' | head -1 } function getIPFromHostname() { echo $(timeout 5 dig $1 A +short | grep '^[.0-9]*$' | head -n 1) } function getAllowedPublicIPs() { set +e ips="0.0.0.0/5,8.0.0.0/7,11.0.0.0/8,12.0.0.0/6,16.0.0.0/4,32.0.0.0/3,64.0.0.0/2,128.0.0.0/3,160.0.0.0/5,168.0.0.0/6,172.0.0.0/12,172.32.0.0/11,172.64.0.0/10,172.128.0.0/9,173.0.0.0/8,174.0.0.0/7,176.0.0.0/4,192.0.0.0/9,192.128.0.0/11,192.160.0.0/13,192.169.0.0/16,192.170.0.0/15,192.172.0.0/14,192.176.0.0/12,192.192.0.0/10,193.0.0.0/8,194.0.0.0/7,196.0.0.0/6,200.0.0.0/5,208.0.0.0/4" if ! [ -z "$1" ]; then ips+=","$1 fi set -e echo $ips } function checkIsIPPrivate() { check_ip="$1" if [ -z "$check_ip" ]; then echo "false" return fi set +e priv_arr=(10.0.0.0/8 172.16.0.0/12 192.168.0.0/16) for subnet in "${priv_arr[@]}" do is_in_subnet=$(isIPInSubnet $check_ip $subnet) if [ "$is_in_subnet" = "true" ]; then echo "true" return fi done echo "false" } function outputInterfaceInfo() { interfaceName="$1" strOut="\n----------------------------------------------------------------" strOut="${strOut}\n Interface Name: $interfaceName" strOut="${strOut}\n IP Address: $(getIPAddressOfInterface $interfaceName)" strOut="${strOut}\n Subnet: $(getSubnetOfInterface $interfaceName)" interfaceType=Other connectedTo="" ipAssignment="" sudo netplan get network | yq -r '.ethernets | keys' 2> /dev/null | grep $interfaceName > /dev/null 2>&1 if [ $? -eq 0 ]; then interfaceType=Wired if [ "$(sudo netplan get network.ethernets.${interfaceName}.dhcp4)" = "true" ]; then ipAssignment="dhcp" else ipAssignment="static" fi fi sudo netplan get network | yq -r '.wifis | keys' 2> /dev/null | grep $interfaceName > /dev/null 2>&1 if [ $? -eq 0 ]; then interfaceType=Wireless connectedTo=$(sudo iwconfig $interfaceName | grep -o "ESSID.*" | cut -d":" -f2) if [ "$(sudo netplan get network.wifis.${interfaceName}.dhcp4)" = "true" ]; then ipAssignment="dhcp" else ipAssignment="static" fi fi strOut="${strOut}\n Interface Type: $interfaceType" if ! [ -z "$ipAssignment" ]; then strOut="${strOut}\n IP Assignment Method: $ipAssignment" fi if ! [ -z "$connectedTo" ]; then strOut="${strOut}\n Wifi Network: $connectedTo" fi isPrimary=$(sqlite3 $HSHQ_DB "select ConnectionType from connections where InterfaceName='$interfaceName';") strOut="${strOut}\n IsPrimary: $isPrimary" isExposeToNetwork=$(sqlite3 $HSHQ_DB "select IsExposeToNetwork from connections where InterfaceName='$interfaceName';") if [ "$isExposeToNetwork" = "1" ]; then isExposeToNetwork=true else isExposeToNetwork=false fi strOut="${strOut}\n IsExposeToNetwork: $isExposeToNetwork" inputAllowPorts=$(sqlite3 $HSHQ_DB "select InputAllowPorts from connections where InterfaceName='$interfaceName';") strOut="${strOut}\n InputAllowPorts: $inputAllowPorts" dockerUserAllowPorts=$(sqlite3 $HSHQ_DB "select DockerUserAllowPorts from connections where InterfaceName='$interfaceName';") strOut="${strOut}\n DockerUserAllowPorts: $dockerUserAllowPorts" strOut="${strOut}\n----------------------------------------------------------------\n" echo "$strOut" } function setAsWiredDHCP() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi if ! sudo test -f /etc/netplan/${NETPLAN_ORIGIN_HINT}.yaml; then echo -e "\nYour netplan configuration is not managed by HSHQ. Please rename your\nconfiguration file to /etc/netplan/88-hshq.yaml in order to use this function.\n" return fi interfaceName="$1" isReplace="$2" set +e sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" > /dev/null 2>&1 if [ "$isReplace" = "true" ]; then sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}=null" > /dev/null 2>&1 else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.addresses=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.nameservers=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.routes=null" > /dev/null 2>&1 fi sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.dhcp4=true" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.optional=true" sudo netplan apply sleep $NETPLAN_APPLY_WAIT checkUpdateSingleHostInterface "$interfaceName" } function setAsWiredStaticIP() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi if ! sudo test -f /etc/netplan/${NETPLAN_ORIGIN_HINT}.yaml; then echo -e "\nYour netplan configuration is not managed by HSHQ. Please rename your\nconfiguration file to /etc/netplan/88-hshq.yaml in order to use this function.\n" return fi interfaceName="$1" isReplace="$2" ipAddress="$3" cidrLength="$4" interfaceGateway="$5" set +e if [ -z "$ipAddress" ]; then echo "ERROR: The IP address cannot be blank. No actions were performed." return 1 elif [ -z "$cidrLength" ]; then echo "ERROR: The CIDR length cannot be blank. No actions were performed." return 2 elif [ -z "$interfaceGateway" ]; then echo "ERROR: The network gateway cannot be blank. No actions were performed." return 3 fi sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" > /dev/null 2>&1 if [ "$isReplace" = "true" ]; then sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}=null" > /dev/null 2>&1 else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.addresses=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.nameservers=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.routes=null" > /dev/null 2>&1 fi sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.addresses=[${ipAddress}/${cidrLength}]" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.routes=[{to: default, via: $interfaceGateway}]" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.dhcp4=false" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}.optional=true" sudo netplan apply sleep $NETPLAN_APPLY_WAIT checkUpdateSingleHostInterface "$interfaceName" } function setAsWirelessDHCP() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi if ! sudo test -f /etc/netplan/${NETPLAN_ORIGIN_HINT}.yaml; then echo -e "\nYour netplan configuration is not managed by HSHQ. Please rename your\nconfiguration file to /etc/netplan/88-hshq.yaml in order to use this function.\n" return fi interfaceName="$1" isReplace="$2" wifiSSID="$3" wifiPass="$4" set +e sudo netplan get | yq '.network.wifis | keys' 2> /dev/null | grep $interfaceName > /dev/null 2>&1 if [ $? -ne 0 ] || [ "$isReplace" = "true" ]; then if [ -z "$wifiSSID" ]; then echo "ERROR: This is a new interface or a forced replacement, thus the wifi SSID cannot be blank. No actions were performed." return 1 elif ! [ -z "$wifiPass" ] && ([ ${#wifiPass} -lt 8 ] || [ ${#wifiPass} -gt 63 ]); then echo "ERROR: The wifi password must be between 8 and 63 characters. No actions were performed." return 2 fi sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" > /dev/null 2>&1 if [ -z "$wifiPass" ]; then sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}={dhcp4: true, optional: true, access-points: {$wifiSSID: {}}}" else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}={dhcp4: true, optional: true, access-points: {$wifiSSID: {auth: {key-management: psk, password: $wifiPass}}}}" fi else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.addresses=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.nameservers=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.routes=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.dhcp4=true" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.optional=true" fi sudo netplan apply sleep $NETPLAN_APPLY_WAIT checkUpdateSingleHostInterface "$interfaceName" } function setAsWirelessStaticIP() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi if ! sudo test -f /etc/netplan/${NETPLAN_ORIGIN_HINT}.yaml; then echo -e "\nYour netplan configuration is not managed by HSHQ. Please rename your\nconfiguration file to /etc/netplan/88-hshq.yaml in order to use this function.\n" return fi interfaceName="$1" isReplace="$2" ipAddress="$3" cidrLength="$4" interfaceGateway="$5" wifiSSID="$6" wifiPass="$7" set +e if [ -z "$ipAddress" ]; then echo "ERROR: The IP address cannot be blank. No actions were performed." return 1 elif [ -z "$cidrLength" ]; then echo "ERROR: The CIDR length cannot be blank. No actions were performed." return 2 elif [ -z "$interfaceGateway" ]; then echo "ERROR: The network gateway cannot be blank. No actions were performed." return 3 fi sudo netplan get | yq '.network.wifis | keys' 2> /dev/null | grep $interfaceName > /dev/null 2>&1 if [ $? -ne 0 ] || [ "$isReplace" = "true" ]; then if [ -z "$wifiSSID" ]; then echo "ERROR: This is a new interface or a forced replacement, thus the wifi SSID cannot be blank. No actions were performed." return 4 elif ! [ -z "$wifiPass" ] && ([ ${#wifiPass} -lt 8 ] || [ ${#wifiPass} -gt 63 ]); then echo "ERROR: The wifi password must be between 8 and 63 characters. No actions were performed." return 6 fi sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" > /dev/null 2>&1 if [ -z "$wifiPass" ]; then sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}={dhcp4: false, optional: true, addresses: [${ipAddress}/${cidrLength}], routes: [{to: default, via: $interfaceGateway}], access-points: {$wifiSSID: {}}}" else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}={dhcp4: false, optional: true, addresses: [${ipAddress}/${cidrLength}], routes: [{to: default, via: $interfaceGateway}], access-points: {$wifiSSID: {auth: {key-management: psk, password: $wifiPass}}}}" fi else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.addresses=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.nameservers=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.routes=null" > /dev/null 2>&1 sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.addresses=[${ipAddress}/${cidrLength}]" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.routes=[{to: default, via: $interfaceGateway}]" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.dhcp4=false" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.optional=false" fi sudo netplan apply sleep $NETPLAN_APPLY_WAIT checkUpdateSingleHostInterface "$interfaceName" } function removeFromNetplan() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi if ! sudo test -f /etc/netplan/${NETPLAN_ORIGIN_HINT}.yaml; then echo -e "\nYour netplan configuration is not managed by HSHQ. Please rename your\nconfiguration file to /etc/netplan/88-hshq.yaml in order to use this function.\n" return fi interfaceName="$1" set +e sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.ethernets.${interfaceName}=null" sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" sudo netplan apply sleep $NETPLAN_APPLY_WAIT checkUpdateSingleHostInterface "$interfaceName" } function displayWifiNetworks() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi interfaceName="$1" OLDIFS=$IFS IFS=$(echo -en "\n\b") resArr=($(sudo iwlist $interfaceName scanning | grep -E --color 'Frequency|Quality|Encryption|ESSID')) if [ $? -ne 0 ]; then return fi numItems=$((${#resArr[@]} - 1)) unset fmtArr fmtArr="" sudo rm -f /tmp/wifi.txt curIndex=0 fmtArr=() for curID in $(seq 0 $numItems); do if ! [[ $(echo "${resArr[$curID]}" | sed 's/^[ \t]*//') =~ ^Frequency ]] || ! [[ $(echo "${resArr[$(($curID + 1))]}" | sed 's/^[ \t]*//') =~ ^Quality ]] || ! [[ $(echo "${resArr[$(($curID + 2))]}" | sed 's/^[ \t]*//') =~ ^Encryption ]] || ! [[ $(echo "${resArr[$(($curID + 3))]}" | sed 's/^[ \t]*//') =~ ^ESSID ]]; then continue fi curFrequency="${resArr[$curID]}" curQuality="${resArr[$(($curID + 1))]}" curEncryption="${resArr[$(($curID + 2))]}" curESSID="${resArr[$(($curID + 3))]}" echo "$curESSID" | grep "x00" > /dev/null 2>&1 if [ $? -eq 0 ] || [ -z "$curESSID" ]; then continue fi curFrequency="$(echo $curFrequency | cut -d":" -f2- | cut -d"(" -f1 | xargs -0)" curQualityNum="$(echo $curQuality | cut -d"=" -f2- | cut -d"/" -f1 | xargs -0)" curSignal="$(echo $curQuality | rev | cut -d"=" -f1 | rev | xargs -0)" curSignalNum="$(echo $curSignal | cut -d" " -f1 | xargs -0)" curEncryption="$(echo $curEncryption | cut -d":" -f2 | xargs -0)" if [ "$curEncryption" = "off" ]; then curEncryption="open" elif [ "$curEncryption" = "on" ]; then curEncryption="" else curEncryption="unknown" fi curESSID="$(echo $curESSID | cut -d":" -f2- | tr -d '"' | xargs -0)" fmtArr+=( "$curESSID|$curQualityNum|$curSignal|$curFrequency|$curEncryption" ) echo "$curQualityNum $curSignalNum $curIndex" >> /tmp/wifi.txt ((curIndex+=1)) done sort -k1 -k2 -rn /tmp/wifi-sorted.txt readarray -t sortedArr < /tmp/wifi-sorted.txt echo "----------------------------------------------------------------" echo " SSID Quality Signal Frequency Open " echo "----------------------------------------------------------------" for curLine in "${sortedArr[@]}" do curIndex=$(echo "$curLine" | rev | cut -d" " -f1 | rev) curESSID=$(echo "${fmtArr[$curIndex]}" | cut -d"|" -f1) curQualityNum=$(echo "${fmtArr[$curIndex]}" | cut -d"|" -f2)/70 curSignal=$(echo "${fmtArr[$curIndex]}" | cut -d"|" -f3) curFrequency=$(echo "${fmtArr[$curIndex]}" | cut -d"|" -f4) curEncryption=$(echo "${fmtArr[$curIndex]}" | cut -d"|" -f5) if [ -z "$curESSID" ]; then continue fi printf "%-20s %-8s %-10s %-12s %-9s\n" "${curESSID:0:20}" "${curQualityNum:0:6}" "${curSignal:0:10}" "${curFrequency:0:10}" "${curEncryption:0:8}" done echo "----------------------------------------------------------------" IFS=$OLDIFS rm -f /tmp/wifi.txt rm -f /tmp/wifi-sorted.txt } function addUpdateWifiAccessPoint() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi if ! sudo test -f /etc/netplan/${NETPLAN_ORIGIN_HINT}.yaml; then echo -e "\nYour netplan configuration is not managed by HSHQ. Please rename your\nconfiguration file to /etc/netplan/88-hshq.yaml in order to use this function.\n" return fi interfaceName="$1" wifiSSID="$2" wifiPass="$3" set +e if [ -z "$wifiSSID" ]; then echo "ERROR: The wifi SSID cannot be blank. No actions were performed." return 1 elif ! [ -z "$wifipass" ] && ([ ${#wifiPass} -lt 8 ] || [ ${#wifiPass} -gt 63 ]); then echo "ERROR: The wifi password must be between 8 and 63 characters. No actions were performed." return 2 fi sudo netplan get | yq '.network.wifis | keys' | grep $interfaceName > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ERROR: This wireless interface was not found within netplan. First try adding the interface." return 3 fi sudo netplan get "network.wifis.${interfaceName}.access-points" | yq 'keys' | grep "$wifiSSID" > /dev/null 2>&1 if [ $? -eq 0 ]; then if [ $(sudo netplan get "network.wifis.${interfaceName}.access-points" | yq 'keys' | wc -l) -eq 1 ]; then # This is the only access point, and we only need to update the password # Since netplan is so poorly designed, we (stupidly) have to remove and re-add the entire interface # Unfortunately, this will cause any other edits in netplan by the user will be lost. # This is so clumsy and dumb. if [ "$(sudo netplan get network.wifis.wlp2s0.dhcp4)" = "true" ]; then sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" setAsWirelessDHCP "$interfaceName" "$wifiSSID" "$wifiPass" else ipaddr=$(sudo netplan get "network.wifis.${interfaceName}" | yq .addresses[0]) if [ $? -ne 0 ] || [ -z "$ipaddr" ]; then echo "ERROR: There was a problem updating the password. Please remove this interface and re-add it with the updated password." return 4 fi iponly=$(echo $ipaddr | cut -d"/" -f1) if [ $? -ne 0 ] || [ -z "$iponly" ]; then echo "ERROR: There was a problem updating the password. Please remove this interface and re-add it with the updated password." return 5 fi cidr=$(echo $ipaddr | cut -d"/" -f2) if [ $? -ne 0 ] || [ -z "$cidr" ]; then echo "ERROR: There was a problem updating the password. Please remove this interface and re-add it with the updated password." return 6 fi gateway=$(sudo netplan get "network.wifis.${interfaceName}" | yq .routes[0].via) if [ $? -ne 0 ] || [ -z "$gateway" ]; then echo "ERROR: There was a problem updating the password. Please remove this interface and re-add it with the updated password." return 7 fi sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" setAsWirelessStaticIP "$interfaceName" "$iponly" "$cidr" "$gateway" "$wifiSSID" "$wifiPass" fi return else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.access-points.${wifiSSID}=null" fi fi if [ -z "$wifipass" ]; then sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.access-points.${wifiSSID}={}" else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.access-points.${wifiSSID}={auth: {key-management: psk, password: $wifiPass}}" fi sudo netplan apply sleep $NETPLAN_APPLY_WAIT checkUpdateSingleHostInterface "$interfaceName" } function removeWifiAccessPoint() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi if ! sudo test -f /etc/netplan/${NETPLAN_ORIGIN_HINT}.yaml; then echo -e "\nYour netplan configuration is not managed by HSHQ. Please rename your\nconfiguration file to /etc/netplan/88-hshq.yaml in order to use this function.\n" return fi interfaceName="$1" wifiSSID="$2" set +e if [ -z "$wifiSSID" ]; then echo "ERROR: The wifi SSID cannot be blank. No actions were performed." return 1 fi sudo netplan get | yq '.network.wifis | keys' | grep $interfaceName > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ERROR: This wireless interface was not found within netplan. First try adding the interface." return 2 fi sudo netplan get | interfaceName=$interfaceName yq '.network.wifis.[strenv(interfaceName)].access-points | keys' | grep "$wifiSSID" > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ERROR: The wireless SSID ($wifiSSID) is not a listed access point. No actions were performed." return 3 fi numAP=$(sudo netplan get | interfaceName=$interfaceName yq '.network.wifis.[strenv(interfaceName)].access-points | keys' | wc -l) if [ $numAP -le 1 ]; then # This the last access point, must remove the interface sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}=null" else sudo netplan set --origin-hint $NETPLAN_ORIGIN_HINT "network.wifis.${interfaceName}.access-points.${wifiSSID}=null" fi sudo netplan apply sleep $NETPLAN_APPLY_WAIT checkUpdateSingleHostInterface "$interfaceName" } function getAllNetworkInterfacesCSV() { set +e allInterfaces=($(ls /sys/class/net | grep -v -E "veth.*|docker.*|lo|vpn-*|ext-*")) timeout 10 docker network ls > /dev/null 2>&1 if [ $? -eq 0 ]; then dockerNets=($(docker network ls -q)) networksArr=() for curNet in "${dockerNets[@]}" do brName=$(docker network inspect $curNet | jq -r '.[] | .Options."com.docker.network.bridge.name"') if [ -z "$brName" ] || [ "$brName" = "null" ]; then brName="br-${curNet}" fi networksArr+=($brName) done fi allInts="" for curInt in "${allInterfaces[@]}" do isFound=false for curNet in "${networksArr[@]}" do if [ "$curNet" = "$curInt" ]; then isFound=true break fi done if [ "$isFound" = "false" ]; then if [ -z "$allInts" ]; then allInts="$curInt" else allInts="${allInts},$curInt" fi fi done echo "$allInts" } function outputAllNetworkInterfaces() { allInterfacesArr=($(echo "$(getAllNetworkInterfacesCSV)" | tr "," "\n")) for curInt in "${allInterfacesArr[@]}" do ifconfig $curInt done } function checkIsPrimaryInterfacePrivate() { checkIsIPPrivate "$(getPlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" } function setHostInternetTrafficWGInterface() { if [ "$(checkIsPrimaryInterfacePrivate)" = "false" ]; then echo "Your HomeServer primary interface is a public static IP address. This option is not available." return fi set +e selWG="$1" curInt=$(getPlaintextRootConfigVar HOST_INTERNET_TRAFFIC_INTERFACE) if [ "$curInt" = "$selWG" ]; then strMsg="This is already the current interface, no changes were made." echo "$strMsg" return fi if ! [ "$selWG" = "default" ]; then if ! sudo test -f $HSHQ_WIREGUARD_DIR/internet/${selWG}.conf; then strMsg="Could not find WireGuard interface ($selWG)" echo "$strMsg" logHSHQEvent error "setHostInternetTrafficWGInterface - $strMsg" return 1 fi rTable=$(getConfigVarFromFile \#ROUTING_TABLE_ID $HSHQ_WIREGUARD_DIR/internet/${selWG}.conf root) if [ -z "$rTable" ]; then strMsg="Could not determine routing table ($selWG)" echo "$strMsg" logHSHQEvent error "setHostInternetTrafficWGInterface - $strMsg" return 2 fi fi sudo ip rule delete priority 8000 > /dev/null 2>&1 while [ $? -eq 0 ] do sudo ip rule delete priority 8000 > /dev/null 2>&1 done if ! [ "$selWG" = "default" ]; then sudo ip rule add not fwmark $rTable table $rTable priority 8000 fi HOST_INTERNET_TRAFFIC_INTERFACE="$selWG" updatePlaintextRootConfigVar HOST_INTERNET_TRAFFIC_INTERFACE $HOST_INTERNET_TRAFFIC_INTERFACE } function initHSHQDB() { sudo rm -f $HSHQ_DB sqlite3 $HSHQ_DB "create table connections(ID integer not null primary key autoincrement,Name text,EmailAddress text,ConnectionType text,NetworkType text,PublicKey text,PresharedKey text,IPAddress text,IsInternet boolean,InterfaceName text,EndpointHostname text,EndpointIP text default null,LastUpdated datetime,Network_Subnet text default null,IsExposeToNetwork boolean,InputAllowPorts text default null,DockerUserAllowPorts text default null);" sqlite3 $HSHQ_DB "create table mailhosts(ID integer not null primary key autoincrement,MailHost text not null);" sqlite3 $HSHQ_DB "create table hsvpn_connections(ID integer not null primary key references connections(ID) on delete cascade,HomeServerName text,IsPrimary boolean,DomainName text default null,ExternalPrefix text default null,InternalPrefix text default null,MailHostID integer references mailhosts(ID) on delete cascade,CA_Abbrev text default null,CA_IP text default null,CA_Subdomain text default null,CA_URL text default null,RS_VPN_IP text default null,VPNRoutingTable integer);" sqlite3 $HSHQ_DB "create table hsvpn_dns(ID integer not null primary key autoincrement,HostDomain text not null,PeerDomain text not null,PeerDomainExtPrefix text not null,IPAddress text not null,DateAdded datetime,IsActive boolean);" sqlite3 $HSHQ_DB "create unique index hpdns on hsvpn_dns(HostDomain,PeerDomain);" sqlite3 $HSHQ_DB "create table mailhostmap(MailHostID integer not null references mailhosts(ID) on delete cascade,Domain text not null,IsFirstDomain boolean,primary key (MailHostID,Domain));" sqlite3 $HSHQ_DB "create table lecertdomains(Domain text primary key,BaseDomain text not null);" sqlite3 $HSHQ_DB "create table exposedomains(Domain text primary key,BaseDomain text not null);" sqlite3 $HSHQ_DB "create table customfwsubnet(ID integer not null primary key autoincrement,Name text not null, Subnet text not null, InputAllowPorts text default null, DockerUserAllowPorts text default null);" addFirewallSubnet "dock-ext-net" "${NET_EXTERNAL_SUBNET_PREFIX}.0" "21" > /dev/null 2>&1 sudo chmod 640 $HSHQ_DB sudo chown root $HSHQ_DB } function getWGNameFromIP() { check_ip=$1 echo $(sqlite3 $HSHQ_DB "select Name from connections where IPAddress='$check_ip';") } function getWGNameFromPubkey() { check_key=$1 echo $(sqlite3 $HSHQ_DB "select Name from connections where PublicKey='$check_key';") } function checkInterfaceNameExists() { check_iface="$1" ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where InterfaceName='$check_iface';") if [ -z "$ifaceName" ]; then echo "false" else echo "true" fi } function checkValidWireGuardKey() { # Thank you gjoranv: https://stackoverflow.com/questions/74438436/how-to-validate-a-wireguard-public-key check_key=$1 keyregex="^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$" if [[ $check_key =~ $keyregex ]]; then echo "true" else echo "false" fi } function isIPInSubnet() { iiis_curE=${-//[^e]/} check_ipaddr=$1 check_subnet=$2 if [ -z "$check_ipaddr" ] || [ -z "$check_subnet" ]; then echo "true" return fi set +e grepcidr ${check_subnet} <(echo ${check_ipaddr}) > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "true" else echo "false" fi set +e if ! [ -z "$iiis_curE" ]; then set -e fi } function getRandomVPNIP() { # This function is specific to a /24 sized subnet. # It should be improved to a more general case based on the size of the network. vpnbase=$(echo $PRIMARY_VPN_SUBNET | rev | cut -d "." -f2- | rev) numvpnipTries=1 maxvpnipTries=88 while [ $numvpnipTries -lt $maxvpnipTries ] do randIP=${vpnbase}.$(($(($RANDOM%250))+1)) if [ -z "$(getWGNameFromIP $randIP)" ]; then echo $randIP return fi ((numvpnipTries++)) done # Just start at the beginning... ip_host_part=1 while [ $ip_host_part -le 250 ] do randIP=${vpnbase}.$ip_host_part if [ -z "$(getWGNameFromIP $randIP)" ]; then echo $randIP return fi ((ip_host_part++)) done # The network is full. Caller needs to handle this. echo "" } function getRandomWireGuardIP() { numRIPTries=1 maxRIPTries=1000 while [ $numRIPTries -lt $maxRIPTries ] do randIP=10.$(( $RANDOM % 256 )).$(( $RANDOM % 256 )).$(($(($RANDOM%250))+1)) if [ "$(isIPInSubnet $randIP $PRIMARY_VPN_SUBNET)" = "false" ] && [ -z "$(getWGNameFromIP $randIP)" ]; then echo $randIP return fi ((numRIPTries++)) done echo "" } function checkNetworkIntersect() { net1=$1 net2=$2 if [ -z "$net1" ] || [ -z "$net2" ]; then echo "true" return fi net1_beginip=$(sipcalc $net1 | grep "^Network range" | rev | cut -d " " -f3 | rev) net1_endip=$(sipcalc $net1 | grep "^Network range" | rev | cut -d " " -f1 | rev) is_in_subnet=$(isIPInSubnet $net1_beginip $net2) if [ "$is_in_subnet" = "true" ]; then echo "true" return fi is_in_subnet=$(isIPInSubnet $net1_endip $net2) if [ "$is_in_subnet" = "true" ]; then echo "true" return fi net2_beginip=$(sipcalc $net2 | grep "^Network range" | rev | cut -d " " -f3 | rev) net2_endip=$(sipcalc $net2 | grep "^Network range" | rev | cut -d " " -f1 | rev) is_in_subnet=$(isIPInSubnet $net2_beginip $net1) if [ "$is_in_subnet" = "true" ]; then echo "true" return fi is_in_subnet=$(isIPInSubnet $net2_endip $net1) if [ "$is_in_subnet" = "true" ]; then echo "true" return fi echo "false" } function isNetworkIntersectOurNetworks() { checkNetwork=$1 checkAll=$2 checkHostInterfaceSubnets=$3 checknet_ip=$(echo $checkNetwork | cut -d"/" -f1) if [ "$(checkValidIPAddress $checknet_ip)" = "false" ]; then echo "ERROR: Invalid IP address/subnet." return fi OLDIFS=$IFS IFS=$(echo -en "\n\b") vpn_arr=($(sqlite3 $HSHQ_DB "select connections.Network_Subnet,hsvpn_connections.HomeServerName from connections join hsvpn_connections on connections.ID = hsvpn_connections.ID where connections.Network_Subnet is not null;")) for curvpncheck in "${vpn_arr[@]}" do curvpn=$(echo "$curvpncheck" | cut -d "|" -f1) curhsname=$(echo "$curvpncheck" | cut -d "|" -f2) if [ "$(checkNetworkIntersect $curvpn $checkNetwork)" = "true" ]; then echo "The requested subnet ($checkNetwork) collides with ${curhsname} VPN subnet ($curvpn)" IFS=$OLDIFS return fi done if [ "$checkHostInterfaceSubnets" = "true" ] || [ "$checkAll" = "true" ]; then hsnet_arr=($(sqlite3 $HSHQ_DB "select Network_Subnet,Name from connections where NetworkType='home_network';")) for curhscheck in "${hsnet_arr[@]}" do curhsnet=$(echo "$curhscheck" | cut -d "|" -f1) curhsname=$(echo "$curhscheck" | cut -d "|" -f2) if [ -z "$curhsnet" ]; then continue fi if [ "$(checkNetworkIntersect $curhsnet $checkNetwork)" = "true" ]; then echo "The requested subnet ($checkNetwork) collides with a HomeServer host interface subnet ($curhsname - $curhsnet)" IFS=$OLDIFS return fi done fi if [ "$checkAll" = "true" ]; then # Check all possible connections ip_arr=($(sqlite3 $HSHQ_DB "select IPAddress,Name from connections;")) else # Check only connections on this HomeServer. ip_arr=($(sqlite3 $HSHQ_DB "select IPAddress,Name from connections where ConnectionType='homeserver_internet' and NetworkType in ('primary','other');")) fi for curipcheck in "${ip_arr[@]}" do curip=$(echo "$curipcheck" | cut -d "|" -f1) curconname=$(echo "$curipcheck" | cut -d "|" -f2) if [ "$(checkNetworkIntersect ${curip}/32 $checkNetwork)" = "true" ]; then echo "The requested subnet ($checkNetwork) collides with connection ${curconname} ($curip)" IFS=$OLDIFS return fi done IFS=$OLDIFS echo "" } function getNextWGRoutingTableID() { rtid=100 flist=$(sudo ls $HSHQ_WIREGUARD_DIR/internet) if [ -z "$flist" ]; then echo $rtid exit 0 fi arr=() for conf in $HSHQ_WIREGUARD_DIR/internet/*.conf do if ! test -f "$conf"; then continue; fi arr+=($(sudo grep ^\#ROUTING_TABLE_ID= $conf | sed 's/^[^=]*=//')) done while true; do echo ${arr[@]} | grep $rtid > /dev/null if [[ $? -ne 0 ]]; then break else rtid=$(($rtid+1)) fi done echo $rtid } function getNextAvailableWGDockerNetwork() { docker_network_name=$1 conn_mtu=$2 docker network create --driver=bridge $docker_network_name > /dev/null 2>&1 docker_subnet=$(getDockerSubnet $docker_network_name) docker network rm $docker_network_name > /dev/null 2>&1 docker network create $docker_network_name --driver=bridge --subnet $docker_subnet -o com.docker.network.driver.mtu=$conn_mtu -o com.docker.network.bridge.name=$docker_network_name > /dev/null 2>&1 echo $docker_subnet } function getWGPortalAuth() { echo "$(echo $(echo -n ${RELAYSERVER_WGPORTAL_ADMIN_EMAIL}:${RELAYSERVER_WGPORTAL_ADMIN_PASSWORD} | base64) | sed 's| ||g')" } function getCACertificateNameFromDomain() { dom_name="$1" echo ${dom_name}-ca.crt } function outputDeviceIPMapping() { map_ouptut_file="$1" sqlite3 -noheader "$HSHQ_DB" " SELECT '{\"targets\":[\"' || IPAddress || '\"],\"labels\":{\"device_name\":\"' || REPLACE(COALESCE(Name,''),'\"','') || '\"}}' FROM connections WHERE IPAddress IS NOT NULL AND IPAddress <> ''; " | { echo '[' first=1 while IFS= read -r line; do [ -n "$line" ] || continue if [ "$first" -eq 1 ]; then printf '%s\n' "$line"; first=0; else printf ',\n%s\n' "$line"; fi done echo ']' } | jq . > "${map_ouptut_file}.tmp" sudo mv "${map_ouptut_file}.tmp" "${map_ouptut_file}" } function sortCSVList() { inCSVList="$1" if [ -z "$inCSVList" ]; then echo "" return fi inCSVArr=($(echo $inCSVList | tr "," "\n")) OLDIFS=$IFS IFS=$'\n' sortedCSVArr=($(sort <<< "${inCSVArr[*]}")) IFS=$OLDIFS sortedCSVList="" for curItem in "${sortedCSVArr[@]}" do if [ -z "$curItem" ]; then continue; fi sortedCSVList="${sortedCSVList}${curItem}," done if ! [ -z "$sortedCSVList" ]; then sortedCSVList=${sortedCSVList%?} fi echo "$sortedCSVList" } function getPortainerToken() { local OPTIND opt u p while getopts ':u:p:' opt; do case "$opt" in u) port_username="$OPTARG" ;; p) port_password="$OPTARG" ;; ?|h) return 1;; esac done shift "$(($OPTIND -1))" set +e # Portainer sometimes exhibits issues with API, so we'll add some error handling and restart capabilities. cur_timeout=5 ptok_retVal=1 ptok_numTries=1 while [ $cur_timeout -le 30 ] do ptok_full=$(http --check-status --ignore-stdin --timeout=$cur_timeout --verify=no https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/auth username=$port_username password=$port_password) ptok_retVal=$? if [ $ptok_retVal -eq 0 ] && ! [ -z "$ptok_full" ]; then # Do a sample query ptok=$(echo $ptok_full | jq -r .jwt) qry=$(http --check-status --ignore-stdin --verify=no --timeout=$cur_timeout --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $ptok") if [ $? -eq 0 ]; then break fi ptok_retVal=1 fi echo "Could not obtain Portainer token, restarting stack ($ptok_numTries of 6)..." 1>&2 restartPortainer 1>&2 cur_timeout=$((cur_timeout+5)) ((ptok_numTries++)) done # Probably going to need to implement full error handling for this type of issue. if [ $ptok_retVal -ne 0 ]; then return 1 fi echo $ptok } function getStackID() { stackID="NA" stackName="$1" if [ -z "$stackName" ]; then echo "ERROR (getStackID): The stack name is empty..." 1>&2 return fi stackName="${stackName//.}" if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi gsid_numTries=1 gsid_totalTries=5 gsid_retVal=1 qry="" while [ $gsid_numTries -le $gsid_totalTries ] do qry=$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN") gsid_retVal=$? if [ $gsid_retVal -eq 0 ]; then break fi ((gsid_numTries++)) done if [ $gsid_retVal -ne 0 ]; then echo "ERROR: Could not obtain stack ID from Portainer..." 1>&2 return fi for row in $(echo "${qry}" | jq -r '.[] | @base64'); do _jq() { echo ${row} | base64 --decode | jq -r ${1} } if [ $(_jq '.Name') = $stackName ]; then stackID=$(_jq '.Id') break fi done if ! [ $stackID = "NA" ]; then echo $stackID fi } function updateStackByID() { set +e update_stack_name=$1 update_stack_id=$2 update_compose_file=$3 update_env_file=$4 if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi echo "$(createStackJson $update_stack_name $update_compose_file "$update_env_file")" > $HOME/${update_stack_name}-json.tmp usid_numTries=1 usid_totalTries=5 usid_retVal=1 while [ $usid_numTries -le $usid_totalTries ] do http --check-status --ignore-stdin --verify=no --timeout=300 PUT https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/$update_stack_id endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/${update_stack_name}-json.tmp > /dev/null 2>&1 usid_retVal=$? if [ $usid_retVal -eq 0 ]; then break fi ((usid_numTries++)) done if [ $usid_retVal -ne 0 ]; then echo "ERROR: Could not update stack in Portainer..." 1>&2 return 1 fi rm -f $update_compose_file $update_env_file $HOME/${update_stack_name}-json.tmp } function restartAllStacksDialog() { set +e showYesNoMessageBox "Confirm Restart Stacks" "This action will shut down all running stacks, restart the docker daemon, then restart the stacks that were stopped. Depending on the number of stacks, it could take up to 10-15 minutes to complete. Continue?" if [ $? -ne 0 ]; then return 0 fi sudo -k refreshSudo restartAllStacks "" false doNothing true doNothing } function restartAllStacks() { ras_curE=${-//[^e]/} set +e skipRestart="$1" isOnlyHSHQManaged="$2" rasModFunction="$3" isGetNetworksLock="$4" modAdguard="$5" startStopStack uptimekuma stop rsi_numTries=1 rsi_totalTries=5 rsi_retVal=1 rstackIDsQry="" if [ "$isGetNetworksLock" = "true" ]; then tgLock="$(tryGetLock networkchecks restartAllStacks)" if ! [ "$tgLock" = "true" ]; then checkRes="$(getLockOpenMsg networkchecks)" strErr="restartAllStacks - Cannot obtain networkchecks lock: $checkRes. Please try again shortly, returning..." logHSHQEvent warning "$strErr" return 1 fi fi if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi if [ $? -ne 0 ]; then echo "ERROR: Could not get Portainer auth token..." 1>&2 releaseLock networkchecks restartAllStacks false return 2 fi while [ $rsi_numTries -le $rsi_totalTries ] do rstackIDsQry=$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN") rsi_retVal=$? if [ $rsi_retVal -eq 0 ]; then break fi ((rsi_numTries++)) done if [ $rsi_retVal -ne 0 ]; then echo "ERROR: Could not get list of stack IDs in Portainer..." 1>&2 releaseLock networkchecks restartAllStacks false return 3 fi # Need to use a temp resolver since Adguard goes down for a bit... sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 docker container prune -f removeDockerNetworks $rasModFunction echo "Restarting Docker..." sudo systemctl restart docker createDockerNetworks startPortainer if [ $? -ne 0 ]; then echo "There was an error restarting portainer. You will need to determine the problem and restart all of the stacks manually." sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 || echo "$skipRestart" | grep ${rstackNames[$curID]}, > /dev/null 2>&1 retVal_1=$? retVal_2=1 if [ "$isOnlyHSHQManaged" = "true" ] && ! [ "$(isStackHSHQManaged ${rstackIDs[$curID]})" = "true" ]; then retVal_2=0 fi if [ $retVal_1 -eq 0 ] || [ $retVal_2 -eq 0 ]; then echo "Skipping ${rstackNames[$curID]} (${rstackIDs[$curID]})..." continue fi fi echo "Starting ${rstackNames[$curID]} (${rstackIDs[$curID]})..." if [ "${rstackNames[$curID]}" = "adguard" ] && ! [ "$modAdguard" = "doNothing" ]; then updateStackEnv adguard mfAdguardV230Fix else startStopStackByID ${rstackIDs[$curID]} start fi sleep 3 if [ "${rstackNames[$curID]}" = "adguard" ]; then waitForContainerLogString adguard 3 60 "entering listener loop proto=tls" if [ $? -eq 0 ]; then sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null <&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound="T" break fi echo "Container not ready, sleeping $sleep_interval second(s), total wait=$i seconds..." sleep $sleep_interval ((i=$i+$sleep_interval)) done sleep 5 } function modFunUpdateMailuRelaySettings() { sed -i "s|RELAYHOST=.*|RELAYHOST=$SMTP_RELAY_HOST|" $HOME/${updateStackName}.env sed -i "s|RELAYUSER=.*|RELAYUSER=$SMTP_RELAY_USERNAME|" $HOME/${updateStackName}.env sed -i "s|RELAYPASSWORD=.*|RELAYPASSWORD=$SMTP_RELAY_PASSWORD|" $HOME/${updateStackName}.env } function getPasswordWithSymbol() { # Generates a password with "_" inserted somewhere randomly # This appeases any symbol requirements that certain password policies enforce. pw_length=$1 if [ -z "$pw_length" ] || [ $pw_length -lt 2 ]; then pw_length=32 fi pw_length=$((pw_length-1)) rand_pw=$(pwgen -c -n $pw_length 1) rand_pos=$(( ( RANDOM % $pw_length ) + 1 )) echo ${rand_pw:0:rand_pos}"@"${rand_pw:rand_pos} } function base64URLEncode() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '=' } function generateFullJWTToken() { header="$1" payload="$2" jwt_secret="$3" header_base64=$(printf %s "$header" | base64URLEncode) payload_base64=$(printf %s "$payload" | base64URLEncode) signed_content="${header_base64}.${payload_base64}" signature=$(printf %s "$signed_content" | openssl dgst -binary -sha256 -hmac "$jwt_secret" | base64URLEncode) printf '%s' "${signed_content}.${signature}" } function performExitFunctions() { is_show_msgbox="$1" releaseLock hshqopen "performExitFunctions" false # This is more aggressive, but cleans up any pre-existing sessions (in case the script terminated abnormally) set +e killall ssh-agent > /dev/null 2>&1 set -e if ([ -z "$IS_CONFIG_INIT" ] || ! [ "$IS_INSTALLED" = "true" ]) && ! [ "$IS_CONFIG_FILE_UNENCRYPTED" = "true" ]; then return fi encryptConfigFile if [ "$is_show_msgbox" = "true" ]; then showMessageBox "Config File Encrypted" "The configuration file has been encrypted." else echo "==========================================" echo "The configuration file has been encrypted." echo "==========================================" fi } function encryptConfigFile() { if ! [ -f $HSHQ_CONFIG_DIR/$CONFIG_FILE_DEFAULT_FILENAME ] && ! [ -f $HSHQ_CONFIG_DIR/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME]; then echo "ERROR: Plain text config file does not exist" elif ! [ -z "$CONFIG_ENCRYPTION_PASSPHRASE" ]; then rm -f $HSHQ_CONFIG_DIR/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME openssl enc -e -aes256 -pbkdf2 -pass pass:$CONFIG_ENCRYPTION_PASSPHRASE -in $HSHQ_CONFIG_DIR/$CONFIG_FILE_DEFAULT_FILENAME -out $HSHQ_CONFIG_DIR/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME if [ -f $HSHQ_CONFIG_DIR/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then chmod 0400 $HSHQ_CONFIG_DIR/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME rm -f $HSHQ_CONFIG_DIR/$CONFIG_FILE_DEFAULT_FILENAME IS_CONFIG_FILE_UNENCRYPTED=false fi fi } function decryptFile() { encFile=$1 resFile=$2 rm -f $resFile touch $resFile # Set the file permissions BEFORE decrypting... chmod 600 $resFile if [ -z "$USER_CONFIG_PW" ]; then openssl enc -d -aes256 -pbkdf2 -in $encFile >> $resFile 2>/dev/null else openssl enc -d -aes256 -pbkdf2 -in $encFile -pass file:<( echo -n "$USER_CONFIG_PW" ) >> $resFile 2>/dev/null fi retVal=$? if [ $retVal -ne 0 ]; then rm -f $resFile fi return $retVal } function decryptConfigFile() { encConfig=$1 if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME IS_CONFIG_FILE_UNENCRYPTED=true set -e return fi set +e decrypt_res=1 while [ $decrypt_res -ne 0 ] do decryptFile $encConfig $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME decrypt_res=$? if [ $decrypt_res -ne 0 ]; then echo "Decryption error, bad password" rm -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME showYesNoMessageBox "Password Error" "You did not enter the correct password, try again?" if [ $? -ne 0 ]; then releaseLock hshqopen "decryptConfigFile" false exit 1 fi fi done mv -f $encConfig $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_BACKUP_FILENAME CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME IS_CONFIG_FILE_UNENCRYPTED=true set -e } function decryptConfigFileAndLoadEnvNoPrompts() { if [ -z "$USER_CONFIG_PW" ]; then USER_CONFIG_PW="$1" fi tgLock="$(tryGetLock hshqopen User-ScriptServer)" if ! [ "$tgLock" = "true" ]; then checkRes="$(getLockOpenMsg hshqopen)" USER_CONFIG_PW="" echo "ERROR: The HSHQ script is already open or running in a different instance ($checkRes). This situation could occur if another scheduled process is currently running or the script exited prematurely with an error. If you are sure that this has occurred erroneously, then run the 04 System Utils -> 06 Reset Mutex Lock function to reset it." exit 9 fi set -e decryptAndLoad true } function decryptAndLoad() { ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME if ! [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then decryptFile "$ENC_CONFIG_FILE" "$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME" mv -f $ENC_CONFIG_FILE $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_BACKUP_FILENAME fi USER_CONFIG_PW="" CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME if ! [ -f $CONFIG_FILE ]; then echo "There was a critical error with the configuration file, exiting..." exit 9 fi IS_CONFIG_FILE_UNENCRYPTED=true loadConfigVars "$1" } function checkDecryptConfigFile() { if [ -z "$USER_CONFIG_PW" ]; then USER_CONFIG_PW="$1" fi set +e if ! [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then DEF_ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME else echo "ERROR: Could not find encrypted configuration file." return 2 fi if ! [ -z "$DEF_ENC_CONFIG_FILE" ]; then openssl enc -d -aes256 -pbkdf2 -in "$DEF_ENC_CONFIG_FILE" -pass file:<( echo -n "$USER_CONFIG_PW" ) > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "bad" echo "ERROR: Incorrect password for encrypted configuration file." return 3 else echo "good" fi else echo "ERROR: Unknown error decrypting configuration file." exit 4 fi fi } function promptTestDecryptConfigFile() { if ! [ -z "$USER_CONFIG_PW" ]; then checkDecryptConfigFile > /dev/null 2>&1 if [ $? -ne 0 ]; then USER_CONFIG_PW="" fi fi while [ -z "$USER_CONFIG_PW" ] do USER_CONFIG_PW=$(promptPasswordMenu "Enter Decrypt Password" "Enter your config decrypt password: ") retVal=$? if [ $retVal -ne 0 ]; then return 3 fi checkDecryptConfigFile > /dev/null 2>&1 if [ $? -ne 0 ]; then showMessageBox "Incorrect Password" "The password is incorrect, please re-enter it." USER_CONFIG_PW="" fi done decryptAndLoad false USER_CONFIG_PW="" } function checkFileShellExpansion() { cfse_curE=${-//[^e]/} check_file=$1 set +e retVal="" grep '\$' $check_file > /dev/null 2>&1 retDS=$? grep '\~' $check_file > /dev/null 2>&1 retTD=$? grep '\*' $check_file > /dev/null 2>&1 retAS=$? if [ $retDS -eq 0 ] || [ $retTD -eq 0 ] || [ $retAS -eq 0 ]; then retVal=$retVal"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n" retVal=$retVal"@ Shell expansion attempt detected! @\n" retVal=$retVal"@ Check the sender of this configuration. @\n" retVal=$retVal"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n" else echo "" fi if ! [ -z "$cfse_curE" ]; then set -e fi echo "$retVal" } function createStackJson() { echo "{\"Name\":\"$1\",""$( jq -Rscjr '{StackFileContent: . }' $2 | tail -c +2 | head -c -1 )"",\"Env\":$(envToJson $3)}" } function installStack() { set +e stack_name=$1 container_name=$2 stack_search_string=$3 envfile=$4 sleep_interval=$5 max_interval=$6 if [ -z "$sleep_interval" ]; then sleep_interval=1 fi if [ -z "$max_interval" ]; then max_interval=300 fi logHSHQEvent info "installStack - Installing Stack ($stack_name)" # Refresh the sudo timestamp refreshSudo if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi echo "Creating stack: $stack_name" set -f echo "$(createStackJson $stack_name $HOME/$stack_name-compose.yml "$envfile")" > $HOME/$stack_name-json.tmp set +f sleep 1 ins_numTries=1 ins_totalTries=5 ins_retVal=1 while [ $ins_numTries -le $ins_totalTries ] do if [ "$IS_STACK_DEBUG" = "true" ] || [ $ins_numTries -eq $ins_totalTries ]; then http --check-status --ignore-stdin --verify=no --timeout=300 https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/create/standalone/string endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/$stack_name-json.tmp else http --check-status --ignore-stdin --verify=no --timeout=300 https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/create/standalone/string endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/$stack_name-json.tmp >/dev/null fi ins_retVal=$? if [ $ins_retVal -eq 0 ]; then break fi ((ins_numTries++)) done if [ $ins_retVal -ne 0 ]; then logHSHQEvent error "installStack - Error installing stack ($stack_name)" echo "ERROR: Could not install stack ($stack_name) in Portainer..." 1>&2 return $ins_retVal fi sleep 1 search=$stack_search_string isFound=false i=0 while [ $i -le $max_interval ] do findtext=$(docker logs $container_name 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping $sleep_interval second(s), total wait=$i seconds..." sleep $sleep_interval ((i=$i+$sleep_interval)) done if ! [ "$isFound" = "true" ]; then echo "$stack_name did not start up correctly..." return 1 fi set -e sleep 1 rm -f $HOME/$stack_name-json.tmp rm -f $HOME/$stack_name-compose.yml rm -f $envfile } function startStopStack() { stackname=$1 startStop=$2 stackID=$(getStackID $stackname) if [ -z "$stackID" ]; then return fi startStopStackByID $stackID $startStop } function startStopStackByID() { stackID=$1 startStop=$2 set +e if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi sss_numTries=1 sss_totalTries=5 sss_retVal=1 while [ $sss_numTries -le $sss_totalTries ] do if [ "$IS_STACK_DEBUG" = "true" ] || [ $sss_numTries -eq $sss_totalTries ]; then http --check-status --ignore-stdin --verify=no --timeout=300 POST https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/$stackID/$startStop endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" else http --check-status --ignore-stdin --verify=no --timeout=300 POST https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/$stackID/$startStop endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" > /dev/null fi sss_retVal=$? if [ $sss_retVal -eq 0 ]; then break fi sleep 1 sudo docker container prune -f sleep 1 ((sss_numTries++)) done if [ $sss_retVal -ne 0 ]; then echo "ERROR: Could not $startStop stack (ID=$stackID) in Portainer..." 1>&2 return $sss_retVal fi } function restartStackIfRunning() { stackName=$1 waitTime=$2 if [ "$stackName" = "portainer" ]; then # Special case for portainer restartPortainer return fi stackID=$(getStackID $stackName) if [ -z "$stackID" ]; then return fi stackStatus=$(getStackStatusByID $stackID) rssif_rtVal=0 if [ "$stackStatus" = "1" ]; then startStopStackByID $stackID stop sleep $waitTime startStopStackByID $stackID start rssif_rtVal=$? fi return $rssif_rtVal } function forceRestartStack() { stackName=$1 waitTime=$2 if [ "$stackName" = "portainer" ]; then # Special case for portainer restartPortainer return fi set +e stackID=$(getStackID $stackName) if [ -z "$stackID" ]; then return fi set +e startStopStackByID $stackID stop sleep $waitTime startStopStackByID $stackID start } function getStackStatusByID() { stackID=$1 if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi gss_numTries=1 gss_totalTries=5 gss_retVal=1 stackStatus="" while [ $gss_numTries -le $gss_totalTries ] do stackStatus=$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/$stackID endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN") gss_retVal=$? if [ $gss_retVal -eq 0 ]; then break fi ((gss_numTries++)) done if [ $gss_retVal -ne 0 ]; then echo "ERROR: Could not get stack status from Portainer..." 1>&2 return fi echo $stackStatus | jq -r '.Status' } function getStackStatusByName() { stackName=$1 stackID=$(getStackID $stackName) if [ -z "$stackID" ]; then return fi stackStatus=$(getStackStatusByID $stackID) echo $stackStatus } function deleteStack() { stackname=$1 if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi stackID=$(getStackID $stackname) if [ -z "$stackID" ]; then return fi ds_numTries=1 ds_totalTries=5 ds_retVal=1 while [ $ds_numTries -le $ds_totalTries ] do http --check-status --ignore-stdin --verify=no --timeout=300 DELETE https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/$stackID endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" > /dev/null ds_retVal=$? if [ $ds_retVal -eq 0 ]; then break fi ((ds_numTries++)) done if [ $ds_retVal -ne 0 ]; then echo "ERROR: Could not delete stack from Portainer..." 1>&2 return fi } function waitForStack() { searchText="$1" containerName="$2" max_interval=$3 sleep_interval=$4 wfs_curE=${-//[^e]/} set +e if [ -z "$max_interval" ]; then max_interval=300 fi if [ -z "$sleep_interval" ]; then sleep_interval=5 fi isStackReady="false" i=0 while [ $i -le $max_interval ] do findtext=$(docker logs $containerName 2>&1 | grep "$searchText") if ! [ -z "$findtext" ]; then isStackReady="true" break fi echo "Container not ready, sleeping $sleep_interval second(s), total wait=$i seconds..." sleep $sleep_interval ((i=$i+$sleep_interval)) done sleep 5 set +e if ! [ -z "$wfs_curE" ]; then set -e fi } function waitForContainerLogString() { container_name="$1" sleep_interval="$2" max_checks="$3" search_string="$4" isFound=false curCheckIter=0 totalWait=0 set +e echo "Checking container log ($container_name)..." while [ $curCheckIter -le $max_checks ] do findtext=$(docker logs $container_name 2>&1 | grep "$search_string") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping $sleep_interval seconds, total wait=$totalWait seconds..." sleep $sleep_interval totalWait=$(($totalWait+$sleep_interval)) ((curCheckIter++)) done if ! [ "$isFound" = "true" ]; then echo "String not found within alotted time for container $container_name: $search_string" return 1 fi } function showMessageBox() { msgmenu=$(cat << EOF $(getLogo) $2 EOF ) whiptail --title "$1" --msgbox "$msgmenu" $MENU_HEIGHT $MENU_WIDTH } function showYesNoMessageBox() { msgmenu=$(cat << EOF $(getLogo) $2 EOF ) whiptail --title "$1" --yesno "$msgmenu" $MENU_HEIGHT $MENU_WIDTH } function showNoYesMessageBox() { msgmenu=$(cat << EOF $(getLogo) $2 EOF ) whiptail --title "$1" --yesno "$msgmenu" $MENU_HEIGHT $MENU_WIDTH --yes-button "No" --no-button "Yes" } function promptUserInputMenu() { usermenu=$(cat << EOF $(getLogo) $3 EOF ) menures=$(whiptail --title "$2" --inputbox "$usermenu" $MENU_HEIGHT $MENU_WIDTH "$1" 3>&1 1>&2 2>&3) if [ $? -ne 0 ]; then exit 1 fi echo "$menures" } function promptPasswordMenu() { usermenu=$(cat << EOF $(getLogo) $2 EOF ) menures=$(whiptail --title "$1" --passwordbox "$usermenu" $MENU_HEIGHT $MENU_WIDTH 3>&1 1>&2 2>&3) retVal=$? echo "$menures" return $retVal } function getDockerSubnet() { subnet=$(docker network inspect $1 | grep \"Subnet\": | awk '{print $2}' | sed 's/[",]//g') echo $subnet } function removeSpecialChars() { # This was an interesting one...https://askubuntu.com/questions/357248/how-to-remove-special-m-bm-character-with-sed sed -i 's/\xc2\xa0/ /g' "$1" sed -i 's/\r$//g' "$1" } function extractValueFromApplication() { app_var_starts_with=$1 app_file_name=$2 grep "^${app_var_starts_with}:" $app_file_name | cut -d ":" -f2- | sed 's/^ *//' | sed 's/ *$//' | sed 's/ */ /' } function replaceSingleLineInFile() { rslif_curE=${-//[^e]/} block_match_begin="$1" replace_text="$2" r_filename=$3 set +e match=$(grep "$block_match_begin" $r_filename) if ! [ -z "$match" ]; then all_text=$(cat $r_filename) echo -e "${all_text%%$block_match_begin*}${replace_text}${all_text##*$block_match_begin}" > $r_filename fi if ! [ -z "$rslif_curE" ]; then set -e fi } function replaceTextBlockInFile() { block_match_begin="$1" block_match_end="$2" replace_text="$3" r_filename=$4 is_keep_block_headers=$5 space_delim=$6 set +e match=$(grep "$block_match_begin" $r_filename) if ! [ -z "$match" ]; then all_text=$(cat $r_filename) if ! [ -z "$space_delim" ]; then replace_text=$(echo "$replace_text" | sed "s|$space_delim| |g") fi if [ "$is_keep_block_headers" = "true" ]; then echo -e "${all_text%%$block_match_begin*}${block_match_begin}\n${replace_text}\n${block_match_end}${all_text##*$block_match_end}" > $r_filename else echo -e "${all_text%%$block_match_begin*}\n${replace_text}\n${all_text##*$block_match_end}" > $r_filename fi fi set -e } function replaceOrAppendTextBlockInFile() { block_match_begin="$1" block_match_end="$2" replace_text="$3" r_filename=$4 space_delim=$5 set +e match=$(grep "$block_match_begin" $r_filename) if [ -z "$match" ]; then #Append if ! [ -z "$space_delim" ]; then replace_text=$(echo "$replace_text" | sed "s|$space_delim| |g") fi echo -e "\n${block_match_begin}\n${replace_text}\n${block_match_end}" >> $r_filename else #Replace replaceTextBlockInFile "$block_match_begin" "$block_match_end" "$replace_text" $r_filename true "$space_delim" fi set -e } function removeTextBlockInFile() { rtbif_curE=${-//[^e]/} block_match_begin="$1" block_match_end="$2" r_filename=$3 set +e match=$(grep "$block_match_begin" $r_filename) if ! [ -z "$match" ]; then sed -i "/$block_match_begin/,/$block_match_end/d" $r_filename cat -s $r_filename > $HOME/tmpremfile mv $HOME/tmpremfile $r_filename fi set +e if ! [ -z "$rtbif_curE" ]; then set -e fi } function insertSubAuthelia() { insSub="$1" selBlock="$2" set +e all_text=$(cat $HSHQ_STACKS_DIR/authelia/config/configuration.yml) echo $all_text | grep -oP '(?<=#AC_BEGIN).*(?=#AC_END)' | grep "$insSub" > /dev/null 2>&1 if [ $? -eq 0 ]; then return fi grep "# Authelia $selBlock END" $HSHQ_STACKS_DIR/authelia/config/configuration.yml > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi sed -i "/# Authelia $selBlock END/i\ - $insSub" $HSHQ_STACKS_DIR/authelia/config/configuration.yml echo "Restarting authelia..." docker container restart authelia docker ps | grep codeserver > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart codeserver > /dev/null 2>&1 fi } function removeSubAuthelia() { set +e remSub="$1" ac_block=$(sed -n "/#AC_BEGIN/,/#AC_END/p" $HSHQ_STACKS_DIR/authelia/config/configuration.yml | sed "/$remSub/d") replaceTextBlockInFile "#AC_BEGIN" "#AC_END" "$ac_block" $HSHQ_STACKS_DIR/authelia/config/configuration.yml true echo "Restarting authelia..." docker container restart authelia docker ps | grep codeserver > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart codeserver > /dev/null 2>&1 fi } function insertOIDCClientAuthelia() { oidcName="$1" oidcBlock="$2" set +e grep "# Authelia OIDC Client $oidcName" $HSHQ_STACKS_DIR/authelia/config/configuration.yml > /dev/null 2>&1 if [ $? -eq 0 ]; then replaceTextBlockInFile "# Authelia OIDC Client $oidcName BEGIN" "# Authelia OIDC Client $oidcName END" "$oidcBlock" $HSHQ_STACKS_DIR/authelia/config/configuration.yml false else oidcBlock="${oidcBlock}\n# Authelia OIDC Clients END" replaceTextBlockInFile "# Authelia OIDC Clients END" "# Authelia OIDC Clients END" "$oidcBlock" $HSHQ_STACKS_DIR/authelia/config/configuration.yml false fi sed -i '/^$/{:a;N;s/\n$//;ta}' $HSHQ_STACKS_DIR/authelia/config/configuration.yml set +e docker container restart authelia > /dev/null 2>&1 docker ps | grep codeserver > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart codeserver > /dev/null 2>&1 fi } function urlEncode() { strLength="${#1}" for (( i = 0; i < strLength; i++ )); do c="${1:i:1}" case $c in [a-zA-Z0-9.~_-]) printf "$c" ;; *) printf '%%%02X' "'$c" ;; esac done } function urlDecode() { url_encoded="${1//+/ }" printf '%b' "${url_encoded//%/\\x}" } function getTextBetweenStrings() { # This function could probably be done better filename=$1 stringbegin=$2 stringend=$3 stringlinebegin=$(grep "$stringbegin" $filename) stringlineend=$(grep "$stringend" $filename) allfiledata=$(cat $filename) textafter=$(echo "${allfiledata##*$stringlinebegin}") result=$(echo "${textafter%%$stringlineend*}") echo -e "$result" } function checkValidString() { check_string=$1 addchars=$2 if [[ "$check_string" =~ ^[0-9a-z$addchars]+$ ]]; then echo "true" else echo "false" fi } function checkValidDirectory() { check_string=$1 if [ $(checkValidString "$check_string" "/") = "false" ]; then echo "false" fi if [[ ${check_string:0:1} != "/" ]]; then echo "false" fi echo "true" } function checkValidBaseDomain() { check_domain=$1 if [ $(checkValidString "$check_domain" ".-") = "false" ]; then echo "false" return fi if [ -z "$(getDomainTLD $check_domain)" ]; then echo "false" return fi if [ -z "$(echo $check_domain | cut -d"." -f1)" ]; then echo "false" return fi echo "true" } function checkValidEmail() { check_string=$1 if [ -z "$check_string" ]; then echo "false"; fi regex="^([A-Za-z]+[A-Za-z0-9]*((\.|\-|\_)?[A-Za-z]+[A-Za-z0-9]*){1,})@(([A-Za-z]+[A-Za-z0-9]*)+((\.|\-|\_)?([A-Za-z]+[A-Za-z0-9]*)+){1,})+\.([A-Za-z]{2,})+" if [[ "$check_string" =~ $regex ]]; then echo "true" else echo "false" fi } function checkValidStringUpperLowerNumbers() { check_string=$1 addchars=$2 if [[ $check_string =~ ^[0-9a-zA-Z$addchars]+$ ]]; then echo "true" else echo "false" fi } function checkValidStringUpper() { check_string=$1 addchars=$2 if [[ $check_string =~ ^[A-Z$addchars]+$ ]]; then echo "true" else echo "false" fi } function checkValidNumber() { check_string=$1 if [[ $check_string =~ ^[0-9]+$ ]]; then echo "true" else echo "false" fi } function checkValidVersionNumber() { check_number="$1" if [ "$(checkValidNumber "$check_number")" = "false" ]; then echo "false" elif [ $check_number -lt 1 ] || [ $check_number -gt 10000 ]; then echo "false" else echo "true" fi } function checkValidIPAddress() { set +e ip=$(echo "$1" | cut -d "/" -f1) cidr="" echo "$1" | grep "/" > /dev/null 2>&1 if [ $? -eq 0 ]; then cidr=$(echo "$1" | cut -d "/" -f2-) fi stat=1 if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then OLDIFS=$IFS IFS='./' ip=($ip) IFS=$OLDIFS [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] stat=$? fi if ! [ -z "$cidr" ]; then if [ "$(checkValidNumber $cidr)" = "false" ] || [ $cidr -lt 0 ] || [ $cidr -gt 32 ]; then stat=1 fi fi set -e if [ $stat -eq 0 ]; then echo "true" else echo "false" fi } function checkValidPassword() { pw_in="$1" min_length=$2 pw_length=${#pw_in} if [ -z "$min_length" ]; then min_length=1 fi if ! [[ "$pw_in" =~ [[:upper:]] ]]; then # Does not contain upper case. echo "false" elif ! [[ "$pw_in" =~ [[:lower:]] ]]; then # Does not contain lower case. echo "false" elif ! [[ "$pw_in" =~ [0-9] ]]; then # Does not contain number. echo "false" elif [[ "$pw_in" =~ [[:space:]] ]]; then # Contains a space. echo "false" elif [[ "$pw_in" =~ '$' ]]; then # Contains a dollar sign. echo "false" elif [[ "$pw_in" =~ '"' ]]; then # Contains a double quote. echo "false" elif [[ "$pw_in" =~ '`' ]]; then # Contains a backtick. echo "false" elif [ $pw_length -lt $min_length ]; then # Less than min characters echo "false" elif echo "$pw_in" | grep -Eq '^[a-zA-Z0-9(~!@#%^&*_+=<>?.,)-]+$'; then echo "true" else # Some other problem echo "false" fi } function checkValidUsername() { chkName="$1" if echo "$chkName" | grep -Eq '^[a-z0-9]+$' && echo "$chkName" | cut -c1 | grep -Eq '[a-z]' && [ ${#chkName} -le 32 ] && [ ${#chkName} -ge 4 ]; then echo "true" else echo "false" fi } function checkValidPortsList() { check_string="$1" if ! [ $(checkValidString "$check_string" ",:/") = "true" ]; then echo "The list of ports is invalid. It must consist of 0-9, a-z, commas(,), colons(:), and/or fwd slash (/) with no spaces." return 1 else portListArr=($(echo "$check_string" | tr "," "\n")) for curPort in "${portListArr[@]}" do port_no=$(echo "$curPort" | cut -d"/" -f1 | xargs) port_prot="" if [[ $curPort =~ "/" ]]; then port_prot=$(echo "$curPort" | cut -d"/" -f2- | xargs) fi if ! [[ $port_no =~ ^[0-9:]+$ ]]; then echo "One of the port numbers in the list is invalid ($port_no). It must consist of 0-9, and/or colons(:) with no spaces. Full string: $check_string" return 2 fi if [[ $port_no =~ ":" ]]; then port_begin=$(echo "$port_no" | cut -d":" -f1 | xargs) port_end=$(echo "$port_no" | cut -d":" -f2- | xargs) if ! [[ $port_begin =~ ^[0-9]+$ ]] || ! [[ $port_end =~ ^[0-9]+$ ]]; then echo "One of the port numbers in the list is invalid [$port_begin:$port_end]. It must consist of 0-9, and/or colons(:) with no spaces." return 3 fi fi if ! [ -z "$port_prot" ]; then case "$port_prot" in tcp|udp|both) continue ;; *) echo "One of the port protocols in the list is invalid. It must be either tcp, udp, or both: $port_prot" return 4 ;; esac fi done fi } function checkValidINPUTChainPortsList() { check_string="$1" portListArr=($(echo "$check_string" | tr "," "\n")) for curPort in "${portListArr[@]}" do port_no=$(echo "$curPort" | cut -d"/" -f1 | xargs) if [[ $curPort =~ "/" ]]; then port_prot=$(echo "$curPort" | cut -d"/" -f2- | xargs) else echo "One of the ports in the list did not specify a protocol, which is required for the INPUT chain. It must be either tcp, udp, or both: $curPort" return 1 fi if ! [[ $port_no =~ ^[0-9:]+$ ]]; then echo "One of the port numbers in the list is invalid ($port_no). It must consist of 0-9, and/or colons(:) with no spaces. Full string: $check_string" return 2 fi if [[ $port_no =~ ":" ]]; then port_begin=$(echo "$port_no" | cut -d":" -f1 | xargs) port_end=$(echo "$port_no" | cut -d":" -f2- | xargs) if ! [[ $port_begin =~ ^[0-9]+$ ]] || ! [[ $port_end =~ ^[0-9]+$ ]]; then echo "One of the port numbers in the list is invalid [$port_begin:$port_end]. It must consist of 0-9, and/or colons(:) with no spaces." return 3 fi fi case "$port_prot" in tcp|udp|both) continue ;; *) echo "One of the port protocols in the list is invalid. It must be either tcp, udp, or both: $port_prot" return 4 ;; esac done } function checkValidCurrencyCode() { chkCode="$1" if [ "$(jq -e --arg c "$chkCode" '."4217" | any(.[]; .alpha_3 == $c)' /usr/share/iso-codes/json/iso_4217.json)" = "true" ]; then echo "true" else echo "false" fi } function getStringLength() { echo "${#1}" } function isEmailMatchBaseDomain() { check_email_addr="$1" check_base_domain="$2" email_addr_domain=$(echo "$check_email_addr" | cut -d"@" -f2-) if [ "$email_addr_domain" = "$check_base_domain" ]; then echo "true" else echo "false" fi } function addHomeNetIP() { add_ip=$1 is_update_iptables=$2 set +e echo $HOMENET_ADDITIONAL_IPS | grep $add_ip >/dev/null if [ $? -ne 0 ]; then if [ -z "$HOMENET_ADDITIONAL_IPS" ]; then HOMENET_ADDITIONAL_IPS=$add_ip else HOMENET_ADDITIONAL_IPS=$HOMENET_ADDITIONAL_IPS","$add_ip fi updatePlaintextRootConfigVar HOMENET_ADDITIONAL_IPS "$HOMENET_ADDITIONAL_IPS" if [ "$IS_INSTALLED" = "true" ]; then checkUpdateAllIPTables addHomeNetIP fi fi } function removeHomeNetIP() { remove_ip=$1 is_update_iptables=$2 set +e echo $HOMENET_ADDITIONAL_IPS | grep $remove_ip >/dev/null if [ $? -eq 0 ]; then HOMENET_ADDITIONAL_IPS=$(echo $HOMENET_ADDITIONAL_IPS | sed "s|${remove_ip},||g") HOMENET_ADDITIONAL_IPS=$(echo $HOMENET_ADDITIONAL_IPS | sed "s|,${remove_ip}||g") updatePlaintextRootConfigVar HOMENET_ADDITIONAL_IPS "$HOMENET_ADDITIONAL_IPS" if [ "$IS_INSTALLED" = "true" ]; then checkUpdateAllIPTables removeHomeNetIP fi fi } function isServiceDisabled() { check_svc_disabled="$1" isd_curE=${-//[^e]/} set +e if grep -q "$check_svc_disabled" <<< "$HSHQ_REQUIRED_STACKS"; then isd_RetVal="false" elif [ "$DISABLED_SERVICES" = "minimal" ]; then isd_RetVal="true" elif [ "$DISABLED_SERVICES" = "none" ]; then isd_RetVal="false" else isd_RetVal="$(isItemInCSVList $check_svc_disabled $DISABLED_SERVICES)" fi if ! [ -z "$isd_curE" ]; then set -e fi echo "$isd_RetVal" } function isItemInCSVList() { check_item="$1" check_list="$2" checkListArr=($(echo $check_list | tr "," "\n")) for curItem in "${checkListArr[@]}" do if [ "$check_item" = "$curItem" ]; then echo "true" return fi done echo "false" } function checkDisableStack() { if [ "$(isServiceDisabled $1)" = "true" ]; then echo "Disabling $1" startStopStack $1 stop fi } function getSvcCredentialsVW() { svc_name=$1 login_uri=$2 abbrev=$3 username=$4 password=$5 echo "$abbrev,,login,$svc_name,,,0,$login_uri,$username,$password," } function getFmtCredentials() { svc_name="$1" login_uri="$2" abbrev="$3" username="$4" password="$5" echo "$svc_name, $username, $password, $login_uri" } function sendRootCAEmail() { is_sudo=$1 mail_msg="" mail_msg=${mail_msg}"Below is the public root certificate for your network. " mail_msg=${mail_msg}"If you are connected to the internal network, then it can also be downloaded via: \n" mail_msg=${mail_msg}"PEM: http://$SUB_FILES.$HOMESERVER_DOMAIN/ca.crt \n" mail_msg=${mail_msg}"DER: http://$SUB_FILES.$HOMESERVER_DOMAIN/ca.der \n" mail_msg=${mail_msg}"Add this to any device that you wish to use to access web-based resources on your network. " mail_msg=${mail_msg}"It only needs to be installed once, and is good for at least 40 years. " mail_msg=${mail_msg}"It is also included as an attachment in both formats (Some mobile devices may require the DER format). " mail_msg=${mail_msg}"You can freely share this with anyone, it will not compromise any security measures. \n" mail_msg=${mail_msg}"\n######################### Root CA Begin #########################\n\n" mail_msg=${mail_msg}"""$(cat $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt)""" mail_msg=${mail_msg}"\n\n########################## Root CA End ##########################\n" if [ "$is_sudo" = "true" ]; then # This special case is only need during the initial installation due # to issues with adding members to groups (mailsenders) within a script. set +e num_email_tries=1 max_email_tries=5 is_success=false while [ $num_email_tries -lt $max_email_tries ] do rm -f dead.letter echo -e "$mail_msg" | sudo timeout $MAILX_TIMEOUT mailx -s "Public Root Certificate" -a "From: $(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -a "Message-Id: <$(uuidgen)@$HOMESERVER_DOMAIN>" -A $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -A $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der "$EMAIL_ADMIN_EMAIL_ADDRESS" if [ $? -eq 0 ]; then is_success=true break fi echo "ERROR: Could not send email, retrying ($num_email_tries of $max_email_tries)..." sleep 3 ((num_email_tries++)) done if ! [ "$is_success" = "true" ]; then echo "ERROR: There was a problem sending the Root CA certificate..." fi set -e else sendEmail -s "Public Root Certificate" -b "$mail_msg" -a $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -a $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der fi } function getDNSRecordsInfo() { cur_domain=$1 str_res="DNS Info for ${cur_domain}:\n" str_res="${str_res}========================General========================\n" str_res="${str_res}1. Type: A | Name: @ | Value: $RELAYSERVER_SERVER_IP |\n" str_res="${str_res}2. Type: A | Name: * | Value: $RELAYSERVER_SERVER_IP |\n" str_res="${str_res}=======================================================\n" str_res="${str_res}=========================Email=========================\n" str_res="${str_res}1. Type: MX | Name: @ | Priority: 0 | Value: $RELAYSERVER_EXT_EMAIL_HOSTNAME |\n" str_res="${str_res}2. Type: TXT | Name: @ | Value: v=spf1 mx -all |\n" str_res="${str_res}3. Type: TXT | Name: * | Value: v=spf1 -all |\n" str_res="${str_res}4. Type: TXT | Name: ${cur_domain}._report._dmarc | Value: v=DMARC1 |\n" str_res="${str_res}5. Type: TXT | Name: _dmarc | Value: v=DMARC1;p=reject;rua=mailto:$EMAIL_ADMIN_EMAIL_ADDRESS;ruf=mailto:$EMAIL_ADMIN_EMAIL_ADDRESS;adkim=s;aspf=s |\n" str_res="${str_res}6*. Type: TXT | Name: dkim._domainkey | Value: v=DKIM1;k=rsa;p=YourDomainKey |\n" str_res="${str_res}\n* - Ensure you replace YourDomainKey correctly\n" str_res="${str_res}=======================================================" echo "$str_res" } function outputDNSZoneFile() { cur_domain="$1" strFilename="$2" tee $strFilename >/dev/null <" $email_attachments "$email_to" if [ $? -eq 0 ]; then is_email_success=true break fi echo "ERROR: Could not send email, retrying ($num_email_tries of $max_email_tries)..." sleep 3 ((num_email_tries++)) done if ! [ "$is_email_success" = "true" ]; then echo "ERROR: There was a problem sending this email (subj: $email_subj)..." fi } function sendEmailToList() { send_email_list="$1" send_email_subj="$2" send_email_body="$3" for curEmail in "${send_email_list[@]}" do echo "Sending email to $curEmail" sendEmail -s "$send_email_subj" -b "$send_email_body" -t "$curEmail" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" done } function loadSvcVars() { OLDIFS=$IFS IFS=$(echo -en "\n\b") SVCS_ARR=($(grep "SVCD_" $CONFIG_FILE)) subdom_list=() for curSVC in "${SVCS_ARR[@]}" do var_base=$(echo $curSVC | cut -d"=" -f1 | cut -d"_" -f 2-) stack_name=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f1) service_name=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f2) net_default=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f3) user_type=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f4) formal_name=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f5) subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6) subdom="${subdom//\"}" manage_tls=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f7) for curSub in "${subdom_list[@]}" do if [ "$curSub" = "$subdom" ]; then echo "FATAL: A duplicate subdomain ($curSub) was found in the configuration file. Please fix this issue and restart the script." exit 1 fi done subdom_list+=($subdom) printf -v "STACKNAME_${var_base}" '%s' "${stack_name//\"}" printf -v "SERVICENAME_${var_base}" '%s' "${service_name//\"}" printf -v "NETDEFAULT_${var_base}" '%s' "${net_default//\"}" printf -v "USERTYPE_${var_base}" '%s' "${user_type//\"}" printf -v "FMLNAME_${var_base}" '%s' "${formal_name//\"}" printf -v "SUB_${var_base}" '%s' "${subdom//\"}" printf -v "MANAGETLS_${var_base}" '%s' "${manage_tls//\"}" done IFS=$OLDIFS } function loadConfigVars() { lcv_curE=${-//[^e]/} is_chk_update="$1" initServiceVars set -e sudo -v if [ $? -ne 0 ]; then # Fatal echo "Error obtaining sudo privileges, exiting..." exit 1 fi set +e if sudo test -f $HSHQ_PLAINTEXT_ROOT_CONFIG; then set -e ptvar="$(sudo cat $HSHQ_PLAINTEXT_ROOT_CONFIG)" source <(echo "$ptvar") fi set +e if test -f $HSHQ_PLAINTEXT_USER_CONFIG; then set -e source $HSHQ_PLAINTEXT_USER_CONFIG fi set +e fixConfigV130 set -e source $CONFIG_FILE if [ -z "$IS_CONFIG_INIT" ]; then echo "FATAL: Decrypted config file did not load properly" exit 9 fi loadSvcVars if ! [ "$is_chk_update" = "false" ]; then checkUpdateVersion fi set +e if ! [ -z "$lcv_curE" ]; then set -e fi } function fixConfigV130() { sed -i "s/,le\"\"/,le\"/g" $CONFIG_FILE } function checkAddSvc() { svcVar=$(echo $1 | cut -d"=" -f1) svcVal=$(echo $1 | cut -d"=" -f2) grep -q "^${svcVar}=" $CONFIG_FILE if [ $? -ne 0 ]; then sed -i "s|^# Services Info END|${svcVar}=\"${svcVal}\"\n# Services Info END|g" $CONFIG_FILE fi } function loadDirectoryStructure() { if [ -z "$HSHQ_BASE_DIR" ]; then HSHQ_BASE_DIR=$HOME/hshq fi HSHQ_DATA_DIR=$HSHQ_BASE_DIR/data HSHQ_BACKUP_DIR=$HSHQ_BASE_DIR/backup HSHQ_NONBACKUP_DIR=$HSHQ_BASE_DIR/nonbackup HSHQ_ASSETS_DIR=$HSHQ_DATA_DIR/assets HSHQ_AIMODELS_DIR=$HSHQ_NONBACKUP_DIR/aimodels HSHQ_BUILD_DIR=$HSHQ_NONBACKUP_DIR/build HSHQ_CONFIG_DIR=$HSHQ_DATA_DIR/config HSHQ_LIB_DIR=$HSHQ_DATA_DIR/lib HSHQ_RELAYSERVER_DIR=$HSHQ_DATA_DIR/relayserver HSHQ_SCRIPTS_DIR=$HSHQ_DATA_DIR/scripts HSHQ_SECRETS_DIR=$HSHQ_DATA_DIR/secrets HSHQ_SSL_DIR=$HSHQ_DATA_DIR/ssl HSHQ_STACKS_DIR=$HSHQ_DATA_DIR/stacks HSHQ_WIREGUARD_DIR=$HSHQ_DATA_DIR/wireguard HSHQ_RESTORE_DIR=$HOME/hshqrestore HSHQ_WRAP_SCRIPT=$HOME/$HSHQ_WRAP_FILENAME HSHQ_LIB_SCRIPT=$HSHQ_LIB_DIR/$HSHQ_LIB_FILENAME HSHQ_NEW_LIB_SCRIPT=$HSHQ_LIB_DIR/$HSHQ_NEW_LIB_FILENAME if ! [ -f $HSHQ_LIB_DIR/$LOCK_UTILS_FILENAME ]; then outputLockUtilsScript fi source $HSHQ_LIB_DIR/$LOCK_UTILS_FILENAME } function getLatestVersionWrapper() { if [ -f $HOME/hshq/$IS_HSHQ_DEV_FILENAME ]; then vnum=$(curl -L --silent $HSHQ_WRAP_DEV_VER_URL | head -n 1) elif [ -f $HOME/hshq/$IS_HSHQ_TEST_FILENAME ]; then vnum=$(curl -L --silent $HSHQ_WRAP_TEST_VER_URL | head -n 1) else vnum=$(curl -L --silent $HSHQ_WRAP_VER_URL | head -n 1) fi if [ "$(checkValidVersionNumber "$vnum")" = "true" ]; then echo "$vnum" fi } function getLatestWrapperURL() { if [ -f $HOME/hshq/$IS_HSHQ_DEV_FILENAME ]; then echo "$HSHQ_WRAP_DEV_URL" elif [ -f $HOME/hshq/$IS_HSHQ_TEST_FILENAME ]; then echo "$HSHQ_WRAP_TEST_URL" else echo "$HSHQ_WRAP_URL" fi } function getThisVersionWrapper() { if ! [ -f $HSHQ_WRAP_SCRIPT ]; then echo 0 fi echo $(sed -n 2p $HSHQ_WRAP_SCRIPT | cut -d"=" -f2) } function getLatestVersionLib() { if [ -f $HOME/hshq/$IS_HSHQ_DEV_FILENAME ]; then vnum=$(curl -L --silent $HSHQ_LIB_DEV_VER_URL | head -n 1) elif [ -f $HOME/hshq/$IS_HSHQ_TEST_FILENAME ]; then vnum=$(curl -L --silent $HSHQ_LIB_TEST_VER_URL | head -n 1) else vnum=$(curl -L --silent $HSHQ_LIB_VER_URL | head -n 1) fi if [ "$(checkValidVersionNumber "$vnum")" = "true" ]; then echo "$vnum" fi } function getLatestLibURL() { if [ -f $HOME/hshq/$IS_HSHQ_DEV_FILENAME ]; then echo "$HSHQ_LIB_DEV_URL" elif [ -f $HOME/hshq/$IS_HSHQ_TEST_FILENAME ]; then echo "$HSHQ_LIB_TEST_URL" else echo "$HSHQ_LIB_URL" fi } function getThisVersionLib() { if ! [ -f $HSHQ_LIB_SCRIPT ]; then echo 0 fi echo $(sed -n 2p $HSHQ_LIB_SCRIPT | cut -d"=" -f2) } function getPendingVersionLib() { if [ -f $HSHQ_NEW_LIB_SCRIPT ]; then echo $(sed -n 2p $HSHQ_NEW_LIB_SCRIPT | cut -d"=" -f2) fi } function verifyFile() { # Perform 4 checks: # 1 - Ensure both src and sig files exist # 2 - Verify the file # 3 - Ensure the verification process used the correct key # 4 - Ensure the output contains "Good signature" (This step is likely redundant, but thats fine) src_file=$1 sig_file=$2 if ! [ -f "$src_file" ] || ! [ -f "$sig_file" ]; then return 4 fi gpg --verify $sig_file $src_file >/dev/null 2>/tmp/verify ver_res=$? if [ $ver_res -ne 0 ]; then rm -f /tmp/verify return $ver_res fi grep $HSHQ_GPG_FINGERPRINT /tmp/verify > /dev/null 2>&1 ver_res=$? if [ $ver_res -ne 0 ]; then rm -f /tmp/verify return $ver_res fi grep "Good signature" /tmp/verify > /dev/null 2>&1 ver_res=$ver_res if [ $? -ne 0 ]; then rm -f /tmp/verify return $ver_res fi rm -f /tmp/verify } function getRandomRequestID() { # Use the date, plus throw some randomness at the end. # Could use a GUID/UUID, but date is more informative. echo "$(date '+%Y%m%d%H%M%S')$((1000 + RANDOM % 9999))$((1000 + RANDOM % 9999))$((10 + RANDOM % 99))" } # BEGIN bash_progress_bar # # The following section of code was obtained # from the project: # https://github.com/pollev/bash_progress_bar # It consists of a total of 12 functions, # and the code is enclosed between the comments: # BEGIN bash_progress_bar and END bash_progress_bar # # SPDX-License-Identifier: MIT # # Copyright (c) 2018--2020 Polle Vanhoof # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Usage: # Source this script # enable_trapping <- optional to clean up properly if user presses ctrl-c # setup_scroll_area <- create empty progress bar # draw_progress_bar 10 <- advance progress bar # draw_progress_bar 40 <- advance progress bar # block_progress_bar 45 <- turns the progress bar yellow to indicate some action is requested from the user # draw_progress_bar 90 <- advance progress bar # destroy_scroll_area <- remove progress bar function pb_init_constants() { # Constants CODE_SAVE_CURSOR="\033[s" CODE_RESTORE_CURSOR="\033[u" CODE_CURSOR_IN_SCROLL_AREA="\033[1A" COLOR_FG="\e[30m" COLOR_BG="\e[42m" COLOR_BG_BLOCKED="\e[43m" RESTORE_FG="\e[39m" RESTORE_BG="\e[49m" # Variables PROGRESS_BLOCKED="false" TRAPPING_ENABLED="false" ETA_ENABLED="false" TRAP_SET="false" CURRENT_NR_LINES=0 PROGRESS_TITLE="" PROGRESS_TOTAL=100 PROGRESS_START=0 BLOCKED_START=0 } function setup_scroll_area() { pb_init_constants # If trapping is enabled, we will want to activate it whenever we setup the scroll area and remove it when we break the scroll area if [ "$TRAPPING_ENABLED" = "true" ]; then trap_on_interrupt fi # Handle first parameter: alternative progress bar title [ -n "$1" ] && PROGRESS_TITLE="$1" || PROGRESS_TITLE="Progress" # Handle second parameter : alternative total count [ -n "$2" ] && PROGRESS_TOTAL=$2 || PROGRESS_TOTAL=100 lines=$(tput lines) CURRENT_NR_LINES=$lines lines=$((lines-1)) # Scroll down a bit to avoid visual glitch when the screen area shrinks by one row echo -en "\n" # Save cursor echo -en "$CODE_SAVE_CURSOR" # Set scroll region (this will place the cursor in the top left) echo -en "\033[0;${lines}r" # Restore cursor but ensure its inside the scrolling area echo -en "$CODE_RESTORE_CURSOR" echo -en "$CODE_CURSOR_IN_SCROLL_AREA" # Store start timestamp to compute ETA if [ "$ETA_ENABLED" = "true" ]; then PROGRESS_START=$( date +%s ) fi # Start empty progress bar draw_progress_bar 0 } function destroy_scroll_area() { lines=$(tput lines) # Save cursor echo -en "$CODE_SAVE_CURSOR" # Set scroll region (this will place the cursor in the top left) echo -en "\033[0;${lines}r" # Restore cursor but ensure its inside the scrolling area echo -en "$CODE_RESTORE_CURSOR" echo -en "$CODE_CURSOR_IN_SCROLL_AREA" # We are done so clear the scroll bar clear_progress_bar # Scroll down a bit to avoid visual glitch when the screen area grows by one row echo -en "\n\n" # Reset title for next usage PROGRESS_TITLE="" # Once the scroll area is cleared, we want to remove any trap previously set. Otherwise, ctrl+c will exit our shell if [ "$TRAP_SET" = "true" ]; then trap - EXIT fi } function format_eta() { local T=$1 local D=$((T/60/60/24)) local H=$((T/60/60%24)) local M=$((T/60%60)) local S=$((T%60)) [ $D -eq 0 -a $H -eq 0 -a $M -eq 0 -a $S -eq 0 ] && echo "--:--:--" && return [ $D -gt 0 ] && printf '%d days, ' $D printf 'ETA: %d:%02.f:%02.f' $H $M $S } function draw_progress_bar() { if ! [ "$is_draw_pb" = "true" ]; then return fi eta="" if [ "$ETA_ENABLED" = "true" -a $1 -gt 0 ]; then if [ "$PROGRESS_BLOCKED" = "true" ]; then blocked_duration=$(($(date +%s)-$BLOCKED_START)) PROGRESS_START=$((PROGRESS_START+blocked_duration)) fi running_time=$(($(date +%s)-PROGRESS_START)) total_time=$((PROGRESS_TOTAL*running_time/$1)) eta=$( format_eta $(($total_time-$running_time)) ) fi percentage=$1 if [ $PROGRESS_TOTAL -ne 100 ] then [ $PROGRESS_TOTAL -eq 0 ] && percentage=100 || percentage=$((percentage*100/$PROGRESS_TOTAL)) fi extra=$2 lines=$(tput lines) lines=$((lines)) # Check if the window has been resized. If so, reset the scroll area if [ "$lines" -ne "$CURRENT_NR_LINES" ]; then setup_scroll_area fi # Save cursor echo -en "$CODE_SAVE_CURSOR" # Move cursor position to last row echo -en "\033[${lines};0f" # Clear progress bar tput el # Draw progress bar PROGRESS_BLOCKED="false" print_bar_text $percentage "$extra" "$eta" # Restore cursor position echo -en "$CODE_RESTORE_CURSOR" } function block_progress_bar() { percentage=$1 lines=$(tput lines) lines=$((lines)) # Save cursor echo -en "$CODE_SAVE_CURSOR" # Move cursor position to last row echo -en "\033[${lines};0f" # Clear progress bar tput el # Draw progress bar PROGRESS_BLOCKED="true" BLOCKED_START=$( date +%s ) print_bar_text $percentage # Restore cursor position echo -en "$CODE_RESTORE_CURSOR" } function clear_progress_bar() { lines=$(tput lines) lines=$((lines)) # Save cursor echo -en "$CODE_SAVE_CURSOR" # Move cursor position to last row echo -en "\033[${lines};0f" # clear progress bar tput el # Restore cursor position echo -en "$CODE_RESTORE_CURSOR" } function print_bar_text() { local percentage=$1 local extra=$2 [ -n "$extra" ] && extra=" ($extra)" local eta=$3 if [ -n "$eta" ]; then [ -n "$extra" ] && extra="$extra " extra="$extra$eta" fi local cols=$(tput cols) bar_size=$((cols-9-${#PROGRESS_TITLE}-${#extra})) local color="${COLOR_FG}${COLOR_BG}" if [ "$PROGRESS_BLOCKED" = "true" ]; then color="${COLOR_FG}${COLOR_BG_BLOCKED}" fi # Prepare progress bar complete_size=$(((bar_size*percentage)/100)) remainder_size=$((bar_size-complete_size)) progress_bar=$(echo -ne "["; echo -en "${color}"; printf_new "#" $complete_size; echo -en "${RESTORE_FG}${RESTORE_BG}"; printf_new "." $remainder_size; echo -ne "]"); # Print progress bar echo -ne " $PROGRESS_TITLE ${percentage}% ${progress_bar}${extra}" } function enable_trapping() { TRAPPING_ENABLED="true" } function trap_on_interrupt() { # If this function is called, we setup an interrupt handler to cleanup the progress bar TRAP_SET="true" trap cleanup_on_interrupt EXIT } function cleanup_on_interrupt() { destroy_scroll_area exit } function printf_new() { str=$1 num=$2 v=$(printf "%-${num}s" "$str") echo -ne "${v// /$str}" } # END bash_progress_bar # RelayServer Utils function resetCaddyDataRelayServer() { is_prompt=$1 if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then if [ "$is_prompt" = "true" ]; then showMessageBox "No RelayServer" "You do not have a RelayServer set up, exiting..." else echo "ERROR: You do not have a RelayServer set up." fi return fi perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo $RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh" -f } function getVPNIPFromHostname() { lookup_hostname=$1 base_domain=$(getBaseDomain $lookup_hostname) if [ "$base_domain" = "$HOMESERVER_DOMAIN" ]; then echo $(getPrimaryVPN_IP) return fi ip_addr=$(sqlite3 $HSHQ_DB "select IPAddress from connections join hsvpn_connections on connections.ID = hsvpn_connections.ID where ConnectionType='homeserver_vpn' and NetworkType='mynetwork' and DomainName='$lookup_hostname';") echo "$ip_addr" } function addDomainsToRelayServer() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then showMessageBox "No RelayServer" "You do not have a RelayServer setup, exiting..." return fi set +e domains_to_add="" while [ -z "$domains_to_add" ] do domains_to_add=$(promptUserInputMenu "" "Enter New Domains" "Enter the new domain names that you wish to add, separated by comma (no spaces):") mbres=$? if [ $mbres -ne 0 ]; then return 1 fi if [ -z "$domains_to_add" ]; then showMessageBox "Domains List Empty" "The domains list cannot be empty" elif [ $(checkValidString "$domains_to_add" ",.-") = "false" ]; then showMessageBox "Invalid Character(s)" "The domain list contain invalid character(s). The value must consist of a-z (lowercase), 0-9, -, and/or ." domains_to_add="" fi domains_to_add_Arr=($(echo $domains_to_add | tr "," "\n")) for curDomain in "${domains_to_add_Arr[@]}" do if [ -z "$curDomain" ] || [ $(checkValidString "$curDomain" ".-") = "false" ]; then showMessageBox "Invalid Character(s)" "A domain contains invalid character(s). The value must consist of a-z (lowercase), 0-9, -, and/or ." domains_to_add="" fi done done deliveryhostmenu=$(cat << EOF $(getLogo) Select the mail delivery host for this domain: EOF ) dh_arr=($(sqlite3 $HSHQ_DB "select ID,MailHost from mailhosts;")) menu_items="" for curdh in "${dh_arr[@]}" do menu_items=${menu_items}"$(echo "("$(echo $curdh | sed 's/|/)/1')) | OFF " done sel_host=$(whiptail --title "Select Delivery Host" --radiolist "$deliveryhostmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3) if [ $? -ne 0 ] || [ -z "$sel_host" ]; then showMessageBox "Selection Empty" "Delivery host selection is empty, returning..." return 1 fi mail_host_id=$(echo $sel_host | cut -d ")" -f1 | sed 's/(//' | sed 's/"//g') deliver_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections join hsvpn_connections on connections.ID = hsvpn_connections.ID where MailHostID=$mail_host_id;") if [ -z "$deliver_ip" ]; then showMessageBox "IP Not Found" "There is no domain in your network based on this delivery host, returning..." return 1 fi deliver_to_host=$(sqlite3 $HSHQ_DB "select MailHost from mailhosts where ID=$mail_host_id;") perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addRelayedDomains.sh $domains_to_add $deliver_to_host $deliver_ip" -f mbres=$? if [ $mbres -ne 0 ]; then showMessageBox "Error" "There was a problem adding the domain(s)" return 1 fi domains_to_add_Arr=($(echo $domains_to_add | tr "," "\n")) for curDomain in "${domains_to_add_Arr[@]}" do sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into mailhostmap(MailHostID,Domain,IsFirstDomain) values($mail_host_id,'$curDomain',false);" sendDNSRecordsEmail $curDomain done set -e } function removeDomainsFromRelayServer() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then showMessageBox "No RelayServer" "You do not have a RelayServer setup, exiting..." return fi set +e rem_menu=$(cat << EOF $(getLogo) Select the domains that you wish to remove: EOF ) domains_to_rem_qry=($(sqlite3 $HSHQ_DB "select Domain,MailHost from mailhostmap join mailhosts on mailhostmap.MailHostID = mailhosts.ID where IsFirstDomain=false;")) if [ -z "$domains_to_rem_qry" ]; then showMessageBox "Empty" "There are no eligible connections for this selection, returning..." return fi menu_items="" for curdom in "${domains_to_rem_qry[@]}" do menu_items=${menu_items}"$(echo $(echo $curdom | sed 's/|/(/1')")") | OFF " done sel_doms=($(whiptail --title "Select Domains" --checklist "$rem_menu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3)) is_remove=$(promptUserInputMenu "" "Confirm Removal" "This will remove these domains from the RelayServer. To confirm, enter the word 'remove' below:") if [ $? -ne 0 ] || [ -z "$is_remove" ] || ! [ "$is_remove" = "remove" ]; then showMessageBox "Incorrect Confirmation" "The text did not match, returning..." return fi domains_to_remove="" for dom in "${sel_doms[@]}" do curdom=$(echo $dom | cut -d "(" -f1 | sed 's/"//g') domains_to_remove="${domains_to_remove}${curdom}," done if -z [ "$domains_to_remove" ]; then showMessageBox "No Selection" "Nothing was selected, returning..." return fi domains_to_remove=${domains_to_remove%?} perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeRelayedDomains.sh $domains_to_remove" -f mbres=$? if [ $mbres -ne 0 ]; then showMessageBox "Error" "There was a problem removing the domain(s)" return 1 fi for dom in "${sel_doms[@]}" do curdom=$(echo $dom | cut -d "(" -f1 | sed 's/"//g') sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhostmap where Domain='$curdom';" done } function addSecondaryDomainToRelayServer() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi add_domain="$1" mail_host_id="$2" # Need to check domain name string for validity if [ -z "$add_domain" ] || [ $(checkValidString "$add_domain" ".-") = "false" ]; then echo "ERROR: Invalid domain name." return 5 fi deliver_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections join hsvpn_connections on connections.ID = hsvpn_connections.ID where MailHostID=$mail_host_id;") if [ -z "$deliver_ip" ]; then echo "ERROR: There is no domain in your network based on this delivery host." return 5 fi deliver_to_host=$(sqlite3 $HSHQ_DB "select MailHost from mailhosts where ID=$mail_host_id;") perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addRelayedDomains.sh $add_domain $deliver_to_host $deliver_ip" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: There was a problem adding the domain to the RelayServer." return 5 fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into mailhostmap(MailHostID,Domain,IsFirstDomain) values($mail_host_id,'$add_domain',false);" sendDNSRecordsEmail $add_domain } function removeSecondaryDomainFromRelayServer() { dom_to_remove="$1" le_arr=($(sqlite3 $HSHQ_DB "select Domain from lecertdomains where BaseDomain='$dom_to_remove';")) subdomlist="" for cursub in "${le_arr[@]}" do subdomlist=${subdomlist}"${cursub}," done if ! [ -z "$subdomlist" ]; then subdomlist=${subdomlist%?} echo "Removing LE cert paths from RelayServer for $dom_to_remove..." removeLECertPathsFromRelayServer "$subdomlist" mbres=$? if [ $mbres -ne 0 ]; then return $mbres fi fi exp_arr=($(sqlite3 $HSHQ_DB "select Domain from exposedomains where BaseDomain='$dom_to_remove';")) subdomlist="" for cursub in "${exp_arr[@]}" do subdomlist=${subdomlist}"${cursub}," done if ! [ -z "$subdomlist" ]; then subdomlist=${subdomlist%?} echo "Removing expose paths from RelayServer for $dom_to_remove..." removeExposeDomainPathsFromRelayServer "$subdomlist" mbres=$? if [ $mbres -ne 0 ]; then return $mbres fi fi echo "Removing $dom_to_remove from RelayServer..." perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeRelayedDomains.sh $dom_to_remove" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: There was a problem connecting to the RelayServer." return 1 fi sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from mailhostmap where Domain='$dom_to_remove';" } function addLECertPathsToRelayServerMsgbox() { set +e add_subdomains=$(promptUserInputMenu "" "Enter Subdomain" "Enter the subdomains for which you want LetsEncrypt to manage the certificates(separated by comma):") if [ $? -ne 0 ]; then return fi seldomainmenu=$(cat << EOF $(getLogo) Select the base domain to associate with these subdomains: EOF ) dom_arr=($(sqlite3 $HSHQ_DB "select Domain from mailhostmap order by MailHostID asc;")) menu_items="" for curdom in "${dom_arr[@]}" do menu_items=${menu_items}"$curdom | OFF " done sel_domain=$(whiptail --title "Select Base Domain" --radiolist "$seldomainmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3) if [ $? -ne 0 ] || [ -z "$sel_domain" ]; then showMessageBox "Selection Empty" "Base domain selection is empty, returning..." return 1 fi sel_domain=$(echo $sel_domain | cut -d "|" -f1 | sed 's/"//g') showYesNoMessageBox "Confirm Selection" "Adding $add_subdomains - associated with $sel_domain. Continue?" if [ $? -eq 0 ]; then addLECertPathsToRelayServer "$add_subdomains" "$sel_domain" fi } function addLECertPathsToRelayServer() { subdoms_le="$1" base_domain="$2" if [ $(checkValidString "$subdoms_le" ",.-") = "false" ]; then echo "ERROR: A subdomain contains invalid character(s). The value must consist of a-z (lowercase), 0-9, -, and/or ." return 1 fi newList="" add_subdomains_Arr=($(echo $subdoms_le | tr "," "\n")) for add_sub in "${add_subdomains_Arr[@]}" do if [ $(checkValidString "$add_sub" ".-") = "false" ]; then echo "ERROR: The subdomain contains invalid character(s). The value must consist of a-z (lowercase), 0-9, -, and/or ." return 2 fi checkSub=$(sqlite3 $HSHQ_DB "select Domain from lecertdomains where Domain='$add_sub';") if ! [ -z "$checkSub" ]; then echo "$checkSub LE path has already been added to RelayServer." else newList=$newList"${add_sub}," fi done if [ -z "$newList" ]; then return fi newList=${newList%?} if [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then sendEmail -s "Add LetsEncrypt Domain" -b "If you have not done so already, you need to contact your RelayServer administrator and ask them to add these subdomains to be managed by LetsEncrypt.\nSubdomains: $subdoms_le" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" return fi if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then if ! [ "$IS_INSTALLING" = "true" ]; then sendEmail -s "Add LetsEncrypt Domain" -b "If and when you setup a RelayServer, you will need to add this subdomain to be managed by LetsEncrypt: $subdoms_le" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" fi return fi perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addLECertDomains.sh $subdoms_le" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: There was a problem connecting to the RelayServer." return 6 fi add_subdomains_Arr=($(echo $subdoms_le | tr "," "\n")) for add_sub in "${add_subdomains_Arr[@]}" do sudo sqlite3 $HSHQ_DB "insert or ignore into lecertdomains(Domain,BaseDomain) values('$add_sub','$base_domain');" done } function removeLECertPathsFromRelayServer() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi subdoms_le="$1" perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeLECertDomains.sh $subdoms_le" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: There was a problem connecting to the RelayServer." return 1 fi rem_subdomains_Arr=($(echo $subdoms_le | tr "," "\n")) for rem_sub in "${rem_subdomains_Arr[@]}" do sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains where Domain='$rem_sub';" done } function addExposeDomainPathsToRelayServer() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi subdoms_exp="$1" base_domain="$2" if [ $(checkValidString "$subdoms_exp" ",.-") = "false" ]; then echo "ERROR: A subdomain contains invalid character(s). The value must consist of a-z (lowercase), 0-9, -, and/or ." return 2 fi add_subdomains_Arr=($(echo $subdoms_exp | tr "," "\n")) newList="" for add_sub in "${add_subdomains_Arr[@]}" do if [ $(checkValidString "$add_sub" ".-") = "false" ]; then echo "ERROR: The subdomain contains invalid character(s). The value must consist of a-z (lowercase), 0-9, -, and/or ." return 3 fi checkSub=$(sqlite3 $HSHQ_DB "select Domain from exposedomains where Domain='$add_sub';") if ! [ -z "$checkSub" ]; then echo "$checkSub is already exposed on the RelayServer." else newList=$newList"${add_sub}," fi done if [ -z "$newList" ]; then return fi last_relay_expose_filename=lastrelayexpose.txt if ! [ -f $HSHQ_CONFIG_DIR/$last_relay_expose_filename ]; then echo "$(date +%s)" > $HSHQ_CONFIG_DIR/$last_relay_expose_filename else lastExpose=$(head -n 1 $HSHQ_CONFIG_DIR/$last_relay_expose_filename) if [ $(date +%s) -lt $(($lastExpose+300)) ]; then echo "ERROR: You need to wait at least 5 minutes between subsequent requests to allow the certificate-issuing process to complete." return 4 fi fi newList=${newList%?} perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addExposeDomains.sh $subdoms_exp" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: There was a problem connecting to the RelayServer." return 5 fi add_subdomains_Arr=($(echo $subdoms_exp | tr "," "\n")) for add_sub in "${add_subdomains_Arr[@]}" do sudo sqlite3 $HSHQ_DB "insert or ignore into exposedomains(Domain,BaseDomain) values('$add_sub','$base_domain');" done echo "$(date +%s)" > $HSHQ_CONFIG_DIR/$last_relay_expose_filename } function removeExposeDomainPathsFromRelayServer() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi subdoms_exp="$1" perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeExposeDomains.sh $subdoms_exp" -f mbres=$? if [ $mbres -ne 0 ]; then echo "ERROR: There was a problem connecting to the RelayServer." return 1 fi rem_subdomains_Arr=($(echo $subdoms_exp | tr "," "\n")) for rem_sub in "${rem_subdomains_Arr[@]}" do sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from exposedomains where Domain='$rem_sub';" done } function showEmailMyNetworkHomeServerDNSListMenu() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then showMessageBox "No RelayServer" "You are not hosting a RelayServer, returning..." return fi sendEmailMyNetworkHomeServerDNSList } function showEmailMyNetworkHomeServerDNSListNoMenu() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi sendEmailMyNetworkHomeServerDNSList } function sendEmailMyNetworkHomeServerDNSList() { hs_email_subj="(MGR COPY)HomeServer DNS Update from $HOMESERVER_NAME" hs_email_body="$(getMyNetworkHomeServersDNSUpdateEmailBody)" echo "Sending HomeServer Update email to self" sendEmail -s "$hs_email_subj" -b "$hs_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function showEmailMyNetworkHomeServerDNSListClientDNSMenu() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then showMessageBox "No RelayServer" "You are not hosting a RelayServer, returning..." return fi sendEmailMyNetworkHomeServerDNSListClient } function showEmailMyNetworkHomeServerDNSListClientDNSNoMenu() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi sendEmailMyNetworkHomeServerDNSListClient } function sendEmailMyNetworkHomeServerDNSListClient() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi users_email_subj="(MGR COPY)HomeServer ClientDNS Update from $HOMESERVER_NAME" users_email_body="$(getMyNetworkHomeServerDNSListClientDNSBody)" echo "Sending ClientDNS email to self" sendEmail -s "$users_email_subj" -b "$users_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function sendEmailMyNetworkFullUserDetails() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi full_network_email_subj="Full User Details for Hosted VPN" full_network_email_body="" full_network_email_body=$full_network_email_body"Full User Details for Hosted VPN\n\n" full_network_email_body=$full_network_email_body"------------------------------------------------------------------------\n" full_network_email_body=$full_network_email_body"Name,Email,PublicKey,PresharedKey,IP Address,Is Internet?\n" full_network_email_body=$full_network_email_body"------------------------------------------------------------------------\n" user_arr=($(sqlite3 $HSHQ_DB "select ID from connections where NetworkType='mynetwork' and ConnectionType='user';")) for cur_user_id in "${user_arr[@]}" do cur_name=$(sqlite3 $HSHQ_DB "select Name from connections where ID='$cur_user_id';") cur_email=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID='$cur_user_id';") pub_key=$(sqlite3 $HSHQ_DB "select PublicKey from connections where ID='$cur_user_id';") pre_key=$(sqlite3 $HSHQ_DB "select PresharedKey from connections where ID='$cur_user_id';") ip_addr=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID='$cur_user_id';") is_int=$(sqlite3 $HSHQ_DB "select IsInternet from connections where ID='$cur_user_id';") is_internet="No" if [ $is_int = 1 ]; then is_internet="Yes" fi full_network_email_body=$full_network_email_body"$cur_name,$cur_email,$pub_key,$pre_key,$ip_addr,$is_internet\n" done full_network_email_body=$full_network_email_body"\n\n" sendEmail -s "$full_network_email_subj" -b "$full_network_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function sendEmailMyNetworkBroadcast() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "ERROR: You are not hosting a RelayServer." return 1 fi strMessage="$1" full_network_email_subj="Broadcase Message from $HOMESERVER_NAME" full_network_email_body="" full_network_email_body=$full_network_email_body"Broadcase Message from $HOMESERVER_NAME\n" full_network_email_body=$full_network_email_body"------------------------------------------------------------------------\n" full_network_email_body=$full_network_email_body"$strMessage\n\n" email_list=($(sqlite3 $HSHQ_DB "select EmailAddress from connections where NetworkType='mynetwork';")) for cur_email in "${email_list[@]}" do echo "Sending broadcast message to ${cur_email}..." sendEmail -s "$full_network_email_subj" -b "$full_network_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" -t "$cur_email" done # Send to self echo "Sending broadcast message to self..." sendEmail -s "$full_network_email_subj" -b "$full_network_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function notifyMyNetworkHomeServersDNSUpdate() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then return fi addOrRemove="$1" addRemoveHSName="$2" addRemoveDomainName="$3" hs_email_subj="HomeServer DNS Update from $HOMESERVER_NAME" hs_email_body=$(getMyNetworkHomeServersDNSUpdateEmailBody "$addOrRemove" "$addRemoveHSName" "$addRemoveDomainName") hs_list=($(sqlite3 $HSHQ_DB "select EmailAddress from connections where ConnectionType='homeserver_vpn' and NetworkType='mynetwork' group by EmailAddress;")) for curEmail in "${hs_list[@]}" do # Skip the domain being added/removed. This will skip anyone with an email address on that domain, but this is less impactful and likely better than the alternative. if ! [ "$(getDomainFromEmailAddress $curEmail)" = "$addRemoveDomainName" ]; then echo "Sending HomeServer Update email to $curEmail" sendEmail -s "$hs_email_subj" -b "$hs_email_body" -t "$curEmail" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" fi done echo "Sending HomeServer Update email to self" hs_email_subj="(MGR COPY)HomeServer DNS Update from $HOMESERVER_NAME" sendEmail -s "$hs_email_subj" -b "$hs_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function getDomainFromEmailAddress() { email_addr="$1" echo echo "$email_addr" | cut -d "@" -f2 } function notifyMyNetworkUsersDNSUpdate() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then return fi addOrRemove="$1" addRemoveHSName="$2" addRemoveDomainName="$3" addRemoveDomainNameExtPrefix="$4" addRemoveDomainNameIPAddress="$5" users_email_subj="HomeServer ClientDNS Update from $HOMESERVER_NAME" users_email_body=$(getMyNetworkHomeServerDNSListClientDNSBody "$addOrRemove" "$addRemoveHSName" "$addRemoveDomainName" "$addRemoveDomainNameExtPrefix" "$addRemoveDomainNameIPAddress") users_list=($(sqlite3 $HSHQ_DB "select EmailAddress from connections where ConnectionType='user' and NetworkType='mynetwork' group by EmailAddress;")) for curEmail in "${users_list[@]}" do echo "Sending ClientDNS email to $curEmail" sendEmail -s "$users_email_subj" -b "$users_email_body" -t "$curEmail" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" done echo "Sending ClientDNS email to self" users_email_subj="(MGR COPY)HomeServer ClientDNS Update from $HOMESERVER_NAME" sendEmail -s "$users_email_subj" -b "$users_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function getMyNetworkHomeServersDNSUpdateEmailBody() { addOrRemove="$1" addRemoveHSName="$2" addRemoveDomainName="$3" email_body="" email_body=${email_body}"HomeServer DNS Update from $HOMESERVER_NAME\n" email_body=${email_body}"=======================================================================\n\n" if ! [ -z "$addOrRemove" ]; then case "$addOrRemove" in add) email_body=${email_body}"ADDED the following HomeServer: \n\n" email_body=${email_body}"-----------------------------------------------------------------------\n" email_body=${email_body}"HomeServer Name: ${addRemoveHSName}\n" email_body=${email_body}"HomeServer Domain: ${addRemoveDomainName}\n" email_body=${email_body}"-----------------------------------------------------------------------\n\n" ;; remove) email_body=${email_body}"REMOVED the following HomeServer: \n\n" email_body=${email_body}"-----------------------------------------------------------------------\n" email_body=${email_body}"HomeServer Name: ${addRemoveHSName}\n" email_body=${email_body}"HomeServer Domain: ${addRemoveDomainName}\n" email_body=${email_body}"-----------------------------------------------------------------------\n\n" ;; esac fi email_body=${email_body}"1. Ensure this is the most recent version of this email with matching subject\n" email_body=${email_body}"line (HomeServer DNS Update from $HOMESERVER_NAME).\n\n" email_body=${email_body}"2. Go to the Script-server app, and navigate to 07 Other Networks > 08 Update HomeServer DNS\n" email_body=${email_body}"and paste the ENTIRE list BELOW the following line where appropriate.\n\n" email_body=${email_body}"3. After applying the update, you may delete any prior emails with the matching\n" email_body=${email_body}"subject line (HomeServer DNS Update from $HOMESERVER_NAME).\n\n\n" email_body=${email_body}"HomeServer Name|Host Domain|Peer Domain|External Prefix|IP Address\n" email_body=${email_body}"_______________________________________________________________________\n\n" email_body=${email_body}"$(getMyNetworkHomeServerDNSList)\n\n" echo "${email_body}" } function getMyNetworkHomeServerDNSListClientDNSBody() { addOrRemove="$1" addRemoveHSName="$2" addRemoveDomainName="$3" addRemoveDomainNameExtPrefix="$4" addRemoveDomainNameIPAddress="$5" email_body="" email_body=${email_body}"HomeServer ClientDNS Update from $HOMESERVER_NAME\n" email_body=${email_body}"=======================================================================\n\n" email_body=${email_body}"If your client device uses a DNS server in this network\n" email_body=${email_body}"($PRIMARY_VPN_SUBNET), then you can likely disregard this message.\n" email_body=${email_body}"Otherwise, see changes and full DNS list below and update accordingly.\n" email_body=${email_body}"These entries are formatted for DNSMasq and can be dropped directly\n" email_body=${email_body}"into your ClientDNS server.\n\n" if ! [ -z "$addOrRemove" ]; then case "$addOrRemove" in add) email_body=${email_body}"ADDED the following HomeServer: \n\n" email_body=${email_body}"-----------------------------------------------------------------------\n" email_body=${email_body}"# ${addRemoveHSName} \n" email_body=${email_body}"server=/.${addRemoveDomainNameExtPrefix}.${addRemoveDomainName}/127.0.0.11\n" email_body=${email_body}"address=/.${addRemoveDomainName}/${addRemoveDomainNameIPAddress}\n" email_body=${email_body}"-----------------------------------------------------------------------\n\n\n" ;; remove) email_body=${email_body}"REMOVED the following HomeServer: \n\n" email_body=${email_body}"-----------------------------------------------------------------------\n" email_body=${email_body}"# ${addRemoveHSName} \n" email_body=${email_body}"server=/.${addRemoveDomainNameExtPrefix}.${addRemoveDomainName}/127.0.0.11\n" email_body=${email_body}"address=/.${addRemoveDomainName}/${addRemoveDomainNameIPAddress}\n" email_body=${email_body}"-----------------------------------------------------------------------\n\n\n" ;; esac fi email_body=${email_body}"Full Domain List for ${HOMESERVER_NAME}: \n" email_body=${email_body}"_______________________________________________________________________\n\n" email_body=${email_body}"$(getMyNetworkHomeServerDNSListForClientDNS)\n\n" echo "${email_body}" } function notifyMyNetworkFullRemoval() { hs_email_join_addendum="" hs_email_subj="HomeServer Network Reset Notice from $HOMESERVER_NAME" hs_email_body="" hs_email_body=${hs_email_body}"HomeServer Network Reset Notice from $HOMESERVER_NAME\n" hs_email_body=${hs_email_body}"================================================================\n\n" hs_email_body=${hs_email_body}"$HOMESERVER_NAME has initiated a full network reset. \n" hs_email_body=${hs_email_body}"Your HomeServer along with all other connections have been \n" hs_email_body=${hs_email_body}"removed from the network. Please disconnect accordingly.\n\n" hs_email_body=${hs_email_body}"Host Domain: $HOMESERVER_DOMAIN\n" hs_email_body=${hs_email_body}"Reason Provided: $disconnect_reason\n\n" full_hs_list=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='mynetwork';")) for curID in "${full_hs_list[@]}" do curEmail=$(sqlite3 $HSHQ_DB "select EmailAddress from connections where ID=$curID;") is_primary=$(sqlite3 $HSHQ_DB "select IsPrimary from hsvpn_connections where ID=$curID;") hs_email_join_addendum="" if [ $is_primary = 1 ]; then hs_email_join_addendum=${hs_email_join_addendum}"\nThis is your HomeServer's primary VPN connection. Thus, you will\n" hs_email_join_addendum=${hs_email_join_addendum}"lose the ability to receive external emails, etc. In the $HSHQ_WRAP_FILENAME script,\n" hs_email_join_addendum=${hs_email_join_addendum}"go to Network -> RelayServer Utils -> Create or Join a VPN RelayServer\n" hs_email_join_addendum=${hs_email_join_addendum}"and set up a new primary VPN connection.\n\n" fi cur_email_body="${hs_email_body}\n${hs_email_join_addendum}" echo "Sending email to $curEmail" sendEmail -s "$hs_email_subj" -b "$cur_email_body" -t "$curEmail" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" done user_email_subj="HomeServer Network Reset Notice from $HOMESERVER_NAME" user_email_body="" user_email_body=${user_email_body}"HomeServer Network Reset Notice from $HOMESERVER_NAME\n" user_email_body=${user_email_body}"================================================================\n\n" user_email_body=${user_email_body}"$HOMESERVER_NAME has initiated a full network reset. \n" user_email_body=${user_email_body}"Your client connection along with all other connections have been \n" user_email_body=${user_email_body}"removed from the network. Please remove this connection accordingly.\n\n" user_email_body=${user_email_body}"Host Domain: $HOMESERVER_DOMAIN\n" user_email_body=${user_email_body}"Reason Provided: $disconnect_reason\n\n" full_user_list=($(sqlite3 $HSHQ_DB "select EmailAddress from connections where ConnectionType='user' and NetworkType='mynetwork' group by EmailAddress;")) for curEmail in "${full_user_list[@]}" do echo "Sending email to $curEmail" sendEmail -s "$user_email_subj" -b "$user_email_body" -t "$curEmail" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" done } function notifyMyNetworkTransferRelayServer() { new_rs_ip="$1" if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then return fi users_email_subj="RelayServer Transfer Notice from $HOMESERVER_NAME" users_email_body="RelayServer Transfer Notice from ${HOMESERVER_NAME}\n" users_email_body=${users_email_body}"=======================================================================\n\n" users_email_body=${users_email_body}"$HOMESERVER_NAME has performed a RelayServer transfer. You will\n" users_email_body=${users_email_body}"need to disable, then re-enable your WireGuard client(s) in order\n" users_email_body=${users_email_body}"to restore connection to this network. There may also be some\n" users_email_body=${users_email_body}"short-term connectivity issues with servers on this network\n" users_email_body=${users_email_body}"while the network is auto-adjusting to this change.\n\n" users_list=($(sqlite3 $HSHQ_DB "select EmailAddress from connections where ConnectionType='user' and NetworkType='mynetwork' group by EmailAddress;")) for curEmail in "${users_list[@]}" do echo "Sending Transfer Notice email to $curEmail" sendEmail -s "$users_email_subj" -b "$users_email_body" -t "$curEmail" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" done echo "Sending Transfer Notice email to self" users_email_subj="(MGR COPY)RelayServer Transfer Notice from $HOMESERVER_NAME" other_doms=($(sqlite3 $HSHQ_DB "select Domain from mailhostmap where Domain not in ('$HOMESERVER_DOMAIN');")) doms_list="" for curDom in "${other_doms[@]}" do doms_list=${doms_list}${curDom}"\n" done mgr_email_body="$users_email_body" if ! [ -z "$doms_list" ]; then mgr_email_body=${mgr_email_body}"The following domains also need the DNS records to be updated\n" mgr_email_body=${mgr_email_body}"with the new RelayServer IP Address ($new_rs_ip): \n${doms_list}" fi sendEmail -s "$users_email_subj" -b "$mgr_email_body" -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function getMyNetworkHomeServerDNSList() { strOutput="" hs_arr=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType in ('primary','mynetwork');")) for curID in "${hs_arr[@]}" do strOutput=${strOutput}"$(sqlite3 $HSHQ_DB "select HomeServerName from hsvpn_connections where ID=$curID;")|" strOutput=${strOutput}"$HOMESERVER_DOMAIN|" strOutput=${strOutput}"$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$curID;")|" strOutput=${strOutput}"$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$curID;")|" strOutput=${strOutput}"$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$curID;")\n" done echo "$strOutput" strOutput="" } function getMyNetworkHomeServerDNSListForClientDNS() { strOutput="" # First, output our primary network info prim_id=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='primary';") curExtPrefix=$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$prim_id;") curIntPrefix=$(sqlite3 $HSHQ_DB "select InternalPrefix from hsvpn_connections where ID=$prim_id;") curDomName=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$prim_id;") curHSName=$(sqlite3 $HSHQ_DB "select HomeServerName from hsvpn_connections where ID=$prim_id;") curIPAddr=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$prim_id;") strOutput=${strOutput}"# ${curHSName}\n" strOutput=${strOutput}"server=/.${curExtPrefix}.${curDomName}/127.0.0.11\n" strOutput=${strOutput}"address=/.${curDomName}/${curIPAddr}\n" curIPAddr=$(sqlite3 $HSHQ_DB "select RS_VPN_IP from hsvpn_connections where ID=$prim_id;") strOutput=${strOutput}"address=/.${curIntPrefix}.${curDomName}/${curIPAddr}\n\n" # Then, output the rest of the network. Slightly repetitive of above, but ensures primary network gets output first, for organizational purposes. hs_arr=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='mynetwork';")) for curID in "${hs_arr[@]}" do curExtPrefix=$(sqlite3 $HSHQ_DB "select ExternalPrefix from hsvpn_connections where ID=$curID;") curDomName=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections where ID=$curID;") curHSName=$(sqlite3 $HSHQ_DB "select HomeServerName from hsvpn_connections where ID=$curID;") curIPAddr=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$curID;") strOutput=${strOutput}"# ${curHSName}\n" strOutput=${strOutput}"server=/.${curExtPrefix}.${curDomName}/127.0.0.11\n" strOutput=${strOutput}"address=/.${curDomName}/${curIPAddr}\n\n" done echo "$strOutput" strOutput="" } function showUpdateHomeServerDNSMenu() { dns_file=$HOME/dns.tmp showMessageBox "Paste DNS Entries" "In this next step, you will need to paste the entire list of DNS entries. After pasting, press CTRL-o to save, then Enter to confirm, then CTRL-x to exit. Please remember these key combinations. Press OK to proceed." nano $dns_file if ! [ -f $dns_file ]; then showMessageBox "Empty File" "The file is empty, returning..." return fi updateHomeServerDNS $dns_file rm -f $dns_file } function removeRevertDNS() { curDomain=$1 curExtPrefix=$2 # First, try to downgrade to direct connection, i.e. we are hosting on their network. new_dns=$(sqlite3 $HSHQ_DB "select ID,PeerDomainExtPrefix,IPAddress from hsvpn_dns where PeerDomain='$curDomain' and HostDomain='$curDomain';") if [ -z "$new_dns" ]; then # No direct connection, try to find any other possible connections. new_dns=$(sqlite3 $HSHQ_DB "select ID,PeerDomainExtPrefix,IPAddress from hsvpn_dns where PeerDomain='$curDomain' order by DateAdded asc limit 1;") fi if [ -z "$new_dns" ]; then # No entries, delete current DNS entry deleteDomainAndWildcardAdguardHS "$curDomain" deleteDomainAdguardHS "*.$curExtPrefix.$curDomain" removeDomainAndWildcardIgnoreQuerylogAndStatsHS "$curDomain" # Eventually delete this next line, but we'll leave it in for awhile deleteSvcUptimeKuma "https://$SUB_HSHQSTATUS.${curDomain}" true else # Found an alternate, replace it curID=$(echo "$new_dns" | cut -d "|" -f1) ext_prefix=$(echo "$new_dns" | cut -d "|" -f2) ip_addr=$(echo "$new_dns" | cut -d "|" -f3) addDomainAndWildcardAdguardHS "$curDomain" "$ip_addr" addDomainAdguardHS "*.$curExtPrefix.$curDomain" "A" sudo sqlite3 $HSHQ_DB "update hsvpn_dns set IsActive=1 where ID=$curID;" fi } function updateHomeServerDNS() { dns_file=$1 removeSpecialChars "$dns_file" OLDIFS=$IFS IFS=$(echo -en "\n\b") lines=$(cat $dns_file) curdt=$(getCurrentDate) sudo sqlite3 $HSHQ_DB "drop table if exists dns_tmp;" sudo sqlite3 $HSHQ_DB "create table dns_tmp(HostDomain text not null,PeerDomain text not null,PRIMARY KEY (HostDomain, PeerDomain));" primaryDomain=$(sqlite3 $HSHQ_DB "select DomainName from hsvpn_connections join connections on hsvpn_connections.ID = connections.ID where ConnectionType='homeserver_vpn' and NetworkType='primary';") is_error=false error_text="" for curLine in $lines do if [ -z "$curLine" ]; then continue fi curHSName=$(echo "$curLine" | cut -d "|" -f1) curHostDomain=$(echo "$curLine" | cut -d "|" -f2) curPeerDomain=$(echo "$curLine" | cut -d "|" -f3) curExtPrefix=$(echo "$curLine" | cut -d "|" -f4) curIP=$(echo "$curLine" | cut -d "|" -f5) if [ "$curPeerDomain" = "$HOMESERVER_DOMAIN" ]; then continue fi if [ -z "$curHSName" ]; then is_error=true error_text="The HomeServer name is empty." break fi if [ -z "$curHostDomain" ]; then is_error=true error_text="The host domain name is empty." break fi if [ -z "$curPeerDomain" ]; then is_error=true error_text="The peer domain name is empty." break fi if [ -z "$curExtPrefix" ]; then is_error=true error_text="The ext prefix is empty." break fi if [ -z "$curIP" ]; then is_error=true error_text="The ip address is empty." break fi check_ip=$(checkHomeServerDNSIPAddressInVPN $curHostDomain $curPeerDomain $curIP) if ! [ "$check_ip" = "true" ]; then is_error=true error_text="$check_ip" break fi if [ "$(checkUpdateHomeServerDNSVars)" = "true" ]; then is_dns_added=$(sqlite3 $HSHQ_DB "select IsActive from hsvpn_dns where HostDomain='$curHostDomain' and PeerDomain='$curPeerDomain';") sudo sqlite3 $HSHQ_DB "insert into dns_tmp(HostDomain,PeerDomain) values('$curHostDomain','$curPeerDomain');" if [ "$curHostDomain" = "$HOMESERVER_DOMAIN" ]; then continue fi if ! [ -z "$is_dns_added" ]; then # Check if IP has changed and update accordingly. db_ip=$(sqlite3 $HSHQ_DB "select IPAddress from hsvpn_dns where HostDomain='$curHostDomain' and PeerDomain='$curPeerDomain';") if ! [ "$db_ip" = "$curIP" ]; then echo "IP has changed, Old: $db_ip, New: $curIP" sudo sqlite3 $HSHQ_DB "update hsvpn_dns set IPAddress='$curIP' where HostDomain='$curHostDomain' and PeerDomain='$curPeerDomain';" if [ $is_dns_added = 1 ]; then addDomainAndWildcardAdguardHS "$curPeerDomain" "$curIP" fi fi continue fi is_active=0 is_dns_exist=$(sqlite3 $HSHQ_DB "select PeerDomain from hsvpn_dns where PeerDomain='$curPeerDomain';") is_host_my_network=$(sqlite3 $HSHQ_DB "select PeerDomain from hsvpn_dns where HostDomain='$HOMESERVER_DOMAIN' and PeerDomain='$curPeerDomain';") if [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ] && [ "$primaryDomain" = "$curHostDomain" ]; then is_active=1 if ! [ -z "$is_dns_exist" ]; then # DNS entry exists, but need to upgrade it since it is on our primary network. sudo sqlite3 $HSHQ_DB "update hsvpn_dns set IsActive=0 where PeerDomain='$curPeerDomain';" fi addDomainAndWildcardAdguardHS "$curPeerDomain" "$curIP" addDomainAdguardHS "*.$curExtPrefix.$curPeerDomain" "A" insertEnableSvcHeimdall heimdall "$curHSName" homeservers "https://$SUB_HSHQHOME.$curPeerDomain" "hs2.png" true "$(getHeimdallOrderFromSub $SUB_HSHQHOME homeservers)" else if [ -z "$is_dns_exist" ]; then # No entry exists, add a new one is_active=1 addDomainAndWildcardAdguardHS "$curPeerDomain" "$curIP" addDomainAdguardHS "*.$curExtPrefix.$curPeerDomain" "A" elif [ -z "$is_host_my_network" ] && [ "$curHostDomain" = "$curPeerDomain" ]; then # DNS entry exists, but need to upgrade it since it is a direct connection. is_active=1 addDomainAndWildcardAdguardHS "$curPeerDomain" "$curIP" addDomainAdguardHS "*.$curExtPrefix.$curPeerDomain" "A" sudo sqlite3 $HSHQ_DB "update hsvpn_dns set IsActive=0 where PeerDomain='$curPeerDomain';" fi fi sudo sqlite3 $HSHQ_DB "insert into hsvpn_dns(HostDomain,PeerDomain,PeerDomainExtPrefix,IPAddress,DateAdded,IsActive) values('$curHostDomain','$curPeerDomain','$curExtPrefix','$curIP','$curdt',$is_active);" fi done IFS=$OLDIFS if [ "$is_error" = "true" ]; then email_subj="ERROR - HomeServer DNS Update" email_body="There was an error detected with the most recent HomeServer DNS Update: \n\n" email_body=$email_body"$error_text" sendEmail -s "$email_subj" -b "$email_body" echo "Error detected, see admin email ($EMAIL_ADMIN_EMAIL_ADDRESS) for details, returning..." sudo sqlite3 $HSHQ_DB "drop table dns_tmp;" return fi host_list=($(sqlite3 $HSHQ_DB "select HostDomain from dns_tmp group by HostDomain;")) for cur_host in "${host_list[@]}" do if [ "$curHostDomain" = "$HOMESERVER_DOMAIN" ]; then # Don't process our own records (protect user error) continue fi # Check if any DNS entries need to be deleted/reverted del_list=($(sqlite3 $HSHQ_DB "select hsvpn_dns.PeerDomain,hsvpn_dns.PeerDomainExtPrefix from hsvpn_dns left join dns_tmp on hsvpn_dns.HostDomain = dns_tmp.HostDomain and hsvpn_dns.PeerDomain = dns_tmp.PeerDomain where hsvpn_dns.HostDomain = '$cur_host' and dns_tmp.HostDomain is NULL;")) for curdns in "${del_list[@]}" do curPeerDomain=$(echo "$curdns" | cut -d "|" -f1) curExtPrefix=$(echo "$curdns" | cut -d "|" -f2) is_dns_active=$(sqlite3 $HSHQ_DB "select IsActive from hsvpn_dns where HostDomain='$cur_host' and PeerDomain='$curPeerDomain';") sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from hsvpn_dns where HostDomain='$cur_host' and PeerDomain='$curPeerDomain';" if [ $is_dns_active = 1 ]; then removeRevertDNS $curPeerDomain $curExtPrefix fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ] && [ "$primaryDomain" = "$cur_host" ]; then deleteSvcHeimdall homeservers "https://$SUB_HSHQHOME.$curPeerDomain" false fi done done sudo sqlite3 $HSHQ_DB "drop table dns_tmp;" } function checkHomeServerDNSIPAddressInVPN() { host_domain=$1 peer_domain=$2 ip_addr=$3 host_net=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections join hsvpn_connections on connections.ID = hsvpn_connections.ID where connections.NetworkType='other' and hsvpn_connections.DomainName='$host_domain';") is_in_subnet=$(isIPInSubnet $ip_addr $host_net) if [ "$is_in_subnet" = "true" ]; then echo "true" else echo "Host Domain: $host_domain, Peer domain: $peer_domain, IP address ($ip_addr) is not in correct subnet ($host_net)." fi } function syncAdguardDNSFromDB() { dns_list=($(sqlite3 $HSHQ_DB "select PeerDomain,PeerDomainExtPrefix,IPAddress from hsvpn_dns where IsActive=1;")) for curdns in "${dns_list[@]}" do curPeerDomain=$(echo "$curdns" | cut -d "|" -f1) curExtPrefix=$(echo "$curdns" | cut -d "|" -f2) curIPAddress=$(echo "$curdns" | cut -d "|" -f3) if ! [ "$curPeerDomain" = "$HOMESERVER_DOMAIN" ]; then addDomainAndWildcardAdguardHS "$curPeerDomain" "$curIPAddress" addDomainAdguardHS "*.$curExtPrefix.$curPeerDomain" "A" fi done } function checkUpdateHomeServerDNSVars() { # TODO echo "true" } # Adguard Utils function getAdguardCredentialsHS() { echo -n $ADGUARD_ADMIN_USERNAME:$ADGUARD_ADMIN_PASSWORD | base64 } function getAdguardCredentialsRS() { echo -n $RELAYSERVER_ADGUARD_ADMIN_USERNAME:$RELAYSERVER_ADGUARD_ADMIN_PASSWORD | base64 } function addDomainAndWildcardAdguardHS() { deleteDomainAndWildcardAdguardHS "$1" addDomainAdguardHS "$1" "$2" addDomainAdguardHS "*.$1" "$2" } function addDomainAdguardHS() { set +e add_domain="$1" add_ip_addr="$2" basic_auth="$(getAdguardCredentialsHS)" deleteDomainAdguardHS $add_domain "$basic_auth" dom_json=$(jq -n --arg add_domain $add_domain --arg add_ip_addr $add_ip_addr '{domain: $add_domain, answer: $add_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/rewrite/add } function addDomainAndWildcardAdguardNoReplaceHS() { addDomainAdguardNoReplaceHS "$1" "$2" addDomainAdguardNoReplaceHS "*.$1" "$2" } function addDomainAdguardNoReplaceHS() { set +e add_domain="$1" add_ip_addr="$2" basic_auth="$(getAdguardCredentialsHS)" curl -s -H "Authorization: Basic $basic_auth" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/rewrite/list | jq -r --arg add_domain $add_domain '.[] | select(.domain==$add_domain) | .domain' | grep $add_domain > /dev/null 2>&1 if [ $? -ne 0 ]; then dom_json=$(jq -n --arg add_domain $add_domain --arg add_ip_addr $add_ip_addr '{domain: $add_domain, answer: $add_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/rewrite/add fi } function deleteDomainAndWildcardAdguardHS() { deleteDomainAdguardHS "$1" deleteDomainAdguardHS "*.$1" } function deleteDomainAdguardHS() { set +e del_domain="$1" basic_auth="$(getAdguardCredentialsHS)" curl -s -H "Authorization: Basic $basic_auth" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/rewrite/list | jq -r --arg del_domain $del_domain '.[] | select(.domain==$del_domain) | .domain' | grep "$del_domain" >/dev/null if [ $? -eq 0 ]; then # Exists, need to determine IP address to delete del_ip_addr=$(curl -s -H "Authorization: Basic $basic_auth" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/rewrite/list | jq -r --arg del_domain $del_domain '.[] | select(.domain==$del_domain) | .answer') dom_json=$(jq -n --arg del_domain $del_domain --arg del_ip_addr $del_ip_addr '{domain: $del_domain, answer: $del_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/rewrite/delete fi } function deleteDomainByIPAndWildcardAdguardHS() { deleteDomainByIPAdguardHS "$1" "$2" deleteDomainByIPAdguardHS "*.$1" "$2" } function deleteDomainByIPAdguardHS() { set +e del_domain="$1" del_ip_addr="$2" basic_auth="$(getAdguardCredentialsHS)" dom_json=$(jq -n --arg del_domain $del_domain --arg del_ip_addr $del_ip_addr '{domain: $del_domain, answer: $del_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/rewrite/delete } function clearQueryLogAndStatsAdguardHS() { basic_auth="$(getAdguardCredentialsHS)" http --verify=no POST https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/querylog_clear "Authorization: Basic $basic_auth" >/dev/null http --verify=no POST https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/stats_reset "Authorization: Basic $basic_auth" >/dev/null } function addDomainAndWildcardIgnoreQuerylogAndStatsHS() { adawiqs_curE=${-//[^e]/} set +e add_domain="$1" basic_auth="$(getAdguardCredentialsHS)" # Query log check_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/querylog/config "Authorization: Basic $basic_auth" | jq '.ignored') echo "$check_conf" | grep "$add_domain" > /dev/null 2>&1 if [ $? -ne 0 ]; then new_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/querylog/config "Authorization: Basic $basic_auth" | jq --arg add_domain "$add_domain" '.ignored += [$add_domain]' | jq --arg add_domain "*.$add_domain" '.ignored += [$add_domain]') curl -s -X PUT -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$new_conf" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/querylog/config/update fi # Stats check_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/stats/config "Authorization: Basic $basic_auth" | jq '.ignored') echo "$check_conf" | grep "$add_domain" > /dev/null 2>&1 if [ $? -ne 0 ]; then new_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/stats/config "Authorization: Basic $basic_auth" | jq --arg add_domain "$add_domain" '.ignored += [$add_domain]' | jq --arg add_domain "*.$add_domain" '.ignored += [$add_domain]') curl -s -X PUT -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$new_conf" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/stats/config/update fi if ! [ -z "$adawiqs_curE" ]; then set -e fi } function removeDomainAndWildcardIgnoreQuerylogAndStatsHS() { rdawiqs_curE=${-//[^e]/} set +e rem_domain="$1" basic_auth="$(getAdguardCredentialsHS)" # Query log check_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/querylog/config "Authorization: Basic $basic_auth" | jq '.ignored') echo "$check_conf" | grep "$rem_domain" > /dev/null 2>&1 if [ $? -eq 0 ]; then new_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/querylog/config "Authorization: Basic $basic_auth" | jq --arg rem_domain "$rem_domain" '.ignored -= [$rem_domain]' | jq --arg rem_domain "*.$rem_domain" '.ignored -= [$rem_domain]') curl -s -X PUT -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$new_conf" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/querylog/config/update fi # Stats check_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/stats/config "Authorization: Basic $basic_auth" | jq '.ignored') echo "$check_conf" | grep "$rem_domain" > /dev/null 2>&1 if [ $? -eq 0 ]; then new_conf=$(http --verify=no GET https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/stats/config "Authorization: Basic $basic_auth" | jq --arg rem_domain "$rem_domain" '.ignored -= [$rem_domain]' | jq --arg rem_domain "*.$rem_domain" '.ignored -= [$rem_domain]') curl -s -X PUT -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$new_conf" -k https://127.0.0.1:$ADGUARD_LOCALHOST_PORT/control/stats/config/update fi if ! [ -z "$rdawiqs_curE" ]; then set -e fi } function addDomainAndWildcardAdguardRS() { deleteDomainAndWildcardAdguardRS "$1" addDomainAdguardRS "$1" "$2" addDomainAdguardRS "*.$1" "$2" } function addDomainAdguardRS() { set +e add_domain=$1 add_ip_addr=$2 basic_auth="$(getAdguardCredentialsRS)" deleteDomainAdguardRS "$add_domain" "$basic_auth" dom_json=$(jq -n --arg add_domain $add_domain --arg add_ip_addr $add_ip_addr '{domain: $add_domain, answer: $add_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/rewrite/add } function addDomainAndWildcardAdguardNoReplaceRS() { addDomainAdguardNoReplaceRS "$1" "$2" addDomainAdguardNoReplaceRS "*.$1" "$2" } function addDomainAdguardNoReplaceRS() { set +e add_domain="$1" add_ip_addr="$2" basic_auth="$(getAdguardCredentialsRS)" curl -s -H "Authorization: Basic $basic_auth" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/rewrite/list | jq -r --arg add_domain $add_domain '.[] | select(.domain==$add_domain) | .domain' | grep $add_domain > /dev/null 2>&1 if [ $? -ne 0 ]; then dom_json=$(jq -n --arg add_domain $add_domain --arg add_ip_addr $add_ip_addr '{domain: $add_domain, answer: $add_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/rewrite/add fi } function deleteDomainAndWildcardAdguardRS() { deleteDomainAdguardRS "$1" deleteDomainAdguardRS "*.$1" } function deleteDomainAdguardRS() { set +e del_domain="$1" basic_auth="$(getAdguardCredentialsRS)" curl -s -H "Authorization: Basic $basic_auth" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/rewrite/list | jq -r --arg del_domain $del_domain '.[] | select(.domain==$del_domain) | .domain' | grep "$del_domain" >/dev/null if [ $? -eq 0 ]; then # Exists, need to determine IP address to delete del_ip_addr=$(curl -s -H "Authorization: Basic $basic_auth" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/rewrite/list | jq -r --arg del_domain $del_domain '.[] | select(.domain==$del_domain) | .answer') dom_json=$(jq -n --arg del_domain $del_domain --arg del_ip_addr $del_ip_addr '{domain: $del_domain, answer: $del_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/rewrite/delete fi } function deleteDomainByIPAndWildcardAdguardRS() { deleteDomainByIPAdguardRS "$1" "$2" deleteDomainByIPAdguardRS "*.$1" "$2" } function deleteDomainByIPAdguardRS() { set +e del_domain="$1" del_ip_addr="$2" basic_auth="$(getAdguardCredentialsRS)" dom_json=$(jq -n --arg del_domain $del_domain --arg del_ip_addr $del_ip_addr '{domain: $del_domain, answer: $del_ip_addr}') curl -s -H "Authorization: Basic $basic_auth" -H 'Content-Type: application/json' -d "$dom_json" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/rewrite/delete } function clearQueryLogAndStatsAdguardRS() { basic_auth="$(getAdguardCredentialsRS)" http --verify=no POST https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/querylog_clear "Authorization: Basic $basic_auth" >/dev/null http --verify=no POST https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/control/stats_reset "Authorization: Basic $basic_auth" >/dev/null } # Port Forwarding function setupPortForwardingDB() { if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then return fi isPFTable=$(sqlite3 $HSHQ_DB "select name from sqlite_master where type='table' and name='portforwarding';") if [ -z "$isPFTable" ]; then curdt=$(getCurrentDate) sudo sqlite3 $HSHQ_DB "create table portforwarding(ID integer not null primary key autoincrement, PFType text, Name text, ExtStart integer, ExtEnd integer, IntStart integer, IntEnd integer, Protocol text, InternalHost text, IPAddress text, LastUpdated datetime);" # Email SMTP (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-EmailSMTP', 25, 25, 0, 0, 'both','','','$curdt');" # DNS Server (need both) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-DNS', 53, 53, 0, 0, 'both','','','$curdt');" # HTTP (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-HTTP', 80, 80, 0, 0, 'both','','','$curdt');" # HTTPS (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-HTTPS', 443, 443, 0, 0, 'both','','','$curdt');" # Email Submission (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-EmailSubmit', $MAILU_PORT_5, $MAILU_PORT_5, 0, 0, 'both','','','$curdt');" # WireGuard Portal (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-WGPortal', 8323, 8323, 0, 0, 'both','','','$curdt');" # Syncthing Web (do both protocols, even though only need TCP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-SyncthingWeb', $SYNCTHING_LOCAL_WEB_PORT, $SYNCTHING_LOCAL_WEB_PORT, 0, 0, 'both','','','$curdt');" # Syncthing Sync (need both) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-SyncthingSync', $SYNCTHING_SYNC_PORT, $SYNCTHING_SYNC_PORT, 0, 0, 'both','','','$curdt');" # Syncthing Discovery (do both protocols, even though only need UDP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-SyncthingDiscovery', $SYNCTHING_DISC_PORT, $SYNCTHING_DISC_PORT, 0, 0, 'both','','','$curdt');" # WireGuard (do both protocols, even though only need UDP) sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-WireGuard', $RELAYSERVER_WG_PORT, $RELAYSERVER_WG_PORT, 0, 0, 'both','','','$curdt');" fi } function version91UploadPortForwardingScripts() { # Upload add/remove scripts to RelayServer if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\n\nThe RelayServer requires an update which requires root privileges.\nYou will be prompted for you sudo password on the RelayServer.\n" notifyRSLogin rm -f $HOME/addPortForward.sh rm -f $HOME/removePortForward.sh tee $HOME/addPortForward.sh >/dev/null </dev/null < /dev/null 2>&1 || iptables -A PREROUTING -t nat -i \$DEFAULT_IFACE -p tcp --dport \$ExtStart:\$ExtEnd -j DNAT --to-destination \$IPAddress:\$IntStart-\$IntEnd/\$ExtStart iptables -C FORWARD -i \$DEFAULT_IFACE -o \$RELAYSERVER_WG_INTERFACE_NAME -p tcp -d \$IPAddress --dport \$IntStart:\$IntEnd --syn -m conntrack --ctstate NEW -j ACCEPT > /dev/null 2>&1 || iptables -A FORWARD -i \$DEFAULT_IFACE -o \$RELAYSERVER_WG_INTERFACE_NAME -p tcp -d \$IPAddress --dport \$IntStart:\$IntEnd --syn -m conntrack --ctstate NEW -j ACCEPT iptables -C FORWARD -i \$RELAYSERVER_WG_INTERFACE_NAME -o \$DEFAULT_IFACE -p tcp -s \$IPAddress --sport \$IntStart:\$IntEnd -m state --state ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 || iptables -A FORWARD -i \$RELAYSERVER_WG_INTERFACE_NAME -o \$DEFAULT_IFACE -p tcp -s \$IPAddress --sport \$IntStart:\$IntEnd -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -C POSTROUTING -t nat -o \$RELAYSERVER_WG_INTERFACE_NAME -p tcp --dport \$IntStart:\$IntEnd -d \$IPAddress -j SNAT --to-source \$RELAYSERVER_WG_SV_IP:\$ExtStart-\$ExtEnd/\$IntStart > /dev/null 2>&1 || iptables -A POSTROUTING -t nat -o \$RELAYSERVER_WG_INTERFACE_NAME -p tcp --dport \$IntStart:\$IntEnd -d \$IPAddress -j SNAT --to-source \$RELAYSERVER_WG_SV_IP:\$ExtStart-\$ExtEnd/\$IntStart } function addUDP() { iptables -C PREROUTING -t nat -i \$DEFAULT_IFACE -p udp --dport \$ExtStart:\$ExtEnd -j DNAT --to-destination \$IPAddress:\$IntStart-\$IntEnd/\$ExtStart > /dev/null 2>&1 || iptables -A PREROUTING -t nat -i \$DEFAULT_IFACE -p udp --dport \$ExtStart:\$ExtEnd -j DNAT --to-destination \$IPAddress:\$IntStart-\$IntEnd/\$ExtStart iptables -C FORWARD -i \$DEFAULT_IFACE -o \$RELAYSERVER_WG_INTERFACE_NAME -p udp -d \$IPAddress --dport \$IntStart:\$IntEnd -j ACCEPT > /dev/null 2>&1 || iptables -A FORWARD -i \$DEFAULT_IFACE -o \$RELAYSERVER_WG_INTERFACE_NAME -p udp -d \$IPAddress --dport \$IntStart:\$IntEnd -j ACCEPT iptables -C POSTROUTING -t nat -o \$RELAYSERVER_WG_INTERFACE_NAME -p udp --dport \$IntStart:\$IntEnd -d \$IPAddress -j SNAT --to-source \$RELAYSERVER_WG_SV_IP:\$ExtStart-\$ExtEnd/\$IntStart > /dev/null 2>&1 || iptables -A POSTROUTING -t nat -o \$RELAYSERVER_WG_INTERFACE_NAME -p udp --dport \$IntStart:\$IntEnd -d \$IPAddress -j SNAT --to-source \$RELAYSERVER_WG_SV_IP:\$ExtStart-\$ExtEnd/\$IntStart } main "\\\$@" EOFSC chmod 744 \$RELAYSERVER_PF_ADD_DIR/15-pfAdd-\${ID}.sh \$RELAYSERVER_PF_ADD_DIR/15-pfAdd-\${ID}.sh tee \$RELAYSERVER_PF_REMOVE_DIR/pfRem-\${ID}.sh >/dev/null < /dev/null 2>&1 iptables -D FORWARD -i \$DEFAULT_IFACE -o \$RELAYSERVER_WG_INTERFACE_NAME -p tcp -d \$IPAddress --dport \$IntStart:\$IntEnd --syn -m conntrack --ctstate NEW -j ACCEPT > /dev/null 2>&1 iptables -D FORWARD -i \$RELAYSERVER_WG_INTERFACE_NAME -o \$DEFAULT_IFACE -p tcp -s \$IPAddress --sport \$IntStart:\$IntEnd -m state --state ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 iptables -D POSTROUTING -t nat -o \$RELAYSERVER_WG_INTERFACE_NAME -p tcp --dport \$IntStart:\$IntEnd -d \$IPAddress -j SNAT --to-source \$RELAYSERVER_WG_SV_IP:\$ExtStart-\$ExtEnd/\$IntStart > /dev/null 2>&1 } function remUDP() { iptables -D PREROUTING -t nat -i \$DEFAULT_IFACE -p udp --dport \$ExtStart:\$ExtEnd -j DNAT --to-destination \$IPAddress:\$IntStart-\$IntEnd/\$ExtStart > /dev/null 2>&1 iptables -D FORWARD -i \$DEFAULT_IFACE -o \$RELAYSERVER_WG_INTERFACE_NAME -p udp -d \$IPAddress --dport \$IntStart:\$IntEnd -j ACCEPT > /dev/null 2>&1 iptables -D POSTROUTING -t nat -o \$RELAYSERVER_WG_INTERFACE_NAME -p udp --dport \$IntStart:\$IntEnd -d \$IPAddress -j SNAT --to-source \$RELAYSERVER_WG_SV_IP:\$ExtStart-\$ExtEnd/\$IntStart > /dev/null 2>&1 } main "\\\$@" EOFSC chmod 744 \$RELAYSERVER_PF_REMOVE_DIR/pfRem-\${ID}.sh } main "\$@" EOFCD chmod 500 $HOME/addPortForward.sh tee $HOME/removePortForward.sh >/dev/null < /dev/null 2>&1 if [ $? -eq 0 ]; then tryFind=$(findCertByKeyword firefox) if [ -z "$tryFind" ] || ! [ "$tryFind" = "true" ]; then initDesktopApp firefox true findCertByKeyword fi fi which thunderbird > /dev/null 2>&1 if [ $? -eq 0 ]; then tryFind=$(findCertByKeyword thunderbird) if [ -z "$tryFind" ] || ! [ "$tryFind" = "true" ]; then initDesktopApp thunderbird true findCertByKeyword fi fi # This is a little bit kludgy, but it covers the case when # Firefox is installed via snap vs apt, i.e. the profile # directory is not the same. for certDB in $(find ~ -name "cert9.db" 2> /dev/null) do certDir=$(dirname $certDB) echo "Adding certificate (${CERTS_ROOT_CA_NAME}.crt) to app..." certutil -A -n "${HOMESERVER_NAME} Root CA" -t "CT,C,C" -i "/usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt" -d sql:${certDir} done which brave-browser > /dev/null 2>&1 if [ $? -eq 0 ]; then tryFind=$(getBraveCertDB) if [ -z "$tryFind" ] || ! [ -f "$tryFind" ]; then initDesktopApp brave-browser true getBraveCertDB fi fi if [ -d ~/.pki/nssdb ]; then echo "Adding certificate (${CERTS_ROOT_CA_NAME}.crt) to Chromium-based browser(s) pki..." certutil -A -n "${HOMESERVER_NAME} Root CA" -t "CT,C,C" -i "/usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt" -d sql:~/.pki/nssdb 2> /dev/null fi } function tryDeleteRootCAFirefox() { OLDIFS=$IFS IFS=$(echo -en "\n\b") for certDB in $(find ~ -path $HSHQ_BASE_DIR -prune -o -name "cert9.db" -print 2> /dev/null) do certDir=$(dirname $certDB) certList=($(certutil -L -d sql:${certDir} | grep -o ".*Root CA")) for curCert in "${certList[@]}" do certutil -D -n "$curCert" -d sql:${certDir} done done IFS=$OLDIFS } function findCertByKeyword() { keyw="$1" findres=$(find ~ -name "cert9.db" 2> /dev/null) echo "$findres" | grep "$keyw" > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "true" else echo "false" fi } function getBraveCertDB() { find ~/.pki -type d -name nssdb 2> /dev/null } function initDesktopApp() { appName="$1" isRunHeadless="$2" checkFunction="$3" # 1. Start app on desktop # 2. Wait for checkFunction to return a result # 3. Kill app export DISPLAY=:0 if [ "$isRunHeadless" = "true" ]; then nohup $appName --headless > /dev/null 2>&1 & > /dev/null 2>&1 else nohup $appName > /dev/null 2>&1 & > /dev/null 2>&1 fi sleep 5 curCount=0 maxCount=30 while [ $curCount -lt $maxCount ] do tryFind=$($checkFunction "$appName") if ! [ -z "$tryFind" ] && [ "$tryFind" = "true" ]; then break fi sleep 1 ((curCount++)) done pkill $appName > /dev/null 2>&1 } function configureFirefox() { # The settings in the file below are taken directly from # https://github.com/pyllyukko/user.js. It is an active, # well-documented project that seeks to provide # security-hardened settings for the Firefox browser. # These settings are very aggressive and can detract # from a normal user experience. However, keep in mind # that this is a server, not a client device. It should # not be used in a traditional everyday environment. # If you want to remove these settings, then the # easiest way is to close Firefox, remove the profile # directory (likely ~/.mozilla/firefox, then restart # Firefox. This will remove all related Firefox data, # such as bookmarks, etc. So backup everything accordingly. cat < $HOME/user.js // // A few more added user_pref("extensions.formautofill.addresses.enabled", false); user_pref("extensions.formautofill.addresses.capture.enabled", false); user_pref("extensions.formautofill.addresses.ignoreAutocompleteOff", false); user_pref("extensions.formautofill.creditCards.available", false); user_pref("extensions.formautofill.creditCards.enabled", false); user_pref("browser.urlbar.suggest.topsites", false); user_pref("identity.fxaccounts.enabled", false); user_pref("browser.startup.couldRestoreSession.count", 1); user_pref("browser.startup.page", 3); user_pref("browser.urlbar.placeholderName", "DuckDuckGo"); user_pref("browser.urlbar.placeholderName.private", "DuckDuckGo"); user_pref("dom.media.webcodecs.enabled", true); // Always dark mode:) user_pref("layout.css.prefers-color-scheme.content-override", 0); user_pref("browser.in-content.dark-mode", true); user_pref("ui.systemUsesDarkTheme", 1); /****************************************************************************** * user.js * * https://github.com/pyllyukko/user.js * ******************************************************************************/ /****************************************************************************** * SECTION: HTML5 / APIs / DOM * ******************************************************************************/ // PREF: Disable Service Workers // https://developer.mozilla.org/en-US/docs/Web/API/Worker // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker_API // https://wiki.mozilla.org/Firefox/Push_Notifications#Service_Workers // NOTICE: Disabling ServiceWorkers breaks functionality on some sites (Google Street View...) // NOTICE: Disabling ServiceWorkers breaks Firefox Sync // Unknown security implications // CVE-2016-5259, CVE-2016-2812, CVE-2016-1949, CVE-2016-5287 (fixed) user_pref("dom.serviceWorkers.enabled", false); // PREF: Disable web notifications // https://support.mozilla.org/en-US/questions/1140439 user_pref("dom.webnotifications.enabled", false); // PREF: Disable DOM timing API // https://wiki.mozilla.org/Security/Reviews/Firefox/NavigationTimingAPI // https://www.w3.org/TR/navigation-timing/#privacy // NOTICE: Disabling DOM timing API breaks item pages in AliExpress (https://github.com/pyllyukko/user.js/issues/561) user_pref("dom.enable_performance", false); // PREF: Disable resource timing API // https://www.w3.org/TR/resource-timing/#privacy-security // NOTICE: Disabling resource timing API breaks some DDoS protection pages (Cloudflare) user_pref("dom.enable_resource_timing", false); // PREF: Make sure the User Timing API does not provide a new high resolution timestamp // https://trac.torproject.org/projects/tor/ticket/16336 // https://www.w3.org/TR/2013/REC-user-timing-20131212/#privacy-security user_pref("dom.enable_user_timing", false); // PREF: Disable Web Audio API // https://bugzilla.mozilla.org/show_bug.cgi?id=1288359 // NOTICE: Web Audio API is required for Unity web player/games // Set this to true for Jitsi user_pref("dom.webaudio.enabled", true); // PREF: Disable Location-Aware Browsing (geolocation) // https://www.mozilla.org/en-US/firefox/geolocation/ user_pref("geo.enabled", false); // PREF: When geolocation is enabled, use Mozilla geolocation service instead of Google // https://bugzilla.mozilla.org/show_bug.cgi?id=689252 user_pref("geo.wifi.uri", "https://location.services.mozilla.com/v1/geolocate?key=%MOZILLA_API_KEY%"); // PREF: When geolocation is enabled, don't log geolocation requests to the console user_pref("geo.wifi.logging.enabled", false); // PREF: Disable raw TCP socket support (mozTCPSocket) // https://trac.torproject.org/projects/tor/ticket/18863 // https://www.mozilla.org/en-US/security/advisories/mfsa2015-97/ // https://developer.mozilla.org/docs/Mozilla/B2G_OS/API/TCPSocket user_pref("dom.mozTCPSocket.enabled", false); // PREF: Disable DOM storage (disabled) // http://kb.mozillazine.org/Dom.storage.enabled // https://html.spec.whatwg.org/multipage/webstorage.html // NOTICE-DISABLED: Disabling DOM storage is known to cause TypeError: localStorage is null errors //user_pref("dom.storage.enabled", false); // PREF: Disable leaking network/browser connection information via Javascript // Network Information API provides general information about the system's connection type (WiFi, cellular, etc.) // https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API // https://wicg.github.io/netinfo/#privacy-considerations // https://bugzilla.mozilla.org/show_bug.cgi?id=960426 user_pref("dom.netinfo.enabled", false); // PREF: Disable network API (Firefox < 32) // https://developer.mozilla.org/en-US/docs/Web/API/Connection/onchange // https://www.torproject.org/projects/torbrowser/design/#fingerprinting-defenses user_pref("dom.network.enabled", false); // PREF: Disable WebRTC entirely to prevent leaking internal IP addresses (Firefox < 42) // NOTICE: Disabling WebRTC breaks peer-to-peer file sharing tools (reep.io ...) // Set this to true for FileDrop user_pref("media.peerconnection.enabled", true); // PREF: Don't reveal your internal IP when WebRTC is enabled (Firefox >= 42) // https://wiki.mozilla.org/Media/WebRTC/Privacy // https://github.com/beefproject/beef/wiki/Module%3A-Get-Internal-IP-WebRTC user_pref("media.peerconnection.ice.default_address_only", true); // Firefox 42-51 user_pref("media.peerconnection.ice.no_host", true); // Firefox >= 52 // PREF: Disable WebRTC getUserMedia, screen sharing, audio capture, video capture // https://wiki.mozilla.org/Media/getUserMedia // https://blog.mozilla.org/futurereleases/2013/01/12/capture-local-camera-and-microphone-streams-with-getusermedia-now-enabled-in-firefox/ // https://developer.mozilla.org/en-US/docs/Web/API/Navigator user_pref("media.navigator.enabled", false); user_pref("media.navigator.video.enabled", false); user_pref("media.getusermedia.screensharing.enabled", false); user_pref("media.getusermedia.audiocapture.enabled", false); // PREF: Disable battery API (Firefox < 52) // https://developer.mozilla.org/en-US/docs/Web/API/BatteryManager // https://bugzilla.mozilla.org/show_bug.cgi?id=1313580 user_pref("dom.battery.enabled", false); // PREF: Disable telephony API // https://wiki.mozilla.org/WebAPI/Security/WebTelephony user_pref("dom.telephony.enabled", false); // PREF: Disable "beacon" asynchronous HTTP transfers (used for analytics) // https://developer.mozilla.org/en-US/docs/Web/API/navigator.sendBeacon user_pref("beacon.enabled", false); // PREF: Disable clipboard event detection (onCut/onCopy/onPaste) via Javascript // https://web.archive.org/web/20210416195937/https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Preference_reference/dom.event.clipboardevents.enabled // https://github.com/pyllyukko/user.js/issues/287 // NOTICE: Disabling clipboard events breaks Ctrl+C/X/V copy/cut/paste functionaility in JS-based web applications (Google Docs...) user_pref("dom.event.clipboardevents.enabled", false); // PREF: Disable "copy to clipboard" functionality via Javascript (Firefox >= 41) // https://hg.mozilla.org/mozilla-central/rev/2f9f8ea4b9c3 // https://github.com/pyllyukko/user.js/issues/287 // NOTICE: Disabling clipboard operations will break legitimate JS-based "copy to clipboard" functionality user_pref("dom.allow_cut_copy", true); // PREF: Disable speech recognition // https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html // https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition // https://wiki.mozilla.org/HTML5_Speech_API user_pref("media.webspeech.recognition.enable", false); // PREF: Disable speech synthesis // https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis user_pref("media.webspeech.synth.enabled", false); // PREF: Disable sensor API // https://wiki.mozilla.org/Sensor_API user_pref("device.sensors.enabled", false); // PREF: Disable pinging URIs specified in HTML ping= attributes // http://kb.mozillazine.org/Browser.send_pings user_pref("browser.send_pings", false); // PREF: When browser pings are enabled, only allow pinging the same host as the origin page // http://kb.mozillazine.org/Browser.send_pings.require_same_host user_pref("browser.send_pings.require_same_host", true); // PREF: Disable IndexedDB (disabled) // https://developer.mozilla.org/en-US/docs/IndexedDB // https://en.wikipedia.org/wiki/Indexed_Database_API // https://wiki.mozilla.org/Security/Reviews/Firefox4/IndexedDB_Security_Review // http://forums.mozillazine.org/viewtopic.php?p=13842047 // https://github.com/pyllyukko/user.js/issues/8 // NOTICE-DISABLED: IndexedDB could be used for tracking purposes, but is required for some add-ons to work (notably uBlock), so is left enabled //user_pref("dom.indexedDB.enabled", false); // TODO: "Access Your Location" "Maintain Offline Storage" "Show Notifications" // PREF: Disable gamepad API to prevent USB device enumeration // https://www.w3.org/TR/gamepad/ // https://trac.torproject.org/projects/tor/ticket/13023 user_pref("dom.gamepad.enabled", false); // PREF: Disable virtual reality devices APIs // https://developer.mozilla.org/en-US/Firefox/Releases/36#Interfaces.2FAPIs.2FDOM // https://developer.mozilla.org/en-US/docs/Web/API/WebVR_API user_pref("dom.vr.enabled", false); // PREF: Disable vibrator API user_pref("dom.vibrator.enabled", false); // PREF: Disable Archive API (Firefox < 54) // https://wiki.mozilla.org/WebAPI/ArchiveAPI // https://bugzilla.mozilla.org/show_bug.cgi?id=1342361 user_pref("dom.archivereader.enabled", false); // PREF: Disable webGL // https://en.wikipedia.org/wiki/WebGL // https://www.contextis.com/resources/blog/webgl-new-dimension-browser-exploitation/ // NOTICE: Disabling WebGL breaks WebGL-based websites/applications (windy, meteoblue...) user_pref("webgl.disabled", true); // PREF: When webGL is enabled, use the minimum capability mode user_pref("webgl.min_capability_mode", true); // PREF: When webGL is enabled, disable webGL extensions // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API#WebGL_debugging_and_testing user_pref("webgl.disable-extensions", true); // PREF: When webGL is enabled, force enabling it even when layer acceleration is not supported // https://trac.torproject.org/projects/tor/ticket/18603 user_pref("webgl.disable-fail-if-major-performance-caveat", true); // PREF: When webGL is enabled, do not expose information about the graphics driver // https://bugzilla.mozilla.org/show_bug.cgi?id=1171228 // https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_debug_renderer_info user_pref("webgl.enable-debug-renderer-info", false); // somewhat related... //user_pref("pdfjs.enableWebGL", false); // PREF: Spoof dual-core CPU // https://trac.torproject.org/projects/tor/ticket/21675 // https://bugzilla.mozilla.org/show_bug.cgi?id=1360039 user_pref("dom.maxHardwareConcurrency", 2); // PREF: Disable WebAssembly // https://webassembly.org/ // https://en.wikipedia.org/wiki/WebAssembly // https://trac.torproject.org/projects/tor/ticket/21549 // NOTICE: WebAssembly is required for Unity web player/games // Required by InfluxDB user_pref("javascript.options.wasm", true); /****************************************************************************** * SECTION: Misc * ******************************************************************************/ // PREF: Disable face detection user_pref("camera.control.face_detection.enabled", false); // PREF: Disable GeoIP lookup on your address to set default search engine region // https://trac.torproject.org/projects/tor/ticket/16254 // https://support.mozilla.org/en-US/kb/how-stop-firefox-making-automatic-connections#w_geolocation-for-default-search-engine user_pref("browser.search.countryCode", "US"); user_pref("browser.search.region", "US"); user_pref("browser.search.geoip.url", ""); // PREF: Set Accept-Language HTTP header to en-US regardless of Firefox localization // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language user_pref("intl.accept_languages", "en-US, en"); // PREF: Don't use OS values to determine locale, force using Firefox locale setting // http://kb.mozillazine.org/Intl.locale.matchOS user_pref("intl.locale.matchOS", false); // Use LANG environment variable to choose locale (disabled) //pref("intl.locale.requested", ""); // PREF: Don't use Mozilla-provided location-specific search engines user_pref("browser.search.geoSpecificDefaults", false); // PREF: Do not automatically send selection to clipboard on some Linux platforms // http://kb.mozillazine.org/Clipboard.autocopy user_pref("clipboard.autocopy", false); // PREF: Prevent leaking application locale/date format using JavaScript // https://bugzilla.mozilla.org/show_bug.cgi?id=867501 // https://hg.mozilla.org/mozilla-central/rev/52d635f2b33d user_pref("javascript.use_us_english_locale", true); // PREF: Do not submit invalid URIs entered in the address bar to the default search engine // http://kb.mozillazine.org/Keyword.enabled //user_pref("keyword.enabled", false); // PREF: Don't trim HTTP off of URLs in the address bar. // https://bugzilla.mozilla.org/show_bug.cgi?id=665580 user_pref("browser.urlbar.trimURLs", false); // PREF: Disable preloading of autocomplete URLs. // https://wiki.mozilla.org/Privacy/Privacy_Task_Force/firefox_about_config_privacy_tweeks user_pref("browser.urlbar.speculativeConnect.enabled", false); // PREF: Don't try to guess domain names when entering an invalid domain name in URL bar // http://www-archive.mozilla.org/docs/end-user/domain-guessing.html user_pref("browser.fixup.alternate.enabled", false); // PREF: When browser.fixup.alternate.enabled is enabled, strip password from 'user:password@...' URLs // https://github.com/pyllyukko/user.js/issues/290#issuecomment-303560851 user_pref("browser.fixup.hide_user_pass", true); // PREF: Send DNS request through SOCKS when SOCKS proxying is in use // https://trac.torproject.org/projects/tor/wiki/doc/TorifyHOWTO/WebBrowsers user_pref("network.proxy.socks_remote_dns", true); // PREF: Don't monitor OS online/offline connection state // https://trac.torproject.org/projects/tor/ticket/18945 user_pref("network.manage-offline-status", false); // PREF: Enforce Mixed Active Content Blocking // https://support.mozilla.org/t5/Protect-your-privacy/Mixed-content-blocking-in-Firefox/ta-p/10990 // https://developer.mozilla.org/en-US/docs/Site_Compatibility_for_Firefox_23#Non-SSL_contents_on_SSL_pages_are_blocked_by_default // https://blog.mozilla.org/tanvi/2013/04/10/mixed-content-blocking-enabled-in-firefox-23/ user_pref("security.mixed_content.block_active_content", true); // PREF: Enforce Mixed Passive Content blocking (a.k.a. Mixed Display Content) // NOTICE: Enabling Mixed Display Content blocking can prevent images/styles... from loading properly when connection to the website is only partially secured user_pref("security.mixed_content.block_display_content", true); // PREF: Disable JAR from opening Unsafe File Types // http://kb.mozillazine.org/Network.jar.open-unsafe-types // CIS Mozilla Firefox 24 ESR v1.0.0 - 3.7 user_pref("network.jar.open-unsafe-types", false); // CIS 2.7.4 Disable Scripting of Plugins by JavaScript // http://forums.mozillazine.org/viewtopic.php?f=7&t=153889 user_pref("security.xpconnect.plugin.unrestricted", false); // PREF: Set File URI Origin Policy // http://kb.mozillazine.org/Security.fileuri.strict_origin_policy // CIS Mozilla Firefox 24 ESR v1.0.0 - 3.8 user_pref("security.fileuri.strict_origin_policy", true); // PREF: Disable Displaying Javascript in History URLs // http://kb.mozillazine.org/Browser.urlbar.filter.javascript // CIS 2.3.6 user_pref("browser.urlbar.filter.javascript", true); // PREF: Disable asm.js // http://asmjs.org/ // https://www.mozilla.org/en-US/security/advisories/mfsa2015-29/ // https://www.mozilla.org/en-US/security/advisories/mfsa2015-50/ // https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-2712 user_pref("javascript.options.asmjs", false); // PREF: Disable SVG in OpenType fonts // https://wiki.mozilla.org/SVGOpenTypeFonts // https://github.com/iSECPartners/publications/tree/master/reports/Tor%20Browser%20Bundle user_pref("gfx.font_rendering.opentype_svg.enabled", false); // PREF: Disable in-content SVG rendering (Firefox >= 53) (disabled) // NOTICE-DISABLED: Disabling SVG support breaks many UI elements on many sites // https://bugzilla.mozilla.org/show_bug.cgi?id=1216893 // https://github.com/iSECPartners/publications/raw/master/reports/Tor%20Browser%20Bundle/Tor%20Browser%20Bundle%20-%20iSEC%20Deliverable%201.3.pdf#16 //user_pref("svg.disabled", true); // PREF: Disable video stats to reduce fingerprinting threat // https://bugzilla.mozilla.org/show_bug.cgi?id=654550 // https://github.com/pyllyukko/user.js/issues/9#issuecomment-100468785 // https://github.com/pyllyukko/user.js/issues/9#issuecomment-148922065 user_pref("media.video_stats.enabled", false); // PREF: Don't reveal build ID // Value taken from Tor Browser // https://bugzilla.mozilla.org/show_bug.cgi?id=583181 user_pref("general.buildID.override", "20100101"); user_pref("browser.startup.homepage_override.buildID", "20100101"); // PREF: Don't use document specified fonts to prevent installed font enumeration (fingerprinting) // https://github.com/pyllyukko/user.js/issues/395 // https://browserleaks.com/fonts // https://github.com/pyllyukko/user.js/issues/120 user_pref("browser.display.use_document_fonts", 0); // PREF: Enable only whitelisted URL protocol handlers // http://kb.mozillazine.org/Network.protocol-handler.external-default // http://kb.mozillazine.org/Network.protocol-handler.warn-external-default // http://kb.mozillazine.org/Network.protocol-handler.expose.%28protocol%29 // https://news.ycombinator.com/item?id=13047883 // https://bugzilla.mozilla.org/show_bug.cgi?id=167475 // https://github.com/pyllyukko/user.js/pull/285#issuecomment-298124005 // NOTICE: Disabling nonessential protocols breaks all interaction with custom protocols such as mailto:, irc:, magnet: ... and breaks opening third-party mail/messaging/torrent/... clients when clicking on links with these protocols // TODO: Add externally-handled protocols from Windows 8.1 and Windows 10 (currently contains protocols only from Linux and Windows 7) that might pose a similar threat (see e.g. https://news.ycombinator.com/item?id=13044991) // TODO: Add externally-handled protocols from Mac OS X that might pose a similar threat (see e.g. https://news.ycombinator.com/item?id=13044991) // If you want to enable a protocol, set network.protocol-handler.expose.(protocol) to true and network.protocol-handler.external.(protocol) to: // * true, if the protocol should be handled by an external application // * false, if the protocol should be handled internally by Firefox user_pref("network.protocol-handler.warn-external-default", true); user_pref("network.protocol-handler.external.http", false); user_pref("network.protocol-handler.external.https", false); user_pref("network.protocol-handler.external.javascript", false); user_pref("network.protocol-handler.external.moz-extension", false); user_pref("network.protocol-handler.external.ftp", false); user_pref("network.protocol-handler.external.file", false); user_pref("network.protocol-handler.external.about", false); user_pref("network.protocol-handler.external.chrome", false); user_pref("network.protocol-handler.external.blob", false); user_pref("network.protocol-handler.external.data", false); user_pref("network.protocol-handler.expose-all", false); user_pref("network.protocol-handler.expose.http", true); user_pref("network.protocol-handler.expose.https", true); user_pref("network.protocol-handler.expose.javascript", true); user_pref("network.protocol-handler.expose.moz-extension", true); user_pref("network.protocol-handler.expose.ftp", true); user_pref("network.protocol-handler.expose.file", true); user_pref("network.protocol-handler.expose.about", true); user_pref("network.protocol-handler.expose.chrome", true); user_pref("network.protocol-handler.expose.blob", true); user_pref("network.protocol-handler.expose.data", true); /****************************************************************************** * SECTION: Extensions / plugins * ******************************************************************************/ // PREF: Ensure you have a security delay when installing add-ons (milliseconds) // http://kb.mozillazine.org/Disable_extension_install_delay_-_Firefox // http://www.squarefree.com/2004/07/01/race-conditions-in-security-dialogs/ user_pref("security.dialog_enable_delay", 1000); // PREF: Require signatures // https://wiki.mozilla.org/Addons/Extension_Signing //user_pref("xpinstall.signatures.required", true); // PREF: Opt-out of add-on metadata updates // https://blog.mozilla.org/addons/how-to-opt-out-of-add-on-metadata-updates/ user_pref("extensions.getAddons.cache.enabled", false); // PREF: Opt-out of themes (Persona) updates // https://support.mozilla.org/t5/Firefox/how-do-I-prevent-autoamtic-updates-in-a-50-user-environment/td-p/144287 user_pref("lightweightThemes.update.enabled", false); // PREF: Disable Flash Player NPAPI plugin // http://kb.mozillazine.org/Flash_plugin user_pref("plugin.state.flash", 0); // PREF: Disable Java NPAPI plugin user_pref("plugin.state.java", 0); // PREF: Disable sending Flash Player crash reports user_pref("dom.ipc.plugins.flash.subprocess.crashreporter.enabled", false); // PREF: When Flash crash reports are enabled, don't send the visited URL in the crash report user_pref("dom.ipc.plugins.reportCrashURL", false); // PREF: When Flash is enabled, download and use Mozilla SWF URIs blocklist // https://bugzilla.mozilla.org/show_bug.cgi?id=1237198 // https://github.com/mozilla-services/shavar-plugin-blocklist user_pref("browser.safebrowsing.blockedURIs.enabled", true); // PREF: Disable Gnome Shell Integration NPAPI plugin user_pref("plugin.state.libgnome-shell-browser-plugin", 0); // PREF: Disable the bundled OpenH264 video codec (disabled) // http://forums.mozillazine.org/viewtopic.php?p=13845077&sid=28af2622e8bd8497b9113851676846b1#p13845077 //user_pref("media.gmp-provider.enabled", false); // PREF: Enable plugins click-to-play // https://wiki.mozilla.org/Firefox/Click_To_Play // https://blog.mozilla.org/security/2012/10/11/click-to-play-plugins-blocklist-style/ user_pref("plugins.click_to_play", true); // PREF: Updates addons automatically // https://blog.mozilla.org/addons/how-to-turn-off-add-on-updates/ user_pref("extensions.update.enabled", true); // PREF: Enable add-on and certificate blocklists (OneCRL) from Mozilla // https://wiki.mozilla.org/Blocklisting // https://blocked.cdn.mozilla.net/ // http://kb.mozillazine.org/Extensions.blocklist.enabled // http://kb.mozillazine.org/Extensions.blocklist.url // https://blog.mozilla.org/security/2015/03/03/revoking-intermediate-certificates-introducing-onecrl/ // Updated at interval defined in extensions.blocklist.interval (default: 86400) user_pref("extensions.blocklist.enabled", true); user_pref("services.blocklist.update_enabled", true); // PREF: Decrease system information leakage to Mozilla blocklist update servers // https://trac.torproject.org/projects/tor/ticket/16931 user_pref("extensions.blocklist.url", "https://blocklist.addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/"); // PREF: Disable system add-on updates (hidden & always-enabled add-ons from Mozilla) // https://firefox-source-docs.mozilla.org/toolkit/mozapps/extensions/addon-manager/SystemAddons.html // https://blog.mozilla.org/data/2018/08/20/effectively-measuring-search-in-firefox/ // https://github.com/pyllyukko/user.js/issues/419 // https://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/AddonManager.jsm#1248-1257 // NOTICE: Disabling system add-on updates prevents Mozilla from "hotfixing" your browser to patch critical problems (one possible use case from the documentation) user_pref("extensions.systemAddon.update.enabled", false); /****************************************************************************** * SECTION: Firefox (anti-)features / components * * ******************************************************************************/ // PREF: Disable Extension recommendations (Firefox >= 65) // https://support.mozilla.org/en-US/kb/extension-recommendations user_pref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr", false); // PREF: Trusted Recursive Resolver (DNS-over-HTTPS) (disabled) // https://wiki.mozilla.org/Trusted_Recursive_Resolver //user_pref("network.trr.mode", 0); // PREF: Disable WebIDE // https://trac.torproject.org/projects/tor/ticket/16222 // https://developer.mozilla.org/docs/Tools/WebIDE user_pref("devtools.webide.enabled", false); user_pref("devtools.webide.autoinstallADBHelper", false); user_pref("devtools.webide.autoinstallFxdtAdapters", false); // PREF: Disable remote debugging // https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Debugging_Firefox_Desktop // https://developer.mozilla.org/en-US/docs/Tools/Tools_Toolbox#Advanced_settings user_pref("devtools.debugger.remote-enabled", false); user_pref("devtools.chrome.enabled", false); user_pref("devtools.debugger.force-local", true); // PREF: Disable Mozilla telemetry/experiments // https://wiki.mozilla.org/Platform/Features/Telemetry // https://wiki.mozilla.org/Privacy/Reviews/Telemetry // https://wiki.mozilla.org/Telemetry // https://www.mozilla.org/en-US/legal/privacy/firefox.html#telemetry // https://support.mozilla.org/t5/Firefox-crashes/Mozilla-Crash-Reporter/ta-p/1715 // https://wiki.mozilla.org/Security/Reviews/Firefox6/ReviewNotes/telemetry // https://gecko.readthedocs.io/en/latest/browser/experiments/experiments/manifest.html // https://wiki.mozilla.org/Telemetry/Experiments // https://support.mozilla.org/en-US/questions/1197144 // https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/internals/preferences.html#id1 user_pref("toolkit.telemetry.enabled", false); user_pref("toolkit.telemetry.unified", false); user_pref("toolkit.telemetry.archive.enabled", false); user_pref("experiments.supported", false); user_pref("experiments.enabled", false); user_pref("experiments.manifest.uri", ""); // PREF: Disallow Necko to do A/B testing // https://trac.torproject.org/projects/tor/ticket/13170 user_pref("network.allow-experiments", false); // PREF: Disable sending Firefox crash reports to Mozilla servers // https://wiki.mozilla.org/Breakpad // http://kb.mozillazine.org/Breakpad // https://dxr.mozilla.org/mozilla-central/source/toolkit/crashreporter // https://bugzilla.mozilla.org/show_bug.cgi?id=411490 // A list of submitted crash reports can be found at about:crashes user_pref("breakpad.reportURL", ""); // PREF: Disable sending reports of tab crashes to Mozilla (about:tabcrashed), don't nag user about unsent crash reports // https://hg.mozilla.org/mozilla-central/file/tip/browser/app/profile/firefox.js user_pref("browser.tabs.crashReporting.sendReport", false); user_pref("browser.crashReports.unsubmittedCheck.enabled", false); // PREF: Disable FlyWeb (discovery of LAN/proximity IoT devices that expose a Web interface) // https://wiki.mozilla.org/FlyWeb // https://wiki.mozilla.org/FlyWeb/Security_scenarios // https://docs.google.com/document/d/1eqLb6cGjDL9XooSYEEo7mE-zKQ-o-AuDTcEyNhfBMBM/edit // http://www.ghacks.net/2016/07/26/firefox-flyweb user_pref("dom.flyweb.enabled", false); // PREF: Disable the UITour backend // https://trac.torproject.org/projects/tor/ticket/19047#comment:3 user_pref("browser.uitour.enabled", false); // PREF: Enable Firefox Tracking Protection // https://wiki.mozilla.org/Security/Tracking_protection // https://support.mozilla.org/en-US/kb/tracking-protection-firefox // https://support.mozilla.org/en-US/kb/tracking-protection-pbm // https://kontaxis.github.io/trackingprotectionfirefox/ // https://feeding.cloud.geek.nz/posts/how-tracking-protection-works-in-firefox/ user_pref("privacy.trackingprotection.enabled", true); user_pref("privacy.trackingprotection.pbmode.enabled", true); // PREF: Enable contextual identity Containers feature (Firefox >= 52) // NOTICE: Containers are not available in Private Browsing mode // https://wiki.mozilla.org/Security/Contextual_Identity_Project/Containers user_pref("privacy.userContext.enabled", true); // PREF: Enable Firefox's anti-fingerprinting mode ("resist fingerprinting" or RFP) (Tor Uplift project) // https://wiki.mozilla.org/Security/Tor_Uplift/Tracking // https://bugzilla.mozilla.org/show_bug.cgi?id=1333933 // https://wiki.mozilla.org/Security/Fingerprinting // NOTICE: RFP breaks some keyboard shortcuts used in certain websites (see #443) // NOTICE: RFP changes your time zone // NOTICE: RFP breaks some DDoS protection pages (Cloudflare) // Must have this for Adguard, Linkwarden, and possibly others - affects the displayed time user_pref("privacy.resistFingerprinting", false); // PREF: disable mozAddonManager Web API [FF57+] // https://bugzilla.mozilla.org/buglist.cgi?bug_id=1384330 // https://bugzilla.mozilla.org/buglist.cgi?bug_id=1406795 // https://bugzilla.mozilla.org/buglist.cgi?bug_id=1415644 // https://bugzilla.mozilla.org/buglist.cgi?bug_id=1453988 // https://trac.torproject.org/projects/tor/ticket/26114 user_pref("privacy.resistFingerprinting.block_mozAddonManager", true); user_pref("extensions.webextensions.restrictedDomains", ""); // PREF: enable RFP letterboxing / resizing of inner window [FF67+] (disabled) // https://bugzilla.mozilla.org/1407366 //user_pref("privacy.resistFingerprinting.letterboxing", true); //user_pref("privacy.resistFingerprinting.letterboxing.dimensions", "800x600, 1000x1000, 1600x900"); // PREF: disable showing about:blank/maximized window as soon as possible during startup [FF60+] // https://bugzilla.mozilla.org/1448423 user_pref("browser.startup.blankWindow", false); // PREF: Disable the built-in PDF viewer // https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-2743 // https://blog.mozilla.org/security/2015/08/06/firefox-exploit-found-in-the-wild/ // https://www.mozilla.org/en-US/security/advisories/mfsa2015-69/ user_pref("pdfjs.disabled", true); // PREF: Disable collection/sending of the health report (healthreport.sqlite*) // https://support.mozilla.org/en-US/kb/firefox-health-report-understand-your-browser-perf // https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/preferences.html user_pref("datareporting.healthreport.uploadEnabled", false); user_pref("datareporting.healthreport.service.enabled", false); user_pref("datareporting.policy.dataSubmissionEnabled", false); // "Allow Firefox to make personalized extension recommendations" user_pref("browser.discovery.enabled", false); // PREF: Disable Shield/Heartbeat/Normandy (Mozilla user rating telemetry) // https://wiki.mozilla.org/Advocacy/heartbeat // https://trac.torproject.org/projects/tor/ticket/19047 // https://trac.torproject.org/projects/tor/ticket/18738 // https://wiki.mozilla.org/Firefox/Shield // https://github.com/mozilla/normandy // https://support.mozilla.org/en-US/kb/shield // https://bugzilla.mozilla.org/show_bug.cgi?id=1370801 // https://wiki.mozilla.org/Firefox/Normandy/PreferenceRollout user_pref("app.normandy.enabled", false); user_pref("app.normandy.api_url", ""); user_pref("extensions.shield-recipe-client.enabled", false); user_pref("app.shield.optoutstudies.enabled", false); // PREF: Disable Firefox Hello (disabled) (Firefox < 49) // https://wiki.mozilla.org/Loop // https://support.mozilla.org/t5/Chat-and-share/Support-for-Hello-discontinued-in-Firefox-49/ta-p/37946 // NOTICE-DISABLED: Firefox Hello requires setting media.peerconnection.enabled and media.getusermedia.screensharing.enabled to true, security.OCSP.require to false to work. //user_pref("loop.enabled", false); // PREF: Disable Firefox Hello metrics collection // https://groups.google.com/d/topic/mozilla.dev.platform/nyVkCx-_sFw/discussion user_pref("loop.logDomains", false); // PREF: Enable Auto Update (disabled) // NOTICE: Fully automatic updates are disabled and left to package management systems on Linux. Windows users may want to change this setting. // CIS 2.1.1 //user_pref("app.update.auto", true); // PREF: Enforce checking for Firefox updates // http://kb.mozillazine.org/App.update.enabled // NOTICE: Update check page might incorrectly report Firefox ESR as out-of-date user_pref("app.update.enabled", true); // PREF: Enable blocking reported web forgeries // https://wiki.mozilla.org/Security/Safe_Browsing // http://kb.mozillazine.org/Safe_browsing // https://support.mozilla.org/en-US/kb/how-does-phishing-and-malware-protection-work // http://forums.mozillazine.org/viewtopic.php?f=39&t=2711237&p=12896849#p12896849 // CIS 2.3.4 user_pref("browser.safebrowsing.enabled", true); // Firefox < 50 user_pref("browser.safebrowsing.phishing.enabled", true); // firefox >= 50 // PREF: Enable blocking reported attack sites // http://kb.mozillazine.org/Browser.safebrowsing.malware.enabled // CIS 2.3.5 user_pref("browser.safebrowsing.malware.enabled", true); // PREF: Disable querying Google Application Reputation database for downloaded binary files // https://www.mozilla.org/en-US/firefox/39.0/releasenotes/ // https://wiki.mozilla.org/Security/Application_Reputation user_pref("browser.safebrowsing.downloads.remote.enabled", false); // PREF: Disable Pocket // https://support.mozilla.org/en-US/kb/save-web-pages-later-pocket-firefox // https://github.com/pyllyukko/user.js/issues/143 user_pref("browser.pocket.enabled", false); user_pref("extensions.pocket.enabled", false); // PREF: Disable "Recommended by Pocket" in Firefox Quantum user_pref("browser.newtabpage.activity-stream.feeds.section.topstories", false); // PREF: Enable Global Privacy Control (GPC) (Firefox >= 120) // https://support.mozilla.org/1/firefox/126.0/Linux/en-US/global-privacy-control // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-GPC // https://globalprivacycontrol.org/ user_pref("privacy.globalprivacycontrol.enabled", true); /****************************************************************************** * SECTION: Automatic connections * ******************************************************************************/ // PREF: Limit the connection keep-alive timeout to 15 seconds (disabled) // https://github.com/pyllyukko/user.js/issues/387 // http://kb.mozillazine.org/Network.http.keep-alive.timeout // https://httpd.apache.org/docs/current/mod/core.html#keepalivetimeout //user_pref("network.http.keep-alive.timeout", 15); // PREF: Disable prefetching of URLs // http://kb.mozillazine.org/Network.prefetch-next // https://developer.mozilla.org/en-US/docs/Web/HTTP/Link_prefetching_FAQ#Is_there_a_preference_to_disable_link_prefetching.3F user_pref("network.prefetch-next", false); // PREF: Disable DNS prefetching // http://kb.mozillazine.org/Network.dns.disablePrefetch // https://developer.mozilla.org/en-US/docs/Web/HTTP/Controlling_DNS_prefetching user_pref("network.dns.disablePrefetch", true); user_pref("network.dns.disablePrefetchFromHTTPS", true); // PREF: Disable the predictive service (Necko) // https://wiki.mozilla.org/Privacy/Reviews/Necko user_pref("network.predictor.enabled", false); // PREF: Reject .onion hostnames before passing the to DNS // https://bugzilla.mozilla.org/show_bug.cgi?id=1228457 // RFC 7686 user_pref("network.dns.blockDotOnion", true); // PREF: Disable search suggestions in the search bar // http://kb.mozillazine.org/Browser.search.suggest.enabled user_pref("browser.search.suggest.enabled", false); // PREF: Disable "Show search suggestions in location bar results" user_pref("browser.urlbar.suggest.searches", false); // PREF: When using the location bar, don't suggest URLs from browsing history user_pref("browser.urlbar.suggest.history", false); // PREF: Disable Firefox Suggest // https://www.ghacks.net/2021/09/09/how-to-disable-firefox-suggest/ // https://support.mozilla.org/en-US/kb/navigate-web-faster-firefox-suggest user_pref("browser.urlbar.groupLabels.enabled", false); // Firefox >= 93 // PREF: Disable SSDP // https://bugzilla.mozilla.org/show_bug.cgi?id=1111967 user_pref("browser.casting.enabled", false); // PREF: Disable automatic downloading of OpenH264 codec // https://support.mozilla.org/en-US/kb/how-stop-firefox-making-automatic-connections#w_media-capabilities // https://andreasgal.com/2014/10/14/openh264-now-in-firefox/ user_pref("media.gmp-gmpopenh264.enabled", false); user_pref("media.gmp-manager.url", ""); // PREF: Disable speculative pre-connections // https://support.mozilla.org/en-US/kb/how-stop-firefox-making-automatic-connections#w_speculative-pre-connections // https://bugzilla.mozilla.org/show_bug.cgi?id=814169 user_pref("network.http.speculative-parallel-limit", 0); // PREF: Disable downloading homepage snippets/messages from Mozilla // https://support.mozilla.org/en-US/kb/how-stop-firefox-making-automatic-connections#w_mozilla-content // https://wiki.mozilla.org/Firefox/Projects/Firefox_Start/Snippet_Service user_pref("browser.aboutHomeSnippets.updateUrl", ""); // PREF: Never check updates for search engines // https://support.mozilla.org/en-US/kb/how-stop-firefox-making-automatic-connections#w_auto-update-checking user_pref("browser.search.update", false); // PREF: Disable automatic captive portal detection (Firefox >= 52.0) // https://support.mozilla.org/en-US/questions/1157121 user_pref("network.captive-portal-service.enabled", false); // PREF: Disable (parts of?) "TopSites" user_pref("browser.topsites.contile.enabled", false); user_pref("browser.newtabpage.activity-stream.feeds.topsites", false); user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false); /****************************************************************************** * SECTION: HTTP * ******************************************************************************/ // PREF: Disallow NTLMv1 // https://bugzilla.mozilla.org/show_bug.cgi?id=828183 user_pref("network.negotiate-auth.allow-insecure-ntlm-v1", false); // it is still allowed through HTTPS. uncomment the following to disable it completely. //user_pref("network.negotiate-auth.allow-insecure-ntlm-v1-https", false); // PREF: Enable CSP 1.1 script-nonce directive support // https://bugzilla.mozilla.org/show_bug.cgi?id=855326 user_pref("security.csp.experimentalEnabled", true); // PREF: Enable Content Security Policy (CSP) // https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Introducing_Content_Security_Policy // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP user_pref("security.csp.enable", true); // PREF: Enable Subresource Integrity // https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity // https://wiki.mozilla.org/Security/Subresource_Integrity user_pref("security.sri.enable", true); // PREF: DNT HTTP header (disabled) // https://www.mozilla.org/en-US/firefox/dnt/ // https://en.wikipedia.org/wiki/Do_not_track_header // https://dnt-dashboard.mozilla.org // https://github.com/pyllyukko/user.js/issues/11 // NOTICE: Do No Track must be enabled manually //user_pref("privacy.donottrackheader.enabled", true); // PREF: Send a referer header with the target URI as the source (disabled) // https://bugzilla.mozilla.org/show_bug.cgi?id=822869 // https://github.com/pyllyukko/user.js/issues/227 // NOTICE-DISABLED: Spoofing referers breaks functionality on websites relying on authentic referer headers // NOTICE-DISABLED: Spoofing referers breaks visualisation of 3rd-party sites on the Lightbeam addon // NOTICE-DISABLED: Spoofing referers disables CSRF protection on some login pages not implementing origin-header/cookie+token based CSRF protection // TODO: https://github.com/pyllyukko/user.js/issues/94, commented-out XOriginPolicy/XOriginTrimmingPolicy = 2 prefs //user_pref("network.http.referer.spoofSource", true); // PREF: Don't send referer headers when following links across different domains // https://github.com/pyllyukko/user.js/issues/227 // https://github.com/pyllyukko/user.js/issues/328 // https://feeding.cloud.geek.nz/posts/tweaking-referrer-for-privacy-in-firefox/ // https://wiki.mozilla.org/Privacy/Privacy_Task_Force/firefox_about_config_privacy_tweeks // NOTICE: Blocking referers across same eTLD sites breaks some login flows relying on them, consider lowering this pref to 1 user_pref("network.http.referer.XOriginPolicy", 2); // PREF: Trim HTTP referer headers to only send the scheme, host, and port // https://wiki.mozilla.org/Privacy/Privacy_Task_Force/firefox_about_config_privacy_tweeks user_pref("network.http.referer.trimmingPolicy", 2); // PREF: When sending Referer across domains, only send scheme, host, and port in the Referer header // https://wiki.mozilla.org/Privacy/Privacy_Task_Force/firefox_about_config_privacy_tweeks user_pref("network.http.referer.XOriginTrimmingPolicy", 2); // PREF: Accept Only 1st Party Cookies // http://kb.mozillazine.org/Network.cookie.cookieBehavior#1 // NOTICE: Blocking 3rd-party cookies breaks a number of payment gateways // CIS 2.5.1 //user_pref("network.cookie.cookieBehavior", 1); // PREF: Enable first-party isolation // https://bugzilla.mozilla.org/show_bug.cgi?id=1299996 // https://bugzilla.mozilla.org/show_bug.cgi?id=1260931 // https://wiki.mozilla.org/Security/FirstPartyIsolation // NOTICE: First-party isolation breaks Microsoft Teams // NOTICE: First-party isolation causes HTTP basic auth to ask for credentials for every new tab (see #425) user_pref("privacy.firstparty.isolate", true); // PREF: Make sure that third-party cookies (if enabled) never persist beyond the session. // https://feeding.cloud.geek.nz/posts/tweaking-cookies-for-privacy-in-firefox/ // http://kb.mozillazine.org/Network.cookie.thirdparty.sessionOnly // https://developer.mozilla.org/en-US/docs/Cookies_Preferences_in_Mozilla#network.cookie.thirdparty.sessionOnly //user_pref("network.cookie.thirdparty.sessionOnly", true); // PREF: Spoof User-agent (disabled) //user_pref("general.useragent.override", "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0"); //user_pref("general.appname.override", "Netscape"); //user_pref("general.appversion.override", "5.0 (Windows)"); //user_pref("general.platform.override", "Win32"); //user_pref("general.oscpu.override", "Windows NT 6.1"); /******************************************************************************* * SECTION: Caching * ******************************************************************************/ // PREF: Permanently enable private browsing mode // https://support.mozilla.org/en-US/kb/Private-Browsing // https://wiki.mozilla.org/PrivateBrowsing // NOTICE: You can not view or inspect cookies when in private browsing: https://bugzilla.mozilla.org/show_bug.cgi?id=823941 // NOTICE: When Javascript is enabled, Websites can detect use of Private Browsing mode // NOTICE: Private browsing breaks Kerberos authentication // NOTICE: Disables "Containers" functionality (see below) // NOTICE: "Always use private browsing mode" (browser.privatebrowsing.autostart) disables the possibility to use password manager: https://support.mozilla.org/en-US/kb/usernames-and-passwords-are-not-saved#w_private-browsing //user_pref("browser.privatebrowsing.autostart", true); // PREF: Do not download URLs for the offline cache // http://kb.mozillazine.org/Browser.cache.offline.enable user_pref("browser.cache.offline.enable", false); // PREF: Clear history when Firefox closes // https://support.mozilla.org/en-US/kb/Clear%20Recent%20History#w_how-do-i-make-firefox-clear-my-history-automatically // NOTICE: Installing user.js will remove your browsing history, caches and local storage. // NOTICE: Installing user.js **will remove your saved passwords** (https://github.com/pyllyukko/user.js/issues/27) // NOTICE: Clearing open windows on Firefox exit causes 2 windows to open when Firefox starts https://bugzilla.mozilla.org/show_bug.cgi?id=1334945 //user_pref("privacy.sanitize.sanitizeOnShutdown", true); //user_pref("privacy.clearOnShutdown.cache", true); //user_pref("privacy.clearOnShutdown.cookies", true); user_pref("privacy.clearOnShutdown.downloads", true); user_pref("privacy.clearOnShutdown.formdata", true); //user_pref("privacy.clearOnShutdown.history", true); user_pref("privacy.clearOnShutdown.offlineApps", true); //user_pref("privacy.clearOnShutdown.sessions", true); //user_pref("privacy.clearOnShutdown.openWindows", true); // PREF: Set time range to "Everything" as default in "Clear Recent History" //user_pref("privacy.sanitize.timeSpan", 0); // PREF: Clear everything but "Site Preferences" in "Clear Recent History" user_pref("privacy.cpd.offlineApps", true); //user_pref("privacy.cpd.cache", true); //user_pref("privacy.cpd.cookies", true); user_pref("privacy.cpd.downloads", true); user_pref("privacy.cpd.formdata", true); //user_pref("privacy.cpd.history", true); //user_pref("privacy.cpd.sessions", true); // PREF: Don't remember browsing history user_pref("places.history.enabled", false); // PREF: Don't remember recently closed tabs //user_pref("browser.sessionstore.max_tabs_undo", 0); // PREF: Disable disk cache // http://kb.mozillazine.org/Browser.cache.disk.enable user_pref("browser.cache.disk.enable", false); // PREF: Disable memory cache (disabled) // http://kb.mozillazine.org/Browser.cache.memory.enable //user_pref("browser.cache.memory.enable", false); // PREF: Disable Caching of SSL Pages // CIS Version 1.2.0 October 21st, 2011 2.5.8 // http://kb.mozillazine.org/Browser.cache.disk_cache_ssl user_pref("browser.cache.disk_cache_ssl", false); // PREF: Disable download history // CIS Version 1.2.0 October 21st, 2011 2.5.5 user_pref("browser.download.manager.retention", 0); // PREF: Disable password manager (use an external password manager!) // CIS Version 1.2.0 October 21st, 2011 2.5.2 user_pref("signon.rememberSignons", false); // PREF: Disable form autofill, don't save information entered in web page forms and the Search Bar user_pref("browser.formfill.enable", false); // PREF: Cookies expires at the end of the session (when the browser closes) // http://kb.mozillazine.org/Network.cookie.lifetimePolicy#2 //user_pref("network.cookie.lifetimePolicy", 2); // PREF: Require manual intervention to autofill known username/passwords sign-in forms // http://kb.mozillazine.org/Signon.autofillForms // https://www.torproject.org/projects/torbrowser/design/#identifier-linkability user_pref("signon.autofillForms", false); // PREF: Disable formless login capture // https://bugzilla.mozilla.org/show_bug.cgi?id=1166947 user_pref("signon.formlessCapture.enabled", false); // PREF: When username/password autofill is enabled, still disable it on non-HTTPS sites // https://hg.mozilla.org/integration/mozilla-inbound/rev/f0d146fe7317 user_pref("signon.autofillForms.http", false); // PREF: Show in-content login form warning UI for insecure login fields // https://hg.mozilla.org/integration/mozilla-inbound/rev/f0d146fe7317 user_pref("security.insecure_field_warning.contextual.enabled", true); // PREF: Disable the password manager for pages with autocomplete=off (disabled) // https://bugzilla.mozilla.org/show_bug.cgi?id=956906 // OWASP ASVS V9.1 // Does not prevent any kind of auto-completion (see browser.formfill.enable, signon.autofillForms) //user_pref("signon.storeWhenAutocompleteOff", false); // PREF: Delete Search and Form History // CIS Version 1.2.0 October 21st, 2011 2.5.6 user_pref("browser.formfill.expire_days", 0); // PREF: Clear SSL Form Session Data // http://kb.mozillazine.org/Browser.sessionstore.privacy_level#2 // Store extra session data for unencrypted (non-HTTPS) sites only. // CIS Version 1.2.0 October 21st, 2011 2.5.7 // NOTE: CIS says 1, we use 2 user_pref("browser.sessionstore.privacy_level", 2); // PREF: Delete temporary files on exit // https://bugzilla.mozilla.org/show_bug.cgi?id=238789 user_pref("browser.helperApps.deleteTempFileOnExit", true); // PREF: Do not create screenshots of visited pages (relates to the "new tab page" feature) // https://support.mozilla.org/en-US/questions/973320 // https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Preference_reference/browser.pagethumbnails.capturing_disabled user_pref("browser.pagethumbnails.capturing_disabled", true); // PREF: Don't fetch and permanently store favicons for Windows .URL shortcuts created by drag and drop // NOTICE: .URL shortcut files will be created with a generic icon // Favicons are stored as .ico files in \profile_dir\shortcutCache user_pref("browser.shell.shortcutFavicons", false); // PREF: Disable bookmarks backups (default: 15) // http://kb.mozillazine.org/Browser.bookmarks.max_backups user_pref("browser.bookmarks.max_backups", 0); // PREF: Export bookmarks to HTML automatically when closing Firefox (disabled) // https://support.mozilla.org/en-US/questions/1176242 //user_pref("browser.bookmarks.autoExportHTML", true); //user_pref("browser.bookmarks.file", '/path/to/bookmarks-export.html'); // PREF: Disable downloading of favicons in response to favicon fingerprinting techniques // https://github.com/jonasstrehle/supercookie // http://kb.mozillazine.org/Browser.chrome.site_icons // https://blog.mozilla.org/security/2021/01/26/supercookie-protections/ user_pref("browser.chrome.site_icons", false); /******************************************************************************* * SECTION: UI related * *******************************************************************************/ // PREF: Enable insecure password warnings (login forms in non-HTTPS pages) // https://blog.mozilla.org/tanvi/2016/01/28/no-more-passwords-over-http-please/ // https://bugzilla.mozilla.org/show_bug.cgi?id=1319119 // https://bugzilla.mozilla.org/show_bug.cgi?id=1217156 user_pref("security.insecure_password.ui.enabled", true); // PREF: Disable right-click menu manipulation via JavaScript (disabled) //user_pref("dom.event.contextmenu.enabled", false); // PREF: Disable "Are you sure you want to leave this page?" popups on page close // https://support.mozilla.org/en-US/questions/1043508 // NOTICE: disabling "beforeunload" events may lead to losing data entered in web forms // Does not prevent JS leaks of the page close event. // https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload //user_pref("dom.disable_beforeunload", true); // PREF: Disable Downloading on Desktop // CIS 2.3.2 user_pref("browser.download.folderList", 2); // PREF: Always ask the user where to download // https://developer.mozilla.org/en/Download_Manager_preferences (obsolete) user_pref("browser.download.useDownloadDir", false); // PREF: Disable the "new tab page" feature and show a blank tab instead // https://wiki.mozilla.org/Privacy/Reviews/New_Tab // https://support.mozilla.org/en-US/kb/new-tab-page-show-hide-and-customize-top-sites#w_how-do-i-turn-the-new-tab-page-off user_pref("browser.newtabpage.enabled", false); user_pref("browser.newtab.url", "about:blank"); // PREF: Disable Snippets // https://wiki.mozilla.org/Firefox/Projects/Firefox_Start/Snippet_Service // https://support.mozilla.org/en-US/kb/snippets-firefox-faq user_pref("browser.newtabpage.activity-stream.feeds.snippets", false); // PREF: Disable Activity Stream // https://wiki.mozilla.org/Firefox/Activity_Stream user_pref("browser.newtabpage.activity-stream.enabled", false); // PREF: Disable new tab tile ads & preload // http://www.thewindowsclub.com/disable-remove-ad-tiles-from-firefox // http://forums.mozillazine.org/viewtopic.php?p=13876331#p13876331 // https://wiki.mozilla.org/Tiles/Technical_Documentation#Ping // https://gecko.readthedocs.org/en/latest/browser/browser/DirectoryLinksProvider.html#browser-newtabpage-directory-source // https://gecko.readthedocs.org/en/latest/browser/browser/DirectoryLinksProvider.html#browser-newtabpage-directory-ping // TODO: deprecated? not in DXR, some dead links user_pref("browser.newtabpage.enhanced", false); user_pref("browser.newtab.preload", false); user_pref("browser.newtabpage.directory.ping", ""); user_pref("browser.newtabpage.directory.source", "data:text/plain,{}"); // PREF: Disable Mozilla VPN ads on the about:protections page // https://support.mozilla.org/en-US/kb/what-mozilla-vpn-and-how-does-it-work // https://en.wikipedia.org/wiki/Mozilla_VPN // https://blog.mozilla.org/security/2021/08/31/mozilla-vpn-security-audit/ // https://www.mozilla.org/en-US/security/advisories/mfsa2021-31/ user_pref("browser.vpn_promo.enabled", false); // PREF: Enable Auto Notification of Outdated Plugins (Firefox < 50) // https://wiki.mozilla.org/Firefox3.6/Plugin_Update_Awareness_Security_Review // CIS Version 1.2.0 October 21st, 2011 2.1.2 // https://hg.mozilla.org/mozilla-central/rev/304560 user_pref("plugins.update.notifyUser", true); // PREF: Force Punycode for Internationalized Domain Names // http://kb.mozillazine.org/Network.IDN_show_punycode // https://www.xudongz.com/blog/2017/idn-phishing/ // https://wiki.mozilla.org/IDN_Display_Algorithm // https://en.wikipedia.org/wiki/IDN_homograph_attack // https://www.mozilla.org/en-US/security/advisories/mfsa2017-02/ // CIS Mozilla Firefox 24 ESR v1.0.0 - 3.6 user_pref("network.IDN_show_punycode", true); // PREF: Disable inline autocomplete in URL bar // http://kb.mozillazine.org/Inline_autocomplete user_pref("browser.urlbar.autoFill", false); user_pref("browser.urlbar.autoFill.typed", false); // PREF: Disable CSS :visited selectors // https://blog.mozilla.org/security/2010/03/31/plugging-the-css-history-leak/ // https://dbaron.org/mozilla/visited-privacy user_pref("layout.css.visited_links_enabled", false); // PREF: Disable URL bar autocomplete and history/bookmarks suggestions dropdown // http://kb.mozillazine.org/Disabling_autocomplete_-_Firefox#Firefox_3.5 user_pref("browser.urlbar.autocomplete.enabled", false); // PREF: Do not check if Firefox is the default browser user_pref("browser.shell.checkDefaultBrowser", false); // PREF: When password manager is enabled, lock the password storage periodically // CIS Version 1.2.0 October 21st, 2011 2.5.3 Disable Prompting for Credential Storage user_pref("security.ask_for_password", 2); // PREF: Lock the password storage every 1 minutes (default: 30) user_pref("security.password_lifetime", 1); // PREF: Display a notification bar when websites offer data for offline use // http://kb.mozillazine.org/Browser.offline-apps.notify user_pref("browser.offline-apps.notify", true); /****************************************************************************** * SECTION: Cryptography * ******************************************************************************/ // PREF: Enable HTTPS-Only Mode // https://blog.mozilla.org/security/2020/11/17/firefox-83-introduces-https-only-mode/ // https://www.feistyduck.com/bulletproof-tls-newsletter/issue_71_firefox_introduces_https_only_mode user_pref("dom.security.https_only_mode", true); // PREF: Enable HSTS preload list (pre-set HSTS sites list provided by Mozilla) // https://blog.mozilla.org/security/2012/11/01/preloading-hsts/ // https://wiki.mozilla.org/Privacy/Features/HSTS_Preload_List // https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security user_pref("network.stricttransportsecurity.preloadlist", true); // PREF: Enable Online Certificate Status Protocol // https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol // https://www.imperialviolet.org/2014/04/19/revchecking.html // https://www.maikel.pro/blog/current-state-certificate-revocation-crls-ocsp/ // https://wiki.mozilla.org/CA:RevocationPlan // https://wiki.mozilla.org/CA:ImprovingRevocation // https://wiki.mozilla.org/CA:OCSP-HardFail // https://news.netcraft.com/archives/2014/04/24/certificate-revocation-why-browsers-remain-affected-by-heartbleed.html // https://news.netcraft.com/archives/2013/04/16/certificate-revocation-and-the-performance-of-ocsp.html // NOTICE: OCSP leaks your IP and domains you visit to the CA when OCSP Stapling is not available on visited host // NOTICE: OCSP is vulnerable to replay attacks when nonce is not configured on the OCSP responder // NOTICE: OCSP adds latency (performance) // NOTICE: Short-lived certificates are not checked for revocation (security.pki.cert_short_lifetime_in_days, default:10) // CIS Version 1.2.0 October 21st, 2011 2.2.4 user_pref("security.OCSP.enabled", 1); // PREF: Enable OCSP Stapling support // https://en.wikipedia.org/wiki/OCSP_stapling // https://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox/ // https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx user_pref("security.ssl.enable_ocsp_stapling", true); // PREF: Enable OCSP Must-Staple support (Firefox >= 45) // https://blog.mozilla.org/security/2015/11/23/improving-revocation-ocsp-must-staple-and-short-lived-certificates/ // https://www.entrust.com/ocsp-must-staple/ // https://github.com/schomery/privacy-settings/issues/40 // NOTICE: Firefox falls back on plain OCSP when must-staple is not configured on the host certificate user_pref("security.ssl.enable_ocsp_must_staple", true); // PREF: Require a valid OCSP response for OCSP enabled certificates (disabled) // https://groups.google.com/forum/#!topic/mozilla.dev.security/n1G-N2-HTVA // https://www.feistyduck.com/newsletter/issue_121_the_slow_death_of_ocsp // Disabling this will make OCSP bypassable by MitM attacks suppressing OCSP responses // NOTICE: security.OCSP.require will make the connection fail when the OCSP responder is unavailable // NOTICE: security.OCSP.require is known to break browsing on some [captive portals](https://en.wikipedia.org/wiki/Captive_portal) //user_pref("security.OCSP.require", true); // PREF: Disable TLS Session Tickets // https://www.blackhat.com/us-13/briefings.html#NextGen // https://media.blackhat.com/us-13/US-13-Daigniere-TLS-Secrets-Slides.pdf // https://media.blackhat.com/us-13/US-13-Daigniere-TLS-Secrets-WP.pdf // https://bugzilla.mozilla.org/show_bug.cgi?id=917049 // https://bugzilla.mozilla.org/show_bug.cgi?id=967977 user_pref("security.ssl.disable_session_identifiers", true); // PREF: Only allow TLS 1.[2-3] // http://kb.mozillazine.org/Security.tls.version.* // 1 = TLS 1.0 is the minimum required / maximum supported encryption protocol. (This is the current default for the maximum supported version.) // 2 = TLS 1.1 is the minimum required / maximum supported encryption protocol. // 3 = TLS 1.2 is the minimum required / maximum supported encryption protocol. // 4 = TLS 1.3 is the minimum required / maximum supported encryption protocol. user_pref("security.tls.version.min", 3); user_pref("security.tls.version.max", 4); // PREF: Disable insecure TLS version fallback // https://bugzilla.mozilla.org/show_bug.cgi?id=1084025 // https://github.com/pyllyukko/user.js/pull/206#issuecomment-280229645 user_pref("security.tls.version.fallback-limit", 4); // PREF: Enforce Public Key Pinning // https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning // https://wiki.mozilla.org/SecurityEngineering/Public_Key_Pinning // "2. Strict. Pinning is always enforced." user_pref("security.cert_pinning.enforcement_level", 2); // PREF: Disallow SHA-1 // https://bugzilla.mozilla.org/show_bug.cgi?id=1302140 // https://shattered.io/ user_pref("security.pki.sha1_enforcement_level", 1); // PREF: Warn the user when server doesn't support RFC 5746 ("safe" renegotiation) // https://wiki.mozilla.org/Security:Renegotiation#security.ssl.treat_unsafe_negotiation_as_broken // https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-3555 user_pref("security.ssl.treat_unsafe_negotiation_as_broken", true); // PREF: Disallow connection to servers not supporting safe renegotiation (disabled) // https://wiki.mozilla.org/Security:Renegotiation#security.ssl.require_safe_negotiation // https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-3555 // TODO: security.ssl.require_safe_negotiation is more secure but makes browsing next to impossible (2012-2014-... - ssl_error_unsafe_negotiation errors), so is left disabled //user_pref("security.ssl.require_safe_negotiation", true); // PREF: Disable automatic reporting of TLS connection errors // https://support.mozilla.org/en-US/kb/certificate-pinning-reports // we could also disable security.ssl.errorReporting.enabled, but I think it's // good to leave the option to report potentially malicious sites if the user // chooses to do so. // you can test this at https://pinningtest.appspot.com/ user_pref("security.ssl.errorReporting.automatic", false); // PREF: Pre-populate the current URL but do not pre-fetch the certificate in the "Add Security Exception" dialog // http://kb.mozillazine.org/Browser.ssl_override_behavior // https://github.com/pyllyukko/user.js/issues/210 user_pref("browser.ssl_override_behavior", 1); // PREF: Encrypted SNI (when TRR is enabled) // https://www.cloudflare.com/ssl/encrypted-sni/ // https://wiki.mozilla.org/Trusted_Recursive_Resolver#ESNI // https://en.wikipedia.org/wiki/Server_Name_Indication#Security_implications_(ESNI) user_pref("network.security.esni.enabled", true); // PREF: Disable the Enterprise Roots preference // https://support.mozilla.org/en-US/kb/how-disable-enterprise-roots-preference // https://github.com/pyllyukko/user.js/issues/560 user_pref("security.certerrors.mitm.auto_enable_enterprise_roots", false); user_pref("security.enterprise_roots.enabled", false); /****************************************************************************** * SECTION: Cipher suites * ******************************************************************************/ // PREF: Disable null ciphers user_pref("security.ssl3.rsa_null_sha", false); user_pref("security.ssl3.rsa_null_md5", false); user_pref("security.ssl3.ecdhe_rsa_null_sha", false); user_pref("security.ssl3.ecdhe_ecdsa_null_sha", false); user_pref("security.ssl3.ecdh_rsa_null_sha", false); user_pref("security.ssl3.ecdh_ecdsa_null_sha", false); // PREF: Disable SEED cipher // https://en.wikipedia.org/wiki/SEED user_pref("security.ssl3.rsa_seed_sha", false); // PREF: Disable 40/56/128-bit ciphers // 40-bit ciphers user_pref("security.ssl3.rsa_rc4_40_md5", false); user_pref("security.ssl3.rsa_rc2_40_md5", false); // 56-bit ciphers user_pref("security.ssl3.rsa_1024_rc4_56_sha", false); // 128-bit ciphers user_pref("security.ssl3.rsa_camellia_128_sha", false); user_pref("security.ssl3.ecdhe_rsa_aes_128_sha", false); user_pref("security.ssl3.ecdhe_ecdsa_aes_128_sha", false); user_pref("security.ssl3.ecdh_rsa_aes_128_sha", false); user_pref("security.ssl3.ecdh_ecdsa_aes_128_sha", false); user_pref("security.ssl3.dhe_rsa_camellia_128_sha", false); user_pref("security.ssl3.dhe_rsa_aes_128_sha", false); // PREF: Disable RC4 // https://developer.mozilla.org/en-US/Firefox/Releases/38#Security // https://bugzilla.mozilla.org/show_bug.cgi?id=1138882 // https://rc4.io/ // https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2013-2566 user_pref("security.ssl3.ecdh_ecdsa_rc4_128_sha", false); user_pref("security.ssl3.ecdh_rsa_rc4_128_sha", false); user_pref("security.ssl3.ecdhe_ecdsa_rc4_128_sha", false); user_pref("security.ssl3.ecdhe_rsa_rc4_128_sha", false); user_pref("security.ssl3.rsa_rc4_128_md5", false); user_pref("security.ssl3.rsa_rc4_128_sha", false); user_pref("security.tls.unrestricted_rc4_fallback", false); // PREF: Disable 3DES (effective key size is < 128) // https://en.wikipedia.org/wiki/3des#Security // http://en.citizendium.org/wiki/Meet-in-the-middle_attack // http://www-archive.mozilla.org/projects/security/pki/nss/ssl/fips-ssl-ciphersuites.html user_pref("security.ssl3.dhe_dss_des_ede3_sha", false); user_pref("security.ssl3.dhe_rsa_des_ede3_sha", false); user_pref("security.ssl3.ecdh_ecdsa_des_ede3_sha", false); user_pref("security.ssl3.ecdh_rsa_des_ede3_sha", false); user_pref("security.ssl3.ecdhe_ecdsa_des_ede3_sha", false); user_pref("security.ssl3.ecdhe_rsa_des_ede3_sha", false); user_pref("security.ssl3.rsa_des_ede3_sha", false); user_pref("security.ssl3.rsa_fips_des_ede3_sha", false); // PREF: Disable ciphers with ECDH (non-ephemeral) user_pref("security.ssl3.ecdh_rsa_aes_256_sha", false); user_pref("security.ssl3.ecdh_ecdsa_aes_256_sha", false); // PREF: Disable 256 bits ciphers without PFS user_pref("security.ssl3.rsa_camellia_256_sha", false); // PREF: Enable GCM ciphers (TLSv1.2 only) // https://en.wikipedia.org/wiki/Galois/Counter_Mode user_pref("security.ssl3.ecdhe_ecdsa_aes_128_gcm_sha256", true); // 0xc02b user_pref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", true); // 0xc02f // PREF: Enable ChaCha20 and Poly1305 (Firefox >= 47) // https://www.mozilla.org/en-US/firefox/47.0/releasenotes/ // https://tools.ietf.org/html/rfc7905 // https://bugzilla.mozilla.org/show_bug.cgi?id=917571 // https://bugzilla.mozilla.org/show_bug.cgi?id=1247860 // https://cr.yp.to/chacha.html user_pref("security.ssl3.ecdhe_ecdsa_chacha20_poly1305_sha256", true); user_pref("security.ssl3.ecdhe_rsa_chacha20_poly1305_sha256", true); // PREF: Disable ciphers susceptible to the logjam attack // https://weakdh.org/ user_pref("security.ssl3.dhe_rsa_camellia_256_sha", false); user_pref("security.ssl3.dhe_rsa_aes_256_sha", false); // PREF: Disable ciphers with DSA (max 1024 bits) user_pref("security.ssl3.dhe_dss_aes_128_sha", false); user_pref("security.ssl3.dhe_dss_aes_256_sha", false); user_pref("security.ssl3.dhe_dss_camellia_128_sha", false); user_pref("security.ssl3.dhe_dss_camellia_256_sha", false); // PREF: Ciphers with CBC & SHA-1 (disabled) //user_pref("security.ssl3.rsa_aes_256_sha", false); // 0x35 //user_pref("security.ssl3.rsa_aes_128_sha", false); // 0x2f //user_pref("security.ssl3.ecdhe_rsa_aes_256_sha", false); // 0xc014 //user_pref("security.ssl3.ecdhe_ecdsa_aes_256_sha", false); // 0xc00a // PREF: Enable X25519Kyber768Draft00 (post-quantum key exchange) [FF Nightly 2024-01-18+] // https://datatracker.ietf.org/doc/draft-tls-westerbaan-xyber768d00/ // https://twitter.com/bwesterb/status/1748017372764475519 // https://pq.cloudflareresearch.com/ user_pref("security.tls.enable_kyber", true); EOFUS for curFile in $(find ~ -name prefs.js 2> /dev/null | grep firefox) do curDir=$(dirname $curFile) cp $HOME/user.js $curDir/ done rm -f $HOME/user.js } function addVNCServer() { set +e sudo systemctl list-unit-files x11vnc.service > /dev/null 2>&1 if [ $? -eq 0 ] || sudo test -f /lib/systemd/system/x11vnc.service; then echo "There is already an existing x11vnc service installed. Please remove it, then retry." return fi echo "Installing x11vnc..." performAptUpdate true > /dev/null 2>&1 performAptInstall x11vnc > /dev/null 2>&1 echo "Configuring x11vnc..." sudo x11vnc -storepasswd "$USER_SUDO_PW" /etc/x11vnc.pass > /dev/null 2>&1 sudo tee /lib/systemd/system/x11vnc.service >/dev/null < /dev/null 2>&1 sudo systemctl enable --now x11vnc.service > /dev/null 2>&1 echo "Updating firewall..." # Allow dock-ext network access (for Guacamole) ruleName="dock-ext-net" subnet="${NET_EXTERNAL_SUBNET_PREFIX}.0/21" custNetID=$(sqlite3 $HSHQ_DB "select ID from customfwsubnet where Name='$ruleName' and Subnet='$subnet';") curPortsList=$(sqlite3 $HSHQ_DB "select InputAllowPorts from customfwsubnet where ID=$custNetID;") echo "$curPortsList" | grep -q "$VNC_SERVER_PORT" > /dev/null 2>&1 if [ $? -ne 0 ]; then if [ -z "$curPortsList" ]; then curPortsList="${VNC_SERVER_PORT}/both" else curPortsList="${curPortsList},${VNC_SERVER_PORT}/both" fi fi updateExposedPortsLists INPUT "$custNetID (Custom)" "$curPortsList" > /dev/null 2>&1 # Allow local network access primaryIfaceDBID=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType = 'primary' and NetworkType = 'home_network';") primaryIfaceBaseName=$(sqlite3 $HSHQ_DB "select Name from connections where ID=$primaryIfaceDBID;") primaryIfaceIFName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$primaryIfaceDBID;") curPortsList=$(sqlite3 $HSHQ_DB "select InputAllowPorts from connections where ID=$primaryIfaceDBID;") echo "$curPortsList" | grep -q "$VNC_SERVER_PORT" > /dev/null 2>&1 if [ $? -ne 0 ]; then if [ -z "$curPortsList" ]; then curPortsList="${VNC_SERVER_PORT}/both" else curPortsList="${curPortsList},${VNC_SERVER_PORT}/both" fi fi updateExposedPortsLists INPUT "$primaryIfaceIFName ($primaryIfaceBaseName)" "$curPortsList" > /dev/null 2>&1 echo "x11vnc installation complete!" } function removeVNCServer() { set +e echo "Removing x11vnc..." sudo systemctl disable --now x11vnc.service > /dev/null 2>&1 sudo rm -f /lib/systemd/system/x11vnc.service > /dev/null 2>&1 sudo systemctl daemon-reload > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y x11vnc > /dev/null 2>&1 echo "Updating firewall..." # dock-ext network ruleName="dock-ext-net" subnet="${NET_EXTERNAL_SUBNET_PREFIX}.0/21" custNetID=$(sqlite3 $HSHQ_DB "select ID from customfwsubnet where Name='$ruleName' and Subnet='$subnet';") if ! [ -z "$custNetID" ]; then curPortsList=$(sqlite3 $HSHQ_DB "select InputAllowPorts from customfwsubnet where ID=$custNetID;") echo "$curPortsList" | grep -q "$VNC_SERVER_PORT" > /dev/null 2>&1 if [ $? -eq 0 ]; then newPortsList="" portsListArr=($(echo "$curPortsList" | tr "," "\n")) for curPort in "${portsListArr[@]}" do chkPort=$(echo $curPort | cut -d"/" -f1) if [ "$chkPort" = "$VNC_SERVER_PORT" ]; then continue fi if [ -z "$newPortsList" ]; then newPortsList="$curPort" else newPortsList="${newPortsList},$curPort" fi done updateExposedPortsLists "INPUT" "$custNetID (Custom)" "$newPortsList" > /dev/null 2>&1 fi fi # Local network primaryIfaceDBID=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType = 'primary' and NetworkType = 'home_network';") primaryIfaceBaseName=$(sqlite3 $HSHQ_DB "select Name from connections where ID=$primaryIfaceDBID;") primaryIfaceIFName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$primaryIfaceDBID;") curPortsList=$(sqlite3 $HSHQ_DB "select InputAllowPorts from connections where ID=$primaryIfaceDBID;") echo "$curPortsList" | grep -q "$VNC_SERVER_PORT" > /dev/null 2>&1 if [ $? -eq 0 ]; then newPortsList="" portsListArr=($(echo "$curPortsList" | tr "," "\n")) for curPort in "${portsListArr[@]}" do chkPort=$(echo $curPort | cut -d"/" -f1) if [ "$chkPort" = "$VNC_SERVER_PORT" ]; then continue fi if [ -z "$newPortsList" ]; then newPortsList="$curPort" else newPortsList="${newPortsList},$curPort" fi done updateExposedPortsLists INPUT "$primaryIfaceIFName ($primaryIfaceBaseName)" "$newPortsList" > /dev/null 2>&1 fi echo "x11vnc removal complete!" } # Initialization function createInitialEnv() { createHSHQLog mkdir -p $HSHQ_BASE_DIR mkdir -p $HSHQ_DATA_DIR mkdir -p $HSHQ_BACKUP_DIR mkdir -p $HSHQ_NONBACKUP_DIR mkdir -p $HSHQ_BUILD_DIR mkdir -p $HSHQ_CONFIG_DIR mkdir -p $HSHQ_RELAYSERVER_DIR mkdir -p $HSHQ_SCRIPTS_DIR mkdir -p $HSHQ_SECRETS_DIR mkdir -p $HSHQ_SSL_DIR mkdir -p $HSHQ_STACKS_DIR mkdir -p $HSHQ_AIMODELS_DIR/huggingface mkdir -p $HSHQ_WIREGUARD_DIR mkdir -p $HSHQ_WIREGUARD_DIR/{internet,vpn,scripts,users,requestkeys,logs} mkdir -p $HSHQ_STACKS_DIR/shared/rdata/media/{audio,books,comics,misc,movies,software,tv} mkdir -p $HSHQ_RELAYSERVER_DIR/{backup,scripts} mkdir -p $HSHQ_SCRIPTS_DIR/user set +e sudo groupadd -g 82 nextwrite >/dev/null 2>&1 set -e sudo usermod -a -G 82 $USERNAME >/dev/null 2>&1 mkdir -p $HSHQ_STACKS_DIR/shared/{Images,SharedConsume,SharedProcessed,PersonalConsume,PersonalProcessed,PersonalTranscribeInput,PersonalTranscribeOutput} mkdir -p $HSHQ_STACKS_DIR/shared/KnowledgeBases sudo chown -R 1000:82 $HSHQ_STACKS_DIR/shared sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/SharedProcessed sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/PersonalProcessed sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/PersonalTranscribeInput sudo chown -R 1000:82 $HSHQ_STACKS_DIR/shared/PersonalTranscribeOutput sudo chmod -R 775 $HSHQ_STACKS_DIR/shared sudo mkdir -p $HSHQ_SCRIPTS_DIR/root sudo chmod 700 $HSHQ_SCRIPTS_DIR/root sudo mkdir -p $HSHQ_SCRIPTS_DIR/userasroot sudo chmod 700 $HSHQ_SCRIPTS_DIR/userasroot sudo mkdir -p $HSHQ_SCRIPTS_DIR/source sudo chmod 700 $HSHQ_SCRIPTS_DIR/source sudo chown -R ${USERNAME}:${USERNAME} $HSHQ_SCRIPTS_DIR/source sudo mkdir -p $HSHQ_SCRIPTS_DIR/boot sudo mkdir -p $HSHQ_SCRIPTS_DIR/boot/beforenetwork sudo mkdir -p $HSHQ_SCRIPTS_DIR/boot/afterdocker sudo sed -i '/\/userasroot/d' /etc/sudoers >/dev/null echo "$USERNAME ALL=(ALL) NOPASSWD: $HSHQ_SCRIPTS_DIR/userasroot/*.sh" | sudo tee -a /etc/sudoers >/dev/null sudo sed -i '/timestamp_timeout/d' /etc/sudoers >/dev/null echo "Defaults timestamp_timeout=$SUDO_NORMAL_TIMEOUT" | sudo tee -a /etc/sudoers >/dev/null sudo sed -i '/passwd_tries/d' /etc/sudoers >/dev/null echo "Defaults passwd_tries=$SUDO_MAX_RETRIES" | sudo tee -a /etc/sudoers >/dev/null sudo sed -i '/logfile=/d' /etc/sudoers >/dev/null if [ "$IS_RUST_UTILS" = "true" ]; then echo "authpriv.* /var/log/sudo.log" | sudo tee /etc/rsyslog.d/50-sudo.conf >/dev/null sudo systemctl restart rsyslog else echo "Defaults logfile=/var/log/sudo.log" | sudo tee -a /etc/sudoers >/dev/null fi sudo sed -i '/includedir/d' /etc/sudoers >/dev/null echo "@includedir /etc/sudoers.d" | sudo tee -a /etc/sudoers >/dev/null mkdir -p $HOME/.ssh set +e tmp_pw1="" tmp_pw2="" if [ "$IS_AUTO_INSTALL" = "true" ]; then tmp_pw1="$INIT_AUTO_PASSWORD" tmp_pw2="$tmp_pw1" fi while [ -z "$tmp_pw1" ] || ! [ "$tmp_pw1" = "$tmp_pw2" ] do tmp_pw1=$(promptPasswordMenu "Create Password" "Create a password to encrypt/decrypt the configuration file. ENSURE you remember this or you will be IRREVERSIBLY locked out of it (unless you have a quantum super-computer) and you will not be able to apply updates, add new services, do networking functions, etc. Also note that this password will be stored in plain text inside of the same config file, in order to (re)encrypt the file in the future: ") if [ $? -ne 0 ]; then exit; fi if [ -z "$tmp_pw1" ]; then showMessageBox "Password Empty" "The password cannot be empty, please try again." continue fi if [ $(checkValidPassword "$tmp_pw1" 8) = "false" ]; then showMessageBox "Password Invalid" "The password is too short or contains invalid characters. It must contain at least 8 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.," tmp_pw1=abc tmp_pw2=def continue fi tmp_pw2=$(promptPasswordMenu "Confirm Password" "Enter the password again to confirm: ") if [ $? -ne 0 ]; then exit; fi if [ -z "$tmp_pw1" ] || ! [ "$tmp_pw1" = "$tmp_pw2" ]; then showMessageBox "Password Mismatch" "The passwords do not match, please try again." fi done CONFIG_ENCRYPTION_PASSPHRASE="$tmp_pw1" tmp_pw1="" tmp_pw2="" CONFIG_FILE=$HSHQ_CONFIG_DIR/$CONFIG_FILE_DEFAULT_FILENAME outputEncConfigFile source $CONFIG_FILE outputPlainTextRootConfig source <(sudo cat $HSHQ_PLAINTEXT_ROOT_CONFIG) outputPlainTextUserConfig source $HSHQ_PLAINTEXT_USER_CONFIG initHSHQDB set -e } function createHSHQLog() { if ! sudo test -f $HSHQ_LOG_FILE; then sudo touch $HSHQ_LOG_FILE sudo chown $USERNAME:$USERNAME $HSHQ_LOG_FILE sudo chmod 644 $HSHQ_LOG_FILE fi } # Version Updates function checkUpdateVersion() { if [ "$IS_INSTALLED" = "false" ] || [ "$IS_PERFORM_RESTORE" = "true" ]; then return fi is_update_performed=false if [ $HSHQ_VERSION -lt $HSHQ_LIB_SCRIPT_VERSION ]; then set +e checkAddAllNewSvcs set -e is_update_performed=true pauseCronService fi if [ $HSHQ_VERSION -lt 11 ]; then echo "Updating to Version 11..." HSHQ_VERSION=11 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 14 ]; then echo "Updating to Version 14..." version14Update HSHQ_VERSION=14 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 22 ]; then echo "Updating to Version 22..." version22Update HSHQ_VERSION=22 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 23 ]; then echo "Updating to Version 23..." version23Update HSHQ_VERSION=23 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 24 ]; then echo "Updating to Version 24..." version24Update HSHQ_VERSION=24 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 25 ]; then echo "Updating to Version 25..." version25Update HSHQ_VERSION=25 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 26 ]; then echo "Updating to Version 26..." version26Update HSHQ_VERSION=26 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 27 ]; then echo "Updating to Version 27..." version27Update HSHQ_VERSION=27 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 29 ]; then echo "Updating to Version 29..." version29Update HSHQ_VERSION=29 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 30 ]; then echo "Updating to Version 30..." version30Update HSHQ_VERSION=30 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 31 ]; then echo "Updating to Version 31..." version31Update HSHQ_VERSION=31 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 32 ]; then echo "Updating to Version 32..." version32Update HSHQ_VERSION=32 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 34 ]; then echo "Updating to Version 34..." version34Update HSHQ_VERSION=34 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 35 ]; then echo "Updating to Version 35..." version35Update HSHQ_VERSION=35 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 37 ]; then echo "Updating to Version 37..." version37Update HSHQ_VERSION=37 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 38 ]; then echo "Updating to Version 38..." version38Update HSHQ_VERSION=38 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 39 ]; then echo "Updating to Version 39..." version39Update HSHQ_VERSION=39 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 40 ]; then echo "Updating to Version 40..." version40Update HSHQ_VERSION=40 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 41 ]; then echo "Updating to Version 41..." version41Update HSHQ_VERSION=41 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 43 ]; then echo "Updating to Version 43..." version43Update HSHQ_VERSION=43 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 44 ]; then echo "Updating to Version 44..." version44Update HSHQ_VERSION=44 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 46 ]; then echo "Updating to Version 46..." version46Update if [ $HSHQ_VERSION -lt 46 ]; then return fi fi if [ $HSHQ_VERSION -lt 48 ]; then echo "Updating to Version 48..." version48Update HSHQ_VERSION=48 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 52 ]; then echo "Updating to Version 52..." version52Update HSHQ_VERSION=52 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 53 ]; then echo "Updating to Version 53..." version53Update HSHQ_VERSION=53 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 54 ]; then echo "Updating to Version 54..." version54Update HSHQ_VERSION=54 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 55 ]; then echo "Updating to Version 55..." version55Update HSHQ_VERSION=55 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 56 ]; then echo "Updating to Version 56..." version56Update HSHQ_VERSION=56 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 59 ]; then echo "Updating to Version 59..." version59Update HSHQ_VERSION=59 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 64 ]; then echo "Updating to Version 64..." version64Update HSHQ_VERSION=64 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 65 ]; then echo "Updating to Version 65..." version65Update HSHQ_VERSION=65 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 66 ]; then echo "Updating to Version 66..." version66Update HSHQ_VERSION=66 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 67 ]; then echo "Updating to Version 67..." version67Update HSHQ_VERSION=67 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 68 ]; then echo "Updating to Version 68..." version68Update HSHQ_VERSION=68 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 71 ]; then echo "Updating to Version 71..." version71Update HSHQ_VERSION=71 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 72 ]; then echo "Updating to Version 72..." version72Update HSHQ_VERSION=72 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 73 ]; then echo "Updating to Version 73..." version73Update HSHQ_VERSION=73 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 75 ]; then echo "Updating to Version 75..." version75Update HSHQ_VERSION=75 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 76 ]; then echo "Updating to Version 76..." version76Update HSHQ_VERSION=76 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 78 ]; then echo "Updating to Version 78..." version78Update HSHQ_VERSION=78 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 80 ]; then echo "Updating to Version 80..." version80Update HSHQ_VERSION=80 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 83 ]; then echo "Updating to Version 83..." version83Update HSHQ_VERSION=83 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 84 ]; then echo "Updating to Version 84..." version84Update HSHQ_VERSION=84 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 85 ]; then echo "Updating to Version 85..." version85Update HSHQ_VERSION=85 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 86 ]; then echo "Updating to Version 86..." version86Update HSHQ_VERSION=86 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 87 ]; then echo "Updating to Version 87..." version87Update HSHQ_VERSION=87 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 91 ]; then echo "Updating to Version 91..." version91Update HSHQ_VERSION=91 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 98 ]; then echo "Updating to Version 98..." version98Update HSHQ_VERSION=98 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 102 ]; then echo "Updating to Version 102..." version102Update HSHQ_VERSION=102 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 105 ]; then echo "Updating to Version 105..." version105Update HSHQ_VERSION=105 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 106 ]; then echo "Updating to Version 106..." version106Update HSHQ_VERSION=106 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 108 ]; then echo "Updating to Version 108..." version108Update HSHQ_VERSION=108 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 111 ]; then echo "Updating to Version 111..." version111Update HSHQ_VERSION=111 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 113 ]; then echo "Updating to Version 113..." version113Update HSHQ_VERSION=113 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 114 ]; then echo "Updating to Version 114..." version114Update HSHQ_VERSION=114 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 119 ]; then echo "Updating to Version 119..." version119Update HSHQ_VERSION=119 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 120 ]; then echo "Updating to Version 120..." version120Update HSHQ_VERSION=120 updateConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 121 ]; then echo "Updating to Version 121..." version121Update HSHQ_VERSION=121 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 126 ]; then echo "Updating to Version 126..." version126Update HSHQ_VERSION=126 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 132 ]; then echo "Updating to Version 132..." version132Update HSHQ_VERSION=132 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 134 ]; then echo "Updating to Version 134..." version134Update HSHQ_VERSION=134 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 136 ]; then echo "Updating to Version 136..." version136Update HSHQ_VERSION=136 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 138 ]; then echo "Updating to Version 138..." version138Update HSHQ_VERSION=138 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 146 ]; then echo "Updating to Version 146..." version146Update HSHQ_VERSION=146 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 152 ]; then echo "Updating to Version 152..." version152Update HSHQ_VERSION=152 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 164 ]; then echo "Updating to Version 164..." version164Update HSHQ_VERSION=164 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 165 ]; then echo "Updating to Version 165..." version165Update HSHQ_VERSION=165 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 171 ]; then echo "Updating to Version 171..." version171Update HSHQ_VERSION=171 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 177 ]; then echo "Updating to Version 177..." version177Update HSHQ_VERSION=177 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 180 ]; then echo "Updating to Version 180..." version180Update HSHQ_VERSION=180 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 181 ]; then echo "Updating to Version 181..." version181Update HSHQ_VERSION=181 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 183 ]; then echo "Updating to Version 183..." version183Update HSHQ_VERSION=183 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 188 ]; then echo "Updating to Version 188..." version188Update HSHQ_VERSION=188 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 189 ]; then echo "Updating to Version 189..." version189Update HSHQ_VERSION=189 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 193 ]; then echo "Updating to Version 193..." version193Update HSHQ_VERSION=193 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 194 ]; then echo "Updating to Version 194..." version194Update HSHQ_VERSION=194 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 202 ]; then echo "Updating to Version 202..." version202Update HSHQ_VERSION=202 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 204 ]; then echo "Updating to Version 204..." version204Update HSHQ_VERSION=204 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 205 ]; then echo "Updating to Version 205..." version205Update HSHQ_VERSION=205 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 218 ]; then echo "Updating to Version 218..." version218Update HSHQ_VERSION=218 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 219 ]; then echo "Updating to Version 219..." version219Update HSHQ_VERSION=219 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 220 ]; then echo "Updating to Version 220..." version220Update HSHQ_VERSION=220 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 221 ]; then echo "Updating to Version 221..." version221Update HSHQ_VERSION=221 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 223 ]; then echo "Updating to Version 223..." version223Update HSHQ_VERSION=223 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 224 ]; then echo "Updating to Version 224..." version224Update HSHQ_VERSION=224 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 226 ]; then echo "Updating to Version 226..." version226Update HSHQ_VERSION=226 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 229 ]; then echo "Updating to Version 229..." version229Update HSHQ_VERSION=229 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 230 ]; then echo "Updating to Version 230..." version230Update HSHQ_VERSION=230 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 231 ]; then echo "Updating to Version 231..." version231Update HSHQ_VERSION=231 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 233 ]; then echo "Updating to Version 233..." version233Update HSHQ_VERSION=233 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 234 ]; then echo "Updating to Version 234..." version234Update HSHQ_VERSION=234 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 235 ]; then echo "Updating to Version 235..." version235Update HSHQ_VERSION=235 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 236 ]; then echo "Updating to Version 236..." version236Update HSHQ_VERSION=236 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 237 ]; then echo "Updating to Version 237..." version237Update HSHQ_VERSION=237 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 238 ]; then echo "Updating to Version 238..." version238Update HSHQ_VERSION=238 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 239 ]; then echo "Updating to Version 239..." version239Update HSHQ_VERSION=239 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 242 ]; then echo "Updating to Version 242..." version242Update HSHQ_VERSION=242 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 243 ]; then echo "Updating to Version 243..." version243Update HSHQ_VERSION=243 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 244 ]; then echo "Updating to Version 244..." version244Update HSHQ_VERSION=244 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt 247 ]; then echo "Updating to Version 247..." version247Update HSHQ_VERSION=247 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ $HSHQ_VERSION -lt $HSHQ_LIB_SCRIPT_VERSION ]; then echo "Updating to Version $HSHQ_LIB_SCRIPT_VERSION..." HSHQ_VERSION=$HSHQ_LIB_SCRIPT_VERSION updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION fi if [ "$is_update_performed" = "true" ]; then set +e outputStackListsScriptServer outputAllScriptServerScripts true false resumeCronService set -e fi } function performPreUpdateCheck() { sudo chmod 0444 $HSHQ_LIB_DIR/$HSHQ_NEW_LIB_FILENAME } function promptTestRelayServerPassword() { set +e while [ -z "$USER_RELAY_SUDO_PW" ] do if [ "$IS_CONSOLE_ENV" = "true" ]; then USER_RELAY_SUDO_PW=$(promptPasswordMenu "Enter Relay Password" "Enter the RelayServer sudo password for $RELAYSERVER_REMOTE_USERNAME: ") retVal=$? if [ $retVal -ne 0 ]; then exit 3 fi testRelayServerPassword if [ $? -ne 0 ]; then showMessageBox "Incorrect Password" "The password is incorrect, please re-enter it." USER_RELAY_SUDO_PW="" fi else echo -e "\n\n" read -r -s -p "Enter the RelayServer sudo password for $RELAYSERVER_REMOTE_USERNAME: " USER_RELAY_SUDO_PW echo testRelayServerPassword if [ $? -ne 0 ]; then echo "The password is incorrect, please re-enter it." USER_RELAY_SUDO_PW="" fi fi done } function testRelayServerPassword() { perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo -S -v" -i "$USER_RELAY_SUDO_PW" -r 3 -b 3 > /dev/null 2>&1 retVal=$? return $retVal } function version14Update() { v14_curE=${-//[^e]/} set +e OLDIFS=$IFS IFS=$(echo -en "\n\b") fixlist=($(cat $CONFIG_FILE | grep "^#" | grep -v "# Configuration File" | grep -v "END" | grep -v "BEGIN")) for curitem in "${fixlist[@]}" do sed -i "s|$curitem|$curitem BEGIN|g" $CONFIG_FILE sed -i "s|$curitem BEGIN END|$curitem END|g" $CONFIG_FILE done IFS=$OLDIFS set +e if ! [ -z "$v14_curE" ]; then set -e fi } function version22Update() { updateMOTD set -e showMessageBox "Version 22 Update" "A large update needs to be applied to your system. Please be patient as it will take around 10 minutes to complete. All running stacks will be stopped and then restarted. Press okay to continue." refreshSudo set +e cdns_stack_name="user1" uptimekumaStackID=$(getStackID uptimekuma) startStopStack uptimekuma stop > /dev/null 2>&1 grep "HOMESERVER_HOST_ISPRIVATE_IP" $CONFIG_FILE > /dev/null 2>&1 if [ $? -ne 0 ]; then HOMESERVER_HOST_ISPRIVATE_IP=$(checkIsIPPrivate "$HOMESERVER_HOST_IP") sed -i "s|^HOMESERVER_HOST_IP=.*|HOMESERVER_HOST_IP=$HOMESERVER_HOST_IP\nHOMESERVER_HOST_ISPRIVATE_IP=$HOMESERVER_HOST_ISPRIVATE_IP|g" $CONFIG_FILE fi grep "HOMESERVER_HOST_RANGE" $CONFIG_FILE > /dev/null 2>&1 if [ $? -ne 0 ]; then if [ "$HOMESERVER_HOST_ISPRIVATE_IP" = "true" ]; then HOMESERVER_HOST_RANGE=$(ip route | grep $(getDefaultIface) | grep / | awk '{print $1}' | head -1) else HOMESERVER_HOST_RANGE="${HOMESERVER_HOST_IP}/32" fi sed -i "s|^HOMESERVER_HOST_ISPRIVATE_IP=.*|HOMESERVER_HOST_ISPRIVATE_IP=$HOMESERVER_HOST_ISPRIVATE_IP\nHOMESERVER_HOST_RANGE=$HOMESERVER_HOST_RANGE|g" $CONFIG_FILE fi set -e mailuStackID=$(getStackID mailu) cdnsStackID=$(getStackID clientdns-${cdns_stack_name}) rstackIDs=($(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN" | jq -r '.[] | select(.Status == 1) | .Id')) rstackNames=($(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN" | jq -r '.[] | select(.Status == 1) | .Name')) numItems=$((${#rstackIDs[@]}-1)) for curID in $(seq 0 $numItems); do echo "Stopping ${rstackNames[$curID]} (${rstackIDs[$curID]})..." startStopStackByID ${rstackIDs[$curID]} stop sleep 1 done stopPortainer set +e removeDockerNetworks docker network rm cdns-${cdns_stack_name} > /dev/null 2>&1 set +e grep DOCKER_METRICS_PORT $CONFIG_FILE >/dev/null 2>&1 if [ $? -ne 0 ]; then DOCKER_METRICS_PORT=8323 replace_block="DOCKER_METRICS_PORT=8323\n# Docker Installation Info END" sed -i "s|# Docker Installation Info END|$replace_block|g" $CONFIG_FILE outputDockerSettings fi set -e echo "Restarting Docker..." sudo systemctl restart docker sleep 3 createDockerNetworks outputConfigPortainer startPortainer set +e total_tries=10 num_tries=1 sleep 5 PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" retVal=$? while [ $retVal -ne 0 ] && [ $num_tries -lt $total_tries ] do echo "Error getting portainer token, retrying ($(($num_tries + 1)) of $total_tries)..." sleep 5 ((num_tries++)) PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" retVal=$? done if [ $retVal -ne 0 ]; then echo "Error getting portainer token, exiting..." exit 1 fi if ! [ -z "$cdnsStackID" ]; then docker network create --driver=bridge tmpnet > /dev/null 2>&1 clientdns_subnet=$(getDockerSubnet tmpnet) clientdns_subnet_prefix=$(echo $clientdns_subnet | rev | cut -d "." -f2- | rev) docker network rm tmpnet >/dev/null docker network create -o com.docker.network.bridge.name=brcd-${cdns_stack_name} --driver=bridge --subnet $clientdns_subnet cdns-${cdns_stack_name} > /dev/null 2>&1 fi # Replace Mailu stack is_antivirus_commented_out="" if [ "$(isServiceDisabled clamav)" = "true" ]; then is_antivirus_commented_out="#" fi set -e cat < $HOME/mailu-compose.yml services: front: image: $IMG_MAILU_FRONT container_name: $SMTP_HOSTNAME hostname: $SMTP_HOSTNAME restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true logging: driver: journald options: tag: $SMTP_HOSTNAME networks: - dock-mailu-ext-net - dock-internalmail-net - dock-proxy-net ports: - "$MAILU_PORT_1:$MAILU_PORT_1" - "$MAILU_PORT_2:$MAILU_PORT_2" - "$MAILU_PORT_3:$MAILU_PORT_3" - "$MAILU_PORT_4:$MAILU_PORT_4" - "$MAILU_PORT_5:$MAILU_PORT_5" - "$MAILU_PORT_6:$MAILU_PORT_6" - "$MAILU_PORT_7:$MAILU_PORT_7" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/certs:/certs - \${PORTAINER_HSHQ_SSL_DIR}/mail.crt:/certs/mail.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/mail.key:/certs/mail.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/nginx:/overrides:ro resolver: image: $IMG_MAILU_UNBOUND container_name: mailu-unbound restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: dock-mailu-ext-net: ipv4_address: \${SUBNET_PREFIX}.253 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro redis: image: $IMG_REDIS container_name: mailu-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --appendonly no networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro admin: image: $IMG_MAILU_ADMIN container_name: mailu-admin restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true logging: driver: journald options: tag: mailu-admin networks: - dock-mailu-ext-net depends_on: - redis volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/dkim:/dkim imap: image: $IMG_MAILU_IMAP container_name: mailu-imap restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true logging: driver: journald options: tag: mailu-imap networks: - dock-mailu-ext-net depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/mail:/mail - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/dovecot:/overrides:ro smtp: image: $IMG_MAILU_SMTP container_name: mailu-smtp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true logging: driver: journald options: tag: mailu-smtp networks: - dock-mailu-ext-net depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/mailqueue:/queue - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/certs:/certs - \${PORTAINER_HSHQ_SSL_DIR}/mail.crt:/certs/mail.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/mail.key:/certs/mail.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/postfix:/overrides:ro oletools: image: $IMG_MAILU_OLETOOLS container_name: mailu-oletools restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-int-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro antispam: image: $IMG_MAILU_ANTISPAM container_name: mailu-antispam hostname: antispam restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true logging: driver: journald options: tag: mailu-antispam networks: - dock-mailu-ext-net - dock-mailu-int-net - dock-proxy-net dns: - \${SUBNET_PREFIX}.253 depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/filter:/var/lib/rspamd - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/rspamd:/etc/rspamd/override.d $is_antivirus_commented_out antivirus: $is_antivirus_commented_out image: $IMG_MAILU_ANTIVIRUS $is_antivirus_commented_out container_name: mailu-antivirus $is_antivirus_commented_out restart: unless-stopped $is_antivirus_commented_out env_file: stack.env $is_antivirus_commented_out security_opt: $is_antivirus_commented_out - no-new-privileges:true $is_antivirus_commented_out networks: $is_antivirus_commented_out - dock-mailu-ext-net $is_antivirus_commented_out - dock-proxy-net $is_antivirus_commented_out volumes: $is_antivirus_commented_out - /etc/localtime:/etc/localtime:ro $is_antivirus_commented_out - /etc/timezone:/etc/timezone:ro $is_antivirus_commented_out - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/filter:/data webdav: image: $IMG_MAILU_WEBDAV container_name: mailu-webdav restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/dav:/data fetchmail: image: $IMG_MAILU_FETCHMAIL container_name: mailu-fetchmail restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/data/fetchmail:/data webmail: image: $IMG_MAILU_WEBMAIL container_name: mailu-webmail restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net depends_on: - imap volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/webmail:/data - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/roundcube:/overrides:ro networks: dock-internalmail-net: name: dock-internalmail external: true dock-proxy-net: name: dock-proxy external: true dock-mailu-int-net: name: dock-mailu-int external: true dock-mailu-ext-net: name: dock-mailu-ext external: true EOFMC sudo cp $HSHQ_STACKS_DIR/portainer/compose/$mailuStackID/stack.env $HOME/mailu.env sudo chown $USERNAME:$USERNAME $HOME/mailu.env updateGlobalVarsEnvFile $HOME/mailu.env sed -i "s|^SUBNET=.*|SUBNET=${NET_MAILU_EXT_SUBNET}|g" $HOME/mailu.env sed -i "s|^SUBNET_PREFIX=.*|SUBNET_PREFIX=${NET_MAILU_EXT_SUBNET_PREFIX}|g" $HOME/mailu.env echo "{$( jq -Rscjr '{StackFileContent: . }' $HOME/mailu-compose.yml | tail -c +2 | head -c -1 ),\"Env\":$(envToJson $HOME/mailu.env)}" > $HOME/mailu-json.tmp http --check-status --ignore-stdin --verify=no --timeout=300 PUT https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/$mailuStackID endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/mailu-json.tmp > /dev/null 2>&1 rm $HOME/mailu-compose.yml $HOME/mailu.env $HOME/mailu-json.tmp if ! [ -z "$cdnsStackID" ]; then # Replace ClientDNS stack cat < $HOME/clientdns-${cdns_stack_name}-compose.yml services: clientdns-${cdns_stack_name}-dnsmasq: image: $IMG_DNSMASQ container_name: clientdns-${cdns_stack_name}-dnsmasq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: dock-ext-net: dock-proxy-net: cdns-${cdns_stack_name}-net: ipv4_address: \${CLIENTDNS_SUBNET_PREFIX}.253 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/clientdns-${cdns_stack_name}/dnsmasq.conf:/etc/dnsmasq.conf environment: - HTTP_USER=$CLIENTDNS_USER1_ADMIN_USERNAME - HTTP_PASS=$CLIENTDNS_USER1_ADMIN_PASSWORD clientdns-${cdns_stack_name}-wireguard: image: $IMG_WIREGUARD container_name: clientdns-${cdns_stack_name}-wireguard hostname: clientdns-${cdns_stack_name}-wireguard restart: unless-stopped env_file: stack.env cap_add: - NET_ADMIN networks: - dock-ext-net - cdns-${cdns_stack_name}-net sysctls: - net.ipv4.conf.all.src_valid_mark=1 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/clientdns-${cdns_stack_name}/Corefile:/config/coredns/Corefile:ro - \${PORTAINER_HSHQ_STACKS_DIR}/clientdns-${cdns_stack_name}/clientdns-${cdns_stack_name}.conf:/config/wg0.conf networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true cdns-${cdns_stack_name}-net: name: cdns-${cdns_stack_name} external: true EOFGL rm -f $HSHQ_STACKS_DIR/clientdns-${cdns_stack_name}/Corefile cat < $HSHQ_STACKS_DIR/clientdns-${cdns_stack_name}/Corefile . { loop reload 15s forward . ${clientdns_subnet_prefix}.253 } EOFCF sudo cp $HSHQ_STACKS_DIR/portainer/compose/$cdnsStackID/stack.env $HOME/clientdns-${cdns_stack_name}.env sudo chown $USERNAME:$USERNAME $HOME/clientdns-${cdns_stack_name}.env updateGlobalVarsEnvFile $HOME/clientdns-${cdns_stack_name}.env sed -i "s|^CLIENTDNS_SUBNET_PREFIX=.*|CLIENTDNS_SUBNET_PREFIX=${clientdns_subnet_prefix}|g" $HOME/clientdns-${cdns_stack_name}.env echo "{$( jq -Rscjr '{StackFileContent: . }' $HOME/clientdns-${cdns_stack_name}-compose.yml | tail -c +2 | head -c -1 ),\"Env\":$(envToJson $HOME/clientdns-${cdns_stack_name}.env)}" > $HOME/clientdns-${cdns_stack_name}-json.tmp http --check-status --ignore-stdin --verify=no --timeout=300 PUT https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/$cdnsStackID endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/clientdns-${cdns_stack_name}-json.tmp > /dev/null 2>&1 rm $HOME/clientdns-${cdns_stack_name}-compose.yml $HOME/clientdns-${cdns_stack_name}.env $HOME/clientdns-${cdns_stack_name}-json.tmp fi rstackIDs[$(($numItems+1))]=$uptimekumaStackID rstackNames[$(($numItems+1))]=uptimekuma numItems=$((${#rstackIDs[@]}-1)) for curID in $(seq 0 $numItems); do case ${rstackNames[$curID]} in mailu|clientdns-${cdns_stack_name}) continue ;; *) echo "Starting ${rstackNames[$curID]} (${rstackIDs[$curID]})..." if sudo test -f $HSHQ_STACKS_DIR/portainer/compose/${rstackIDs[$curID]}/stack.env; then sudo cp $HSHQ_STACKS_DIR/portainer/compose/${rstackIDs[$curID]}/stack.env $HOME/${rstackIDs[$curID]}.env sudo chown $USERNAME:$USERNAME $HOME/${rstackIDs[$curID]}.env updateGlobalVarsEnvFile $HOME/${rstackIDs[$curID]}.env fi sudo cp $HSHQ_STACKS_DIR/portainer/compose/${rstackIDs[$curID]}/docker-compose.yml $HOME/${rstackIDs[$curID]}.yml sudo chown $USERNAME:$USERNAME $HOME/${rstackIDs[$curID]}.yml ;; esac case ${rstackNames[$curID]} in caddy-home) sed -i "s|$HOMESERVER_HOST_IP|\${HOMESERVER_HOST_IP}|g" $HOME/${rstackIDs[$curID]}.yml sed -i "s|^CADDY_HSHQ_PRIVATE_IPS=.*|CADDY_HSHQ_PRIVATE_IPS=\${HOMESERVER_HOST_RANGE} $(getPrivateIPRangesCaddy)|g" $HOME/${rstackIDs[$curID]}.env ;; jitsi) sed -i "s|^DOCKER_HOST_ADDRESS=.*|DOCKER_HOST_ADDRESS=\${HOMESERVER_HOST_IP}|g" $HOME/${rstackIDs[$curID]}.env JITSI_ADVERTISE_IPS=$(echo "$JITSI_ADVERTISE_IPS" | sed "s|$HOMESERVER_HOST_IP||g") updateConfigVar JITSI_ADVERTISE_IPS $JITSI_ADVERTISE_IPS sed -i "s|^JVB_ADVERTISE_IPS=.*|JVB_ADVERTISE_IPS=\${HOMESERVER_HOST_IP}$JITSI_ADVERTISE_IPS|g" $HOME/${rstackIDs[$curID]}.env sed -i '/^CONFIG=/d' $HOME/${rstackIDs[$curID]}.env ;; esac if ! [ -f $HOME/${rstackIDs[$curID]}.env ] || ! [ -s $HOME/${rstackIDs[$curID]}.env ]; then echo "TZ=\${TZ}" > $HOME/${rstackIDs[$curID]}.env fi echo "{$( jq -Rscjr '{StackFileContent: . }' $HOME/${rstackIDs[$curID]}.yml | tail -c +2 | head -c -1 ),\"Env\":$(envToJson $HOME/${rstackIDs[$curID]}.env)}" > $HOME/${rstackIDs[$curID]}-json.tmp num_tries=1 while [ $num_tries -lt $total_tries ] do http --check-status --ignore-stdin --verify=no --timeout=300 PUT https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks/${rstackIDs[$curID]} endpointId==$PORTAINER_ENDPOINT_ID "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/${rstackIDs[$curID]}-json.tmp > /dev/null if [ $? -eq 0 ]; then break else echo "Failed update, retrying ($(($num_tries + 1)) of $total_tries)..." sleep 5 PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi ((num_tries++)) done rm -f $HOME/${rstackIDs[$curID]}.yml $HOME/${rstackIDs[$curID]}.env $HOME/${rstackIDs[$curID]}-json.tmp sleep 5 done docker container restart ofelia set -e } function version23Update() { outputBootScripts deleteFromRootCron "restartHomeAssistantStack.sh" appendToRoonCron "@reboot bash $HSHQ_SCRIPTS_DIR/root/restartHomeAssistantStack.sh >/dev/null 2>&1" updateSysctl true } function version24Update() { initServicesCredentials } function version25Update() { sudo systemctl stop systemd-resolved > /dev/null 2>&1 sudo systemctl disable systemd-resolved > /dev/null 2>&1 sudo systemctl mask systemd-resolved > /dev/null 2>&1 sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null </dev/null < $HOME/custom.inc.php EOFRO sudo chown root:root $HOME/custom.inc.php sudo mv $HOME/custom.inc.php $HSHQ_STACKS_DIR/mailu/overrides/roundcube/custom.inc.php docker container restart mailu-webmail } function version29Update() { # This should have been addressed in the version 23 update, # but the metrics port was not correctly added to the config. # Add Docker metrics port to config set +e grep DOCKER_METRICS_PORT $CONFIG_FILE >/dev/null 2>&1 if [ $? -ne 0 ]; then DOCKER_METRICS_PORT=8323 replace_block="DOCKER_METRICS_PORT=8323\n# Docker Installation Info END" sed -i "s|# Docker Installation Info END|$replace_block|g" $CONFIG_FILE outputDockerSettings fi set -e outputBootScripts setVersionOnStacks echo -e "\n\n========================================" echo -e " Stack Versioning Results" echo -e "========================================" if [ -z "$strSetVersionReport" ]; then echo -e "All stacks were correctly versioned." else echo -e "$strSetVersionReport" echo -e "\n========================================" echo -e "\nCheck your email($EMAIL_ADMIN_EMAIL_ADDRESS) for a copy of these results." sendEmail -s "Stack Versioning Report" -b "$strSetVersionReport" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -t $EMAIL_ADMIN_EMAIL_ADDRESS fi } function version30Update() { # Sidekiq should be mounted on same volume. Bitnami configured it incorrectly. (https://github.com/bitnami/containers/blob/main/bitnami/discourse/docker-compose.yml) discourseStackID=$(getStackID discourse) if ! [ -z "$discourseStackID" ]; then sudo sed -i "s|v-sidekiq-data:\/bitnami\/discourse|v-discourse-data:\/bitnami\/discourse|" $HSHQ_STACKS_DIR/portainer/compose/$discourseStackID/docker-compose.yml fi } function version34Update() { sudo sed -i "/timestamp_timeout/a Defaults passwd_tries=$SUDO_MAX_RETRIES" /etc/sudoers } function version35Update() { updateMOTD } function version37Update() { sed -i "s|Relay Server|RelayServer|g" $CONFIG_FILE CHANGEDETECTION_INIT_ENV=false SCRIPTSERVER_INIT_ENV=false SCRIPTSERVER_LOCALHOST_PORT=8008 sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing python, please wait..." performAptInstall python3 > /dev/null 2>&1 echo "Installing python-pip, please wait..." performAptInstall python3-pip > /dev/null 2>&1 echo "Installing unzip, please wait..." performAptInstall unzip > /dev/null 2>&1 getUpdateAssets set +e outputBootScripts grep $SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN $HSHQ_STACKS_DIR/authelia/config/configuration.yml > /dev/null 2>&1 if [ $? -ne 0 ]; then sed -i "/$SUB_ADGUARD.$HOMESERVER_DOMAIN/a\ - $SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN" $HSHQ_STACKS_DIR/authelia/config/configuration.yml docker container restart authelia > /dev/null 2>&1 fi if ! [ -d $HSHQ_STACKS_DIR/script-server ]; then installStackByName script-server fi mkdir -p $HSHQ_WIREGUARD_DIR/requestkeys checkFixPortainerEnv } function version38Update() { refreshSudo sudo sqlite3 $HSHQ_DB "create table if not exists exposedomains(Domain text primary key,BaseDomain text not null);" if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then sendRSExposeScripts fi # Fixes internal mailing issue startStopStack mailu stop cat < $HOME/mailu-groups.conf symbols { "R_DKIM_ALLOW" { weight = -22.0; } } EOFRS chmod 664 $HOME/mailu-groups.conf sudo chown 101:101 $HOME/mailu-groups.conf sudo mv $HOME/mailu-groups.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/groups.conf # Also need to add host IP to Postfix and RSpamd (this should have been done in v27) set +e sudo grep "$HOMESERVER_HOST_IP" $HSHQ_STACKS_DIR/mailu/overrides/postfix/postfix.cf > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo sed -i "/^mynetworks/ s/$/ ${HOMESERVER_HOST_IP}\/32/" $HSHQ_STACKS_DIR/mailu/overrides/postfix/postfix.cf fi sudo grep "$HOMESERVER_HOST_IP" $HSHQ_STACKS_DIR/mailu/overrides/rspamd/ip_whitelist.map > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "${HOMESERVER_HOST_IP}/32" | sudo tee -a $HSHQ_STACKS_DIR/mailu/overrides/rspamd/ip_whitelist.map > /dev/null 2>&1 fi sleep 3 startStopStack mailu start sleep 5 set -e HUGINN_INIT_ENV=false } function version39Update() { set +e clearAllScriptServerScripts set -e sudo sqlite3 $HSHQ_DB "create table newconnections(ID integer not null primary key autoincrement,Name text,EmailAddress text,ConnectionType text,NetworkType text,PublicKey text,PresharedKey text,IPAddress text,IsInternet boolean,InterfaceName text,EndpointHostname text,EndpointIP text default null,LastUpdated datetime);" sudo sqlite3 $HSHQ_DB "insert into newconnections(ID,Name,EmailAddress,ConnectionType,NetworkType,PublicKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,EndpointIP,LastUpdated) select ID,Name,EmailAddress,ConnectionType,NetworkType,PublicKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,EndpointIP,LastUpdated from connections;" sudo sqlite3 $HSHQ_DB "drop table connections;" sudo sqlite3 $HSHQ_DB "ALTER TABLE newconnections RENAME TO connections;" if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then sendRSExposeScripts keylist=($(perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo sqlite3 $RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal/wg_portal.db \"select public_key,preshared_key from peers;\"" -f -d)) for curKeys in "${keylist[@]}" do pub_key=$(echo "$curKeys" | cut -d "|" -f1) pre_key=$(echo "$curKeys" | cut -d "|" -f2) sudo sqlite3 $HSHQ_DB "update connections set PresharedKey='$pre_key' where PublicKey='$pub_key';" done sudo sqlite3 $HSHQ_DB "update connections set PresharedKey='$RELAYSERVER_WG_SV_PRESHAREDKEY' where Name='WireGuardServer';" fi hs_vpn=($(sqlite3 $HSHQ_DB "select ID,InterfaceName from connections where NetworkType='other' and ConnectionType='homeserver_vpn';")) for curVPN in "${hs_vpn[@]}" do cur_id=$(echo "$curVPN" | cut -d "|" -f1) cur_iface=$(echo "$curVPN" | cut -d "|" -f2) pre_key=$(sudo grep "^PresharedKey" $HSHQ_WIREGUARD_DIR/vpn/${cur_iface}.conf | cut -d " " -f3) sudo sqlite3 $HSHQ_DB "update connections set PresharedKey='$pre_key' where ID='$cur_id';" done hs_int=($(sqlite3 $HSHQ_DB "select ID,InterfaceName from connections where NetworkType='other' and ConnectionType='homeserver_internet';")) for curInt in "${hs_int[@]}" do cur_id=$(echo "$curInt" | cut -d "|" -f1) cur_iface=$(echo "$curInt" | cut -d "|" -f2) pre_key=$(sudo grep "^PresharedKey" $HSHQ_WIREGUARD_DIR/internet/${cur_iface}.conf | cut -d " " -f3) sudo sqlite3 $HSHQ_DB "update connections set PresharedKey='$pre_key' where ID='$cur_id';" done sudo sqlite3 $HSHQ_DB "update connections set IsInternet=true where IsInternet='true';" sudo sqlite3 $HSHQ_DB "update connections set IsInternet=false where IsInternet='false';" sudo chmod 640 $HSHQ_DB sudo chown root $HSHQ_DB } function version40Update() { set +e clearAllScriptServerScripts set -e } function version41Update() { set +e if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\n\nThe RelayServer requires an update which requires root privileges.\nThis update will also reboot the RelayServer.\nYou will be prompted for you sudo password on the RelayServer.\n" read -r -p "Press enter to continue." rs_default_iface=$(echo $(perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "ip route | grep -e \"^default\"" -f -d) | awk -F'dev ' '{print $2}' | xargs | cut -d" " -f1) tee $HOME/wgupdown.sh >/dev/null </dev/null </dev/null if [ \$? -ne 0 ]; then echo "There was a problem adding this client..." exit 1 fi ipset add alldevices \${ipAddress}/32 if ! [ "\$isInVPNSubnet" = "true" ]; then route add \${ipAddress}/32 $RELAYSERVER_WG_INTERFACE_NAME fi if [ "\$isInet" = "true" ]; then ipset add inetusers \${ipAddress}/32 echo \${ipAddress}/32 | tee -a $RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/inetusers.ipset >/dev/null fi EOFWA tee $HOME/removePeer.sh >/dev/null </dev/null if [ \$? -ne 0 ]; then echo "There was a problem removing this client..." fi ipset del alldevices \${ipAddress}/32 if ! [ "\$isInVPNSubnet" = "true" ]; then route del \${ipAddress}/32 $RELAYSERVER_WG_INTERFACE_NAME fi if [ "\$isInet" = "true" ]; then ipset del inetusers \${ipAddress}/32 sed -i '/\${ipAddress}/d' $RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/inetusers.ipset fi } function urlEncode() { strLength="\${#1}" for (( i = 0; i < strLength; i++ )); do c="\${1:i:1}" case \$c in [a-zA-Z0-9.~_-]) printf "\$c" ;; *) printf '%%%02X' "'\$c" ;; esac done } function urlDecode() { url_encoded="\${1//+/ }" printf '%b' "\${url_encoded//%/\\x}" } main "\$@" EOFWA tee $HOME/v41.sh >/dev/null < /dev/null 2>&1 installStackByName script-server releaseLock hshqopen "version46Update" false sudo systemctl disable runHSHQManager sudo systemctl stop runHSHQManager echo -e "\n\nPlease restart the hshq.sh script to complete the update.\n" exit fi } function version48Update() { set +e grep HOMEASSISTANT_CONFIGURATOR_USER $CONFIG_FILE >/dev/null 2>&1 if [ $? -ne 0 ]; then initServicesCredentials # Email to user sendEmail -s "HomeAssistant Configurator Login Info" -b "HomeAssistant Configurator Username: $HOMEASSISTANT_CONFIGURATOR_USER\nHomeAssistant Configurator Password: $HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" fi if [ -f $HSHQ_STACKS_DIR/homeassistant/configurator/settings.conf ]; then grep "USERNAME" $HSHQ_STACKS_DIR/homeassistant/configurator/settings.conf >/dev/null 2>&1 if [ $? -ne 0 ]; then hc_pw_hash=$(echo -n $HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD | openssl dgst -sha256 | cut -d"=" -f2 | xargs) sed -i "/BASEPATH.*/a\ \"HASS_API\": \"\",\n \"USERNAME\": \"$HOMEASSISTANT_CONFIGURATOR_USER\",\n \"PASSWORD\": \"{sha256}$hc_pw_hash\"," $HSHQ_STACKS_DIR/homeassistant/configurator/settings.conf deleteSvcUptimeKuma "https://$SUB_HOMEASSISTANT_CONFIGURATOR.$HOMESERVER_DOMAIN" true fi echo "Restarting HomeAssistant stack (if running)..." restartStackIfRunning homeassistant 10 fi } function version52Update() { set +e if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\n\nThe RelayServer requires an update which requires root privileges.\nYou will be prompted for you sudo password on the RelayServer.\n" notifyRSLogin set +e rs_default_iface=$(echo $(perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "ip route | grep -e \"^default\"" -f -d) | awk -F'dev ' '{print $2}' | xargs | cut -d" " -f1) tee $HOME/88-hshq.conf >/dev/null </dev/null < /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \$cur_port --ctdir ORIGINAL -j DROP 2> /dev/null iptables -I DOCKER-USER -m conntrack --ctorigdstport \$cur_port --ctdir ORIGINAL -j DROP iptables -I DOCKER-USER -s 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 -m conntrack --ctorigdstport \$cur_port --ctdir ORIGINAL -j ACCEPT done # See https://gist.github.com/mattia-beta/bd5b1c68e3d51db933181d8a3dc0ba64?permalink_comment_id=3728715#gistcomment-3728715 iptables -t raw -F PREROUTING iptables -t raw -F chain-icmp > /dev/null 2>&1 iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 iptables -t raw -X chain-icmp > /dev/null 2>&1 iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 iptables -t raw -N chain-icmp > /dev/null 2>&1 iptables -t raw -N chain-bad_tcp > /dev/null 2>&1 iptables -t raw -A PREROUTING -s 192.0.0.0/24,192.0.2.0/24,198.51.100.0/24,203.0.113.0/24 -j DROP iptables -t raw -A PREROUTING -d 192.0.0.0/24,192.0.2.0/24,198.51.100.0/24,203.0.113.0/24 -j DROP iptables -t raw -A PREROUTING -d 0.0.0.0/8 -j DROP iptables -t raw -A PREROUTING -s 0.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,224.0.0.0/4,198.18.0.0/15,100.64.0.0/10,192.88.99.0/24 -i \$default_iface -j DROP iptables -t raw -A PREROUTING -s 127.0.0.0/8 ! -i lo -j DROP iptables -t raw -A PREROUTING -p udp -m udp -m multiport --ports 0 -j DROP iptables -t raw -A PREROUTING -p icmp -j chain-icmp iptables -t raw -A PREROUTING -p tcp -m tcp -j chain-bad_tcp iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-reply -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type network-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type host-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type protocol-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type port-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type fragmentation-needed -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-request -i \$default_iface -j DROP iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-request -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type time-exceeded -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type parameter-problem -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type any -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN FIN,SYN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,RST SYN,RST -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,RST FIN,RST -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,ACK FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,URG URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,FIN FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,PSH PSH -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL ALL -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL NONE -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp -m tcp -m multiport --ports 0 -j DROP iptables -t raw -P PREROUTING ACCEPT # Configure loopback iptables -C INPUT -i lo -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -i lo -j ACCEPT iptables -C INPUT ! -i lo -s 127.0.0.0/8 -j DROP > /dev/null 2>&1 || iptables -A INPUT ! -i lo -s 127.0.0.0/8 -j DROP # Limit connections per source IP iptables -C INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset > /dev/null 2>&1 || iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset # Allow established connections iptables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Drop invalid packets iptables -C INPUT -m conntrack --ctstate INVALID -j DROP > /dev/null 2>&1 || iptables -A INPUT -m conntrack --ctstate INVALID -j DROP # Drop fragments iptables -C INPUT -f -j DROP > /dev/null 2>&1 || iptables -A INPUT -f -j DROP # Drop SYN packets with suspicious MSS value iptables -C INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP > /dev/null 2>&1 || iptables -A INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP # Allow ICMP iptables -C INPUT -p icmp -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p icmp -j ACCEPT # Allow SSH iptables -C INPUT -p tcp -m tcp --dport \$SSH_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p tcp -m tcp --dport \$SSH_PORT -j ACCEPT # Allow WireGuard iptables -C INPUT -p udp --dport \$WG_CON_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p udp --dport \$WG_CON_PORT -j ACCEPT # Special case for WG Portal from reverse proxy iptables -C INPUT -p tcp -m tcp -i brdockext -s \$DOCK_EXT_NET --dport \$WG_PORTAL_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p tcp -m tcp -i brdockext -s \$DOCK_EXT_NET --dport \$WG_PORTAL_PORT -j ACCEPT # Policy drop for input and forward iptables -P INPUT DROP iptables -P FORWARD DROP # Policy drop all ipv6 traffic ip6tables -P INPUT DROP ip6tables -P FORWARD DROP ip6tables -P OUTPUT DROP sysctl --system > /dev/null 2>&1 EOFSC chmod 744 $HOME/10-setupDockerUserIPTables.sh tee $HOME/clearDockerUserIPTables.sh >/dev/null < /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \$cur_port --ctdir ORIGINAL -j DROP 2> /dev/null done iptables -D INPUT -f -j DROP > /dev/null 2>&1 iptables -D INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP > /dev/null 2>&1 iptables -D INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset > /dev/null 2>&1 iptables -D INPUT -m conntrack --ctstate INVALID -j DROP > /dev/null 2>&1 iptables -D INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -i lo -j ACCEPT > /dev/null 2>&1 iptables -D INPUT ! -i lo -s 127.0.0.0/8 -j DROP > /dev/null 2>&1 iptables -D INPUT -p icmp -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -p tcp -m tcp --dport \$SSH_PORT -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -p udp --dport \$WG_CON_PORT -j ACCEPT > /dev/null 2>&1 iptables -D INPUT -p tcp -m tcp -i brdockext -s \$DOCK_EXT_NET --dport \$WG_PORTAL_PORT -j ACCEPT > /dev/null 2>&1 iptables -t raw -F PREROUTING iptables -t raw -F chain-icmp > /dev/null 2>&1 iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 iptables -t raw -X chain-icmp > /dev/null 2>&1 iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 iptables -P INPUT ACCEPT ip6tables -P INPUT ACCEPT ip6tables -P FORWARD ACCEPT ip6tables -P OUTPUT ACCEPT EOFSC chmod 744 $HOME/clearDockerUserIPTables.sh perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a $HOME/88-hshq.conf -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":~" -f perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a $HOME/10-setupDockerUserIPTables.sh -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":~" -f perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a $HOME/clearDockerUserIPTables.sh -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":~" -f perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo chown root:root ~/88-hshq.conf; sudo chown root:root ~/10-setupDockerUserIPTables.sh; sudo chown root:root ~/clearDockerUserIPTables.sh; sudo mv ~/88-hshq.conf /etc/sysctl.d/88-hshq.conf; sudo mv ~/10-setupDockerUserIPTables.sh $RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh; sudo rm -f $RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/setupDockerUserIPTables.sh; sudo mv ~/clearDockerUserIPTables.sh $RELAYSERVER_HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh; sudo apt-mark hold docker-ce; sudo apt-mark hold docker-ce-cli; sudo reboot" -f -i "$USER_RELAY_SUDO_PW" rm -f $HOME/88-hshq.conf rm -f $HOME/10-setupDockerUserIPTables.sh rm -f $HOME/clearDockerUserIPTables.sh fi set -e updateSysctl false # See https://www.portainer.io/blog/portainer-and-docker-26 sudo apt-mark hold docker-ce sudo apt-mark hold docker-ce-cli } function version53Update() { set +e outputMaintenanceScripts outputPerformPostDockerBootActionsScript deleteFromRootCron "restartHomeAssistantStack.sh" sudo rm -f $HSHQ_SCRIPTS_DIR/root/restartHomeAssistantStack.sh sudo rm -f $HSHQ_SCRIPTS_DIR/boot/onBootRoot.sh sudo tee $HSHQ_SCRIPTS_DIR/boot/onBootRoot.sh >/dev/null < /dev/null 2>&1 if [ $? -ne 0 ]; then # Reinstall coturn with new ports echo -e "\n\n\nCoturn must be reinstalled due to a change in the port range.\nThis will result in the reverse proxy (Caddy) being restarted,\nand this web page will desync as a result. Ensure to refresh \nthe web page when this occurs, in order to continue the update." notifyRSLogin deleteListOfStacks true coturn installListOfServices coturn fi fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\n\nThe RelayServer requires an update which requires root privileges.\nThis update will also reboot the RelayServer.\nYou will be prompted for you sudo password on the RelayServer.\n" notifyRSLogin tee $HOME/onBootRoot.sh >/dev/null < /dev/null 2>&1 || sudo iptables -A INPUT -p tcp -m tcp -i $default_iface -s $HOMESERVER_HOST_RANGE --dport $SCRIPTSERVER_LOCALHOST_PORT -j ACCEPT generateCert script-server "script-server,host.docker.internal,localhost" "127.0.0.1,$HOMESERVER_HOST_IP" insertEnableSvcHeimdall script-server "$FMLNAME_SCRIPTSERVER (IP)" $USERTYPE_SCRIPTSERVER "https://$HOMESERVER_HOST_IP:$SCRIPTSERVER_LOCALHOST_PORT" "script-server.png" true "$(getHeimdallOrderFromSub $SUB_SCRIPTSERVER $USERTYPE_SCRIPTSERVER)" HSHQ_VERSION=54 updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION performExitFunctions sudo systemctl restart runScriptServer exit } function version55Update() { set +e outputWireGuardScripts grep "HOMENET_ADDITIONAL_IPS" $CONFIG_FILE > /dev/null 2>&1 if [ $? -ne 0 ]; then sed -i "s|^HOMESERVER_HOST_RANGE=.*|HOMESERVER_HOST_RANGE=$HOMESERVER_HOST_RANGE\nHOMENET_ADDITIONAL_IPS=|g" $CONFIG_FILE fi if [ "$HOMESERVER_HOST_ISPRIVATE_IP" = "false" ] && ! [ -z "$CONNECTING_IP" ]; then addHomeNetIP ${CONNECTING_IP}/32 false fi if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then addHomeNetIP ${RELAYSERVER_SERVER_IP}/32 false fi if [ "$HOMESERVER_HOST_ISPRIVATE_IP" = "false" ]; then # Add special rules when HomeServer is on non-private network, i.e. cloud-server, etc. if ! [ -z "$HOMENET_ADDITIONAL_IPS" ]; then default_iface=$(getDefaultIface) sudo iptables -C INPUT -p tcp -m tcp -i $default_iface -s $HOMENET_ADDITIONAL_IPS --dport $SCRIPTSERVER_LOCALHOST_PORT -j ACCEPT > /dev/null 2>&1 || sudo iptables -A INPUT -p tcp -m tcp -i $default_iface -s $HOMENET_ADDITIONAL_IPS --dport $SCRIPTSERVER_LOCALHOST_PORT -j ACCEPT fi fi set -e } function version56Update() { performSuggestedSecUpdates sudo systemctl restart sshd } function version59Update() { # This helps with sending internal emails within a primary network. cat < $HOME/mailu-groups.conf group "dkim" { symbols = { "R_DKIM_ALLOW" = { weight = -6.0; } } } EOFRS chmod 664 $HOME/mailu-groups.conf sudo chown 101:101 $HOME/mailu-groups.conf sudo mv $HOME/mailu-groups.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/groups.conf docker container restart mailu-antispam > /dev/null 2>&1 if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\n\nThe RelayServer requires an update which requires root privileges.\nYou will be prompted for you sudo password on the RelayServer.\n" notifyRSLogin cat < $HOME/groups.conf group "policies" { symbols = { "DMARC_POLICY_REJECT" = { weight = 100.0; } } } EOFHC chmod 644 $HOME/groups.conf perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a $HOME/groups.conf -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":~" -f perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo chown root:root ~/groups.conf; sudo mv ~/groups.conf $RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/rspamd/conf/groups.conf; docker container restart mail-relay-rspamd > /dev/null 2>&1" -f -i "$USER_RELAY_SUDO_PW" rm -f $HOME/groups.conf fi set +e cp $HSHQ_ASSETS_DIR/images/hslogo.png $HSHQ_ASSETS_DIR/images/${HOMESERVER_DOMAIN}.png } function version64Update() { set +e clearAllScriptServerScripts set -e } function version65Update() { # Fixes extra leading slash in "map =" entries cat < $HOME/multimap.conf IP_WHITELIST { type = "ip"; prefilter = true; map = "\${LOCAL_CONFDIR}/override.d/ip_whitelist.map"; action = "accept"; } IP_BLACKLIST { type = "ip"; prefilter = true; map = "\${LOCAL_CONFDIR}/override.d/ip_blacklist.map"; action = "reject"; } DOMAIN_WHITELIST { regexp = true; type = "from"; filter = "email:domain"; map = "\${LOCAL_CONFDIR}/override.d/domain_whitelist.map"; score = -100.0; action = "accept"; } DOMAIN_BLACKLIST { regexp = true; type = "from"; filter = "email:domain"; map = "\${LOCAL_CONFDIR}/override.d/domain_blacklist.map"; score = 100.0; action = "reject"; } EOFRS sudo mv $HOME/multimap.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/multimap.conf docker container restart mailu-antispam } function version66Update() { set +e grep "base target" $HSHQ_STACKS_DIR/script-server/web/index.html > /dev/null 2>&1 if [ $? -ne 0 ]; then sed -i "s|<\/title>|<\/title>|" $HSHQ_STACKS_DIR/script-server/web/index.html fi set -e } function version67Update() { if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "chmod 600 $RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal/config.yml" -f fi } function version68Update() { if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\n\nThe RelayServer requires an update which requires root privileges.\nYou will be prompted for you sudo password on the RelayServer.\n" notifyRSLogin tee $HOME/resetCaddyContainer.sh >/dev/null < /dev/null 2>&1 } function version72Update() { mkdir -p $HSHQ_WIREGUARD_DIR/logs outputWGDockInternetScript outputPerformPostDockerBootActionsScript sudo systemctl enable networkd-dispatcher > /dev/null 2>&1 sudo systemctl start networkd-dispatcher > /dev/null 2>&1 for conf in $HSHQ_WIREGUARD_DIR/internet/*.conf do if ! test -f "$conf"; then continue; fi cur_ext_dom=$(sudo grep "^#EXT_DOMAIN=" $conf | sed 's/^[^=]*=//' | sed 's/ *\$//g') if ! [[ $cur_ext_dom =~ ^$RELAYSERVER_SUB_WG.* ]]; then sudo sed -i "s|^#EXT_DOMAIN=.*|#EXT_DOMAIN=${RELAYSERVER_SUB_WG}\.${cur_ext_dom}|g" $conf fi done } function version73Update() { updateStackEnv heimdall outputHeimdallCompose docker container restart ofelia > /dev/null 2>&1 } function version75Update() { if [ -d $HSHQ_STACKS_DIR/wazuh ]; then sudo apt update sudo apt install -y --only-upgrade wazuh-agent=4.7.5-1 sudo apt-mark hold wazuh-agent if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\n\nThe RelayServer requires an update which requires root privileges.\nYou will be prompted for you sudo password on the RelayServer.\n" notifyRSLogin perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo apt update; sudo apt install -y --only-upgrade wazuh-agent=4.7.5-1; sudo apt-mark hold wazuh-agent" -f -i "$USER_RELAY_SUDO_PW" fi fi } function version76Update() { if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then outputRelayServerInstallSetupScript outputRelayServerInstallTransferScript fi } function version78Update() { outputPerformPostDockerBootActionsScript if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo -e "\n\nUpdating the mail relay postfix image on the RelayServer." set +e notifyRSLogin nrsl_retVal=$? set -e if [ $nrsl_retVal -eq 0 ]; then promptTestRelayServerPassword perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "sudo -S -v; git clone https://github.com/homeserverhq/mail-relay.git $RELAYSERVER_HSHQ_NONBACKUP_DIR/build/mail-relay; docker image build --network host -t $IMG_MAIL_RELAY_POSTFIX -f $RELAYSERVER_HSHQ_NONBACKUP_DIR/build/mail-relay/postfix/Dockerfile $RELAYSERVER_HSHQ_NONBACKUP_DIR/build/mail-relay/postfix; sudo rm -fr $RELAYSERVER_HSHQ_NONBACKUP_DIR/build/mail-relay" -f -i "$USER_RELAY_SUDO_PW" fi fi } function version80Update() { set +e clearAllScriptServerScripts set -e } function version83Update() { set +e clearAllScriptServerScripts set -e } function version84Update() { set +e outputWGDockInternetScript outputPerformPostDockerBootActionsScript set -e } function version85Update() { set +e updateLogrotateConfig if sudo test -f $HSHQ_STACKS_DIR/wazuh/volumes/etc/shared/default/agent.conf; then sudo sed -i "s/.*Shared agent configuration here.*/\ \n \/var\/lib\/docker\/overlay2<\/ignore>\n <\/rootcheck>/" $HSHQ_STACKS_DIR/wazuh/volumes/etc/shared/default/agent.conf fi if [ -d $HSHQ_STACKS_DIR/wazuh ]; then echo "Updating wazuh stack..." updateStackEnv wazuh mfFixCACertPath fi echo "Updating authelia stack..." updateStackEnv authelia mfFixCACertPath updateSysctl true set -e } function version86Update() { # Move the priorities in the RPDB from 10,20 to 1000,2000 # to give space for other changes that someone might want to apply. # Also, removed entries for private IP ranges (priority 15), as they are superfluous. set +e outputWGDockInternetScript outputMaintenanceScripts set +e sudo ip rule delete priority 10 > /dev/null 2>&1 sudo ip rule delete priority 15 > /dev/null 2>&1 while [ $? -eq 0 ] do sudo ip rule delete priority 15 > /dev/null 2>&1 done sudo ip rule delete priority 20 > /dev/null 2>&1 while [ $? -eq 0 ] do sudo ip rule delete priority 20 > /dev/null 2>&1 done wgDockInternetUpAll set -e } function version87Update() { if [ -d $HSHQ_STACKS_DIR/paperless/redis ]; then # Move paperless redis to nonbackup directory echo "Updating paperless stack..." # Stack status: 1=running, 2=stopped stackStatus=$(getStackStatusByName paperless) mkdir -p $HSHQ_NONBACKUP_DIR/paperless mkdir -p $HSHQ_NONBACKUP_DIR/paperless/redis if [ "$stackStatus" = "1" ]; then startStopStack paperless stop fi updateStackEnv paperless mvFixRedisDir sudo rm -fr $HSHQ_STACKS_DIR/paperless/redis if ! [ "$stackStatus" = "1" ]; then startStopStack paperless stop fi fi } function version91Update() { set +e refreshSudo setupPortForwardingDB version91UploadPortForwardingScripts version91WazuhUpdate set -e } function version98Update() { set +e initServicesCredentials outputPerformPostDockerBootActionsScript set -e } function version102Update() { set +e initServicesCredentials set -e } function version105Update() { # Seriously, who replies to emails underneath rather than above? set +e sudo grep reply_mode $HSHQ_STACKS_DIR/mailu/overrides/roundcube/custom.inc.php > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo sed -i "s|?>|\$config['reply_mode'] = 1;\n?>|" $HSHQ_STACKS_DIR/mailu/overrides/roundcube/custom.inc.php docker container restart mailu-webmail > /dev/null 2>&1 fi set -e } function version106Update() { # Adding password auth to Wazuh manager for agent enrollment # Excellent article - https://neodyme.io/en/blog/wazuh_rce/#the-target-wazuh if [ -d /var/ossec/etc ]; then sudo tee /var/ossec/etc/authd.pass >/dev/null </dev/null <no<\/use_password>/yes<\/use_password>/" $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf docker container restart wazuh.manager > /dev/null 2>&1 fi set +e if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then set +e promptTestRelayServerPassword set -e tee $HOME/authd.pass >/dev/null < $HOME/$RS_UPDATE_SCRIPT_NAME #!/bin/bash function main() { read -r -s -p "" rspw echo "\$rspw" | sudo -S -v -p "" > /dev/null 2>&1 sudo chown root:wazuh ~/authd.pass sudo mv ~/authd.pass /var/ossec/etc/authd.pass sudo systemctl restart wazuh-agent echo "Done" } main "\$@" EOFLO updateRelayServerWithScript false rm -f $HOME/authd.pass fi set -e } function version108Update() { set +e outputAutheliaIDPSecretsV108 grep "^identity_providers:" $HSHQ_STACKS_DIR/authelia/config/configuration.yml > /dev/null 2>&1 if [ $? -ne 0 ]; then auth_add=$(cat <> $HSHQ_STACKS_DIR/authelia/config/configuration.yml updateStackEnv authelia modFunAutheliaConfigFilterVar docker ps | grep codeserver > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart codeserver > /dev/null 2>&1 fi fi set -e } function version111Update() { mailuV111Bugfix } function version113Update() { outputWGDockInternetScript } function version114Update() { set +e grep "# sn-sub-${SUB_HSHQSTATUS} BEGIN" $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip > /dev/null 2>&1 if [ $? -ne 0 ]; then hshqstatus_block=$(cat </dev/null sudo sed -i '/includedir/d' /etc/sudoers >/dev/null if [ "$IS_RUST_UTILS" = "true" ]; then echo "authpriv.* /var/log/sudo.log" | sudo tee /etc/rsyslog.d/50-sudo.conf >/dev/null sudo systemctl restart rsyslog else echo "Defaults logfile=/var/log/sudo.log" | sudo tee -a /etc/sudoers >/dev/null fi echo "@includedir /etc/sudoers.d" | sudo tee -a /etc/sudoers >/dev/null fixNetplanBackport rm -f $HOME/$RS_UPDATE_SCRIPT_NAME cat < $HOME/$RS_UPDATE_SCRIPT_NAME #!/bin/bash set +e function main() { read -r -s -p "" rspw echo "\$rspw" | sudo -S -v -p "" > /dev/null 2>&1 if sudo test -f /etc/logrotate.d/rsyslog; then # Set max size of syslog to 2G grep maxsize /etc/logrotate.d/rsyslog > /dev/null 2>&1 if [ \$? -ne 0 ]; then sudo sed -i "/weekly\$/a\ maxsize 2G" /etc/logrotate.d/rsyslog fi fi # Move logrotate into hourly if sudo test -f /etc/cron.daily/logrotate; then sudo mv /etc/cron.daily/logrotate /etc/cron.hourly fi # Set conditions for docker container logs sudo tee /etc/logrotate.d/docker >/dev/null < $HOME/$RS_UPDATE_SCRIPT_NAME function main() { read -r -s -p "" rspw echo "\$rspw" | sudo -S -v -p "" > /dev/null 2>&1 RELAYSERVER_HSHQ_SCRIPTS_DIR=\$HOME/hshq/data/scripts exposedPortsList=53,$MAILU_PORT_5,$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT,$RELAYSERVER_WG_PORTAL_PORT,$SYNCTHING_SYNC_PORT,$SYNCTHING_DISC_PORT sudo rm -f \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/onBootRoot.sh sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/onBootRoot.sh >/dev/null </dev/null < /dev/null sudo iptables -t filter -X DOCKER-USER > /dev/null sudo iptables -t filter -N DOCKER-USER > /dev/null portsArr=(\\\$(echo \\\$ports_list | tr "," "\n")) for cur_port in "\\\${portsArr[@]}" do iptables -D DOCKER-USER -s 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j RETURN 2> /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j DROP 2> /dev/null iptables -I DOCKER-USER -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j DROP iptables -I DOCKER-USER -s 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 -m conntrack --ctorigdstport \\\$cur_port --ctdir ORIGINAL -j RETURN done # See https://gist.github.com/mattia-beta/bd5b1c68e3d51db933181d8a3dc0ba64?permalink_comment_id=3728715#gistcomment-3728715 iptables -t raw -F PREROUTING iptables -t raw -F chain-icmp > /dev/null 2>&1 iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 iptables -t raw -X chain-icmp > /dev/null 2>&1 iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 iptables -t raw -N chain-icmp > /dev/null 2>&1 iptables -t raw -N chain-bad_tcp > /dev/null 2>&1 iptables -t raw -A PREROUTING -s 192.0.0.0/24,192.0.2.0/24,198.51.100.0/24,203.0.113.0/24 -j DROP iptables -t raw -A PREROUTING -d 192.0.0.0/24,192.0.2.0/24,198.51.100.0/24,203.0.113.0/24 -j DROP iptables -t raw -A PREROUTING -d 0.0.0.0/8 -j DROP # Block spoofed packets iptables -t raw -A PREROUTING -s 0.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,224.0.0.0/4,198.18.0.0/15,100.64.0.0/10,192.88.99.0/24 -i \$default_iface -j DROP iptables -t raw -A PREROUTING -s 127.0.0.0/8 ! -i lo -j DROP iptables -t raw -A PREROUTING -p udp -m udp -m multiport --ports 0 -j DROP iptables -t raw -A PREROUTING -p icmp -j chain-icmp iptables -t raw -A PREROUTING -p tcp -m tcp -j chain-bad_tcp iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-reply -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type network-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type host-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type protocol-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type port-unreachable -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type fragmentation-needed -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-request -i \$default_iface -j DROP iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type echo-request -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type time-exceeded -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type parameter-problem -j ACCEPT iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type any -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN FIN,SYN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,RST SYN,RST -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,RST FIN,RST -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,ACK FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,URG URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,FIN FIN -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,PSH PSH -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL ALL -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL NONE -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP iptables -t raw -A chain-bad_tcp -p tcp -m tcp -m multiport --ports 0 -j DROP iptables -t raw -P PREROUTING ACCEPT # Configure loopback iptables -C INPUT -i lo -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -i lo -j ACCEPT iptables -C INPUT ! -i lo -s 127.0.0.0/8 -j DROP > /dev/null 2>&1 || iptables -A INPUT ! -i lo -s 127.0.0.0/8 -j DROP # Limit connections per source IP iptables -C INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset > /dev/null 2>&1 || iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 50 -j REJECT --reject-with tcp-reset # Allow established connections iptables -C INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Drop invalid packets iptables -C INPUT -m conntrack --ctstate INVALID -j DROP > /dev/null 2>&1 || iptables -A INPUT -m conntrack --ctstate INVALID -j DROP # Drop fragments iptables -C INPUT -f -j DROP > /dev/null 2>&1 || iptables -A INPUT -f -j DROP # Drop SYN packets with suspicious MSS value iptables -C INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP > /dev/null 2>&1 || iptables -A INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP # Allow ICMP iptables -C INPUT -p icmp -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p icmp -j ACCEPT # Allow SSH iptables -C INPUT -p tcp -m tcp --dport \\\$SSH_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p tcp -m tcp --dport \\\$SSH_PORT -j ACCEPT # Allow WireGuard iptables -C INPUT -p udp --dport \\\$WG_CON_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p udp --dport \\\$WG_CON_PORT -j ACCEPT # Special case for WG Portal from reverse proxy iptables -C INPUT -p tcp -m tcp -i brdockext -s \\\$DOCK_EXT_NET --dport \\\$WG_PORTAL_PORT -j ACCEPT > /dev/null 2>&1 || iptables -A INPUT -p tcp -m tcp -i brdockext -s \\\$DOCK_EXT_NET --dport \\\$WG_PORTAL_PORT -j ACCEPT iptables -D DOCKER-USER -s 127.0.0.0/8 -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j RETURN 2> /dev/null iptables -D DOCKER-USER -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j DROP 2> /dev/null iptables -I DOCKER-USER -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j DROP iptables -I DOCKER-USER -s 127.0.0.0/8 -m conntrack --ctorigdstport \\\$WG_PORTAL_PORT --ctdir ORIGINAL -j RETURN # Policy drop for input and forward iptables -P INPUT DROP iptables -P FORWARD DROP # Policy drop all ipv6 traffic ip6tables -P INPUT DROP ip6tables -P FORWARD DROP ip6tables -P OUTPUT DROP sysctl --system > /dev/null 2>&1 EOFBS # Special case - if RelayServer is behind a firewall and using a private IP for default route, # then allow traffic from the gateway, since raw table blocks all private ranges def_route_gate=\$(ip route | grep -e "^default" | awk '{print \$3}') is_def_priv=\$(checkIsIPPrivate \$def_route_gate) def_route_cidr_part=\$(ip route | grep \$(ip route | grep -e "^default" | head -n 1 | awk -F'dev ' '{print \$2}' | xargs | cut -d" " -f1) | grep / | xargs | cut -d" " -f1 | cut -d"/" -f2) if [ "\$is_def_priv" = "true" ]; then echo "Default route is in private range, adding allowance to (raw)iptables..." sudo sed -i "/ # Block spoofed packets.*/a\ iptables -t raw -C PREROUTING -s \$def_route_gate\/\$def_route_cidr_part -i \$default_iface -j RETURN > \/dev\/null 2>&1 || iptables -t raw -I PREROUTING -s \$def_route_gate\/\$def_route_cidr_part -i \$default_iface -j RETURN" \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh else echo "Default route is in public range." fi sudo chmod 744 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh sudo \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh sudo rm -f \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh >/dev/null < /dev/null 2>&1 iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 iptables -t raw -F chain-ipspoof > /dev/null 2>&1 iptables -t raw -X chain-icmp > /dev/null 2>&1 iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 iptables -t raw -X chain-ipspoof > /dev/null 2>&1 iptables -P INPUT ACCEPT ip6tables -P INPUT ACCEPT ip6tables -P FORWARD ACCEPT ip6tables -P OUTPUT ACCEPT EOFBS sudo chmod 500 \$RELAYSERVER_HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh sudo systemctl disable runOnBootRoot sudo systemctl daemon-reload sudo rm -f /etc/systemd/system/runOnBootRoot.service sudo rm -f \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/runOnBootRoot.service sudo tee \$RELAYSERVER_HSHQ_SCRIPTS_DIR/boot/runOnBootRoot.service >/dev/null < /dev/null 2>&1 if [ \$? -eq 0 ]; then echo "true" else echo "false" fi set +e if ! [ -z \$iiis_curE ]; then set -e fi } main "\$@" EOFRS updateRelayServerWithScript false echo "Installing yq..." performAptInstall yq echo "Installing wireless-tools..." performAptInstall wireless-tools # Only let superuser modify db sudo chmod 640 $HSHQ_DB sudo chown root $HSHQ_DB isVPNSubnetMoved=$(sqlite3 $HSHQ_DB "select count(*) from pragma_table_info('hsvpn_connections') where name='VPN_Subnet';") if [ $isVPNSubnetMoved -ne 0 ]; then # Move VPN_Subnet from hsvpn_connections to Network_Subnet in connections table sudo sqlite3 $HSHQ_DB "alter table connections add Network_Subnet text default null;" sudo sqlite3 $HSHQ_DB "update connections set Network_Subnet = (select VPN_Subnet from hsvpn_connections where hsvpn_connections.ID = connections.ID);" sudo sqlite3 $HSHQ_DB "create table new_hsvpn_connections(ID integer not null primary key references connections(ID) on delete cascade,HomeServerName text,IsPrimary boolean,DomainName text default null,ExternalPrefix text default null,InternalPrefix text default null,MailHostID integer references mailhosts(ID) on delete cascade,CA_Abbrev text default null,CA_IP text default null,CA_Subdomain text default null,CA_URL text default null,RS_VPN_IP text default null);" sudo sqlite3 $HSHQ_DB "insert into new_hsvpn_connections(ID,HomeServerName,IsPrimary,DomainName,ExternalPrefix,InternalPrefix,MailHostID,CA_Abbrev,CA_IP,CA_Subdomain,CA_URL,RS_VPN_IP) select ID,HomeServerName,IsPrimary,DomainName,ExternalPrefix,InternalPrefix,MailHostID,CA_Abbrev,CA_IP,CA_Subdomain,CA_URL,RS_VPN_IP from hsvpn_connections;" sudo sqlite3 $HSHQ_DB "drop table hsvpn_connections;" sudo sqlite3 $HSHQ_DB "alter table new_hsvpn_connections rename to hsvpn_connections;" # Add new columns sudo sqlite3 $HSHQ_DB "alter table connections add IsExposeToNetwork boolean;" sudo sqlite3 $HSHQ_DB "alter table connections add InputAllowPorts text default null;" sudo sqlite3 $HSHQ_DB "alter table connections add DockerUserAllowPorts text default null;" sudo sqlite3 $HSHQ_DB "create table customfwsubnet(ID integer not null primary key autoincrement,Name text not null, Subnet text not null, InputAllowPorts text default null, DockerUserAllowPorts text default null);" fi sudo grep "172\.16\.0\.0\/12" /etc/docker/daemon.json > /dev/null 2>&1 if [ $? -eq 0 ]; then sudo sed -i "s/172.16.0.0\/12/172.16.0.0\/14/" /etc/docker/daemon.json echo "Restarting docker, this make take a few minutes..." set -e sudo systemctl restart docker set +e echo "Docker succesfully restarted, continuing update..." fi if ! [ -f $HSHQ_PLAINTEXT_ROOT_CONFIG ]; then outputPlainTextRootConfig source <(sudo cat $HSHQ_PLAINTEXT_ROOT_CONFIG) source $CONFIG_FILE fi if ! [ -f $HSHQ_PLAINTEXT_USER_CONFIG ]; then outputPlainTextUserConfig source $HSHQ_PLAINTEXT_USER_CONFIG source $CONFIG_FILE fi if [ -z "$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME" ]; then add_interface=$(getDefaultIface) addHSInterface $add_interface true if [ $? -ne 0 ]; then exit 5 fi fi moveVarsToPlaintextFile sudo rm -f $HSHQ_SCRIPTS_DIR/boot/bootscripts/10-setupDockerUserIPTables.sh sudo rm -f $HSHQ_SCRIPTS_DIR/boot/afterdocker/10-setupDockerUserIPTables.sh sudo rm -f $HSHQ_SCRIPTS_DIR/root/clearDockerUserIPTables.sh sudo rm -f $HSHQ_SCRIPTS_DIR/boot/bootscripts/50-dockerWireGuardCaddyFix.sh sudo rm -f $HSHQ_SCRIPTS_DIR/boot/afterdocker/50-dockerWireGuardCaddyFix.sh if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then prID=$(getPrimaryVPN_DBID) PRIMARY_VPN_SUBNET=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections where ID=$prID;") updatePlaintextRootConfigVar PRIMARY_VPN_SUBNET $PRIMARY_VPN_SUBNET fi source <(sudo cat $HSHQ_PLAINTEXT_ROOT_CONFIG) source $HSHQ_PLAINTEXT_USER_CONFIG idList=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='other';")) for curID in "${idList[@]}" do sudo sqlite3 $HSHQ_DB "update connections set IsExposeToNetwork=true where ID=$curID;" sudo sqlite3 $HSHQ_DB "update connections set InputAllowPorts='$INPUT_OTHER_VPN_ALLOW_PORTS_DEFAULT' where ID=$curID;" sudo sqlite3 $HSHQ_DB "update connections set DockerUserAllowPorts='$DOCKERUSER_OTHER_VPN_ALLOW_PORTS_DEFAULT' where ID=$curID;" done primID=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='primary';") if ! [ -z "$primID" ]; then sudo sqlite3 $HSHQ_DB "update connections set IsExposeToNetwork=true where ID=$primID;" sudo sqlite3 $HSHQ_DB "update connections set InputAllowPorts='$INPUT_PRIMARY_VPN_ALLOW_PORTS_DEFAULT' where ID=$primID;" sudo sqlite3 $HSHQ_DB "update connections set DockerUserAllowPorts='$DOCKERUSER_PRIMARY_VPN_ALLOW_PORTS_DEFAULT' where ID=$primID;" fi set +e echo "Restarting Portainer..." stopPortainer outputConfigPortainer startPortainer echo "Fixing interface names..." fixInterfaceNames echo "Fixing mailu network names..." fixMailuNetworkNames echo "Clearing and re-initializing iptables..." performClearIPTables true checkUpdateAllIPTables versionUpdate echo "Updating Sysctl..." updateSysctl true outputMaintenanceScripts outputWGDockInternetScript jitsiStackID=$(getStackID jitsi) if ! [ -z "$jitsiStackID" ]; then updateStackEnv jitsi outputEnvJitsi fi stackID=$(getStackID caddy-home) if ! [ -z "$stackID" ]; then echo "Fixing caddy-home name..." # Reinsatll caddy-home stack deleteStack caddy-home sudo mv $HSHQ_STACKS_DIR/caddy-home $HSHQ_STACKS_DIR/caddy-home-$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME outputConfigCaddy home-$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME caddy-home-$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME home $(getHSHostIPVarName $HOMESERVER_HOST_PRIMARY_INTERFACE_NAME) $HOMESERVER_HOST_PRIMARY_INTERFACE_IP home na na na "$(getHSHostSubnetVarName $HOMESERVER_HOST_PRIMARY_INTERFACE_NAME)" "$(getNonPrivateConnectingIP)" installStack caddy-home-$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME caddy-home-$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME "serving initial configuration" $HOME/caddy-home-$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME.env fi echo "Updating boot services..." sudo systemctl disable runOnBootRoot sudo systemctl daemon-reload sudo rm -f /etc/systemd/system/runOnBootRoot.service sudo rm -f $HSHQ_SCRIPTS_DIR/boot/runOnBootRoot.service if sudo test -d $HSHQ_SCRIPTS_DIR/boot/bootscripts; then sudo mv $HSHQ_SCRIPTS_DIR/boot/bootscripts $HSHQ_SCRIPTS_DIR/boot/afterdocker fi outputBootScripts echo "Updating cronjobs..." initCronJobs set -e } function version126Update() { set +e sudo crontab -r ema1=$(getConfigVarFromFile EMAIL_ADMIN_EMAIL_ADDRESS $CONFIG_FILE) ema2=$(getConfigVarFromFile EMAIL_ADMIN_EMAIL_ADDRESS $HSHQ_PLAINTEXT_ROOT_CONFIG root) if ! [ -z "$ema1" ] && [ -z "$ema2" ]; then updatePlaintextRootConfigVar EMAIL_ADMIN_EMAIL_ADDRESS $EMAIL_ADMIN_EMAIL_ADDRESS sed -i "/^EMAIL_ADMIN_EMAIL_ADDRESS=/d" $CONFIG_FILE >/dev/null fi sudo rm -f $HSHQ_SCRIPTS_DIR/root/checkUpdateIPTables.sh sudo rm -f $HSHQ_SCRIPTS_DIR/root/checkHostIPChanges.sh sudo rm -f $HSHQ_WIREGUARD_DIR/scripts/updateEndpointIPs.sh sudo rm -f $HSHQ_SCRIPTS_DIR/boot/afterdocker/*.sh sudo rm -f $HSHQ_SCRIPTS_DIR/boot/beforenetwork/updateIPTablesBeforeNetwork.sh sudo rm -f $HSHQ_WIREGUARD_DIR/scripts/wgDockInternetUpAll.sh sudo rm -f /etc/networkd-dispatcher/routable.d/wgDockInternetUpAll.sh set +e sudo systemctl stop createWGDockerNetworks > /dev/null 2>&1 sudo systemctl disable createWGDockerNetworks > /dev/null 2>&1 sudo rm -f /etc/systemd/system/createWGDockerNetworks.service sudo systemctl daemon-reload sudo rm -f $HSHQ_SCRIPTS_DIR/root/createWGDockerNetworks.sh sudo sed -i "s/CHECKIP_REFRESH_RATE/NETWORK_UPDATE_INTERVAL/" $HSHQ_PLAINTEXT_ROOT_CONFIG >/dev/null NETWORK_UPDATE_INTERVAL=5 updatePlaintextRootConfigVar NETWORK_UPDATE_INTERVAL $NETWORK_UPDATE_INTERVAL addBindIPCaddy outputUpdateIPTablesBeforeNetworkBootscript outputPerformPostDockerBootActionsScript outputPerformNetworkingChecks outputWireGuardScripts initCronJobs updateMOTD getUpdateAssets outputScriptServerTheme } function version132Update() { set +e # Fix matrix version number matrixStackID=$(getStackID matrix) if ! [ -z "$matrixStackID" ]; then echo "Checking matrix stack version..." matrixComposeFile="$HSHQ_STACKS_DIR/portainer/compose/$matrixStackID/docker-compose.yml" sudo grep "matrixdotorg/synapse:v1.118.0" $matrixComposeFile if [ $? -eq 0 ]; then echo "Fixing matrix stack version..." sudo sed -i "1s|.*|$STACK_VERSION_PREFIX matrix v6|" $matrixComposeFile fi fi updateSysctl true sudo grep "mirror.gcr.io" /etc/docker/daemon.json > /dev/null 2>&1 if [ $? -ne 0 ]; then outputDockerDaemonJson echo "Docker daemon is being restarted. If you have a lot of containers, then this may take a few minutes..." sudo systemctl restart docker echo "Docker daemon successfully restarted. Please log in to Portainer and confirm that all containers are" echo "running, i.e. none of them have exited. If any have exited, then ensure to restart the entire stack." echo "You can also double check in UptimeKuma that all services are in a good running state." fi } function version134Update() { if [ -z "$RELAYSERVER_HSHQ_BASE_DIR" ]; then RELAYSERVER_HSHQ_BASE_DIR=/home/$RELAYSERVER_REMOTE_USERNAME/hshq RELAYSERVER_HSHQ_DATA_DIR=$RELAYSERVER_HSHQ_BASE_DIR/data RELAYSERVER_HSHQ_NONBACKUP_DIR=$RELAYSERVER_HSHQ_BASE_DIR/nonbackup RELAYSERVER_HSHQ_SCRIPTS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/scripts RELAYSERVER_HSHQ_SECRETS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/secrets RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/stacks RELAYSERVER_HSHQ_SSL_DIR=$RELAYSERVER_HSHQ_DATA_DIR/ssl updateConfigVar RELAYSERVER_HSHQ_BASE_DIR $RELAYSERVER_HSHQ_BASE_DIR updateConfigVar RELAYSERVER_HSHQ_DATA_DIR $RELAYSERVER_HSHQ_DATA_DIR updateConfigVar RELAYSERVER_HSHQ_NONBACKUP_DIR $RELAYSERVER_HSHQ_NONBACKUP_DIR updateConfigVar RELAYSERVER_HSHQ_SCRIPTS_DIR $RELAYSERVER_HSHQ_SCRIPTS_DIR updateConfigVar RELAYSERVER_HSHQ_SECRETS_DIR $RELAYSERVER_HSHQ_SECRETS_DIR updateConfigVar RELAYSERVER_HSHQ_STACKS_DIR $RELAYSERVER_HSHQ_STACKS_DIR updateConfigVar RELAYSERVER_HSHQ_SSL_DIR $RELAYSERVER_HSHQ_SSL_DIR fi } function version136Update() { sudo rm -f $HSHQ_STACKS_DIR/duplicati/config/hshq-backup.json } function version138Update() { outputPerformNetworkingChecks updateSysctl true } function version146Update() { outputScriptServerConfigFile IS_RESTART_SCRIPTSERVER=true } function version152Update() { if [ -d $HSHQ_STACKS_DIR/coturn ]; then echo "Fixing allowed IPs on coturn stack..." outputCoturnConf updateCoturnAllowedIPs fi echo "Updating adguard env..." updateStackEnv adguard modFunAdguardFixMSS checkUpdateAllIPTables versionUpdate } function version164Update() { checkUpdateAllIPTables versionUpdate } function version165Update() { outputLockUtilsScript } function version171Update() { # Fixes the annoying excess space at the bottom of Firefox browser windows in Heimdall sed -i '/\.sidenav ul {/{:a;N;/}/!ba;s/\.sidenav ul {.*}/\.sidenav ul {\n list-style: none;\n margin: 0;\n padding: 20px;\n overflow-y: auto;\n}/g}' $HSHQ_STACKS_DIR/heimdall/html/public/css/app.css rm -f $HOME/$RS_UPDATE_SCRIPT_NAME cat < $HOME/$RS_UPDATE_SCRIPT_NAME #!/bin/bash RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_STACKS_DIR function main() { rm -f \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/addRelayedMailDomains.sh cat < \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/addRelayedMailDomains.sh #!/bin/bash set -e domains_to_add=\\\$1 deliver_to_host=\\\$2 if [ -z "\\\$domains_to_add" ] || [ -z "\\\$deliver_to_host" ]; then echo "Empty Parameters" exit 1 fi deliver_to_host_username=\\\$(echo \\\$deliver_to_host | cut -d"." -f2-) domains_to_add_Arr=(\\\$(echo \\\$domains_to_add | tr "," "\n")) for curDomain in "\\\${domains_to_add_Arr[@]}" do sed -i "/^@\\\$curDomain /d" /etc/postfix/config/sasl_senders sed -i "/\\\$curDomain/d" /etc/postfix/config/transport sed -i "/^\\\$curDomain /d" /etc/postfix/config/relay echo "@\\\${curDomain} \\\${deliver_to_host_username}@mail-relay-postfix" >> /etc/postfix/config/sasl_senders sed -i "1s/^/\\\${curDomain} relay:[\\\${deliver_to_host}]:25\n/" /etc/postfix/config/transport echo "\\\${curDomain} OK" >> /etc/postfix/config/relay done postmap /etc/postfix/config/transport postmap /etc/postfix/config/sasl_senders postmap /etc/postfix/config/relay postfix reload EOFCS chmod 544 \$RELAYSERVER_HSHQ_STACKS_DIR/mail-relay/postfix/scripts/addRelayedMailDomains.sh } main "\$@" EOFRS updateRelayServerWithScript false } function version177Update() { PORTAINER_ENDPOINT_ID=1 outputMaintenanceScripts } function version180Update() { outputLockUtilsScript } function version181Update() { if [ ${#DISCOURSE_ADMIN_USERNAME} -gt 20 ]; then DISCOURSE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE updateConfigVar DISCOURSE_ADMIN_USERNAME $DISCOURSE_ADMIN_USERNAME fi mkdir -p $HSHQ_STACKS_DIR/shared/rdata/media/{audio,books,comics,misc,movies,software,tv} } function version183Update() { performAptInstall ffmpeg > /dev/null 2>&1 outputMaintenanceScripts } function version188Update() { echo "========================================================================" echo " Halting all stacks and upgrading Docker and Portainer." echo " This may take a few minutes, so please be patient." echo "========================================================================" sleep 5 fixPortainerAndUpgradeDockerV188 outputMaintenanceScripts upgradeHostOS } function version189Update() { if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "========================================================================" echo " Performing updates on RelayServer." echo " This may take a few minutes, so please be patient." echo "========================================================================" sleep 5 rm -f $HOME/$RS_UPDATE_SCRIPT_NAME cat < $HOME/$RS_UPDATE_SCRIPT_NAME #!/bin/bash function main() { read -r -s -p "" rspw echo "\$rspw" | sudo -S -v -p "" > /dev/null 2>&1 relayServerFixPortainerAndUpgradeDockerV188 if [ \$? -ne 0 ]; then return 1 fi sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' rm -f ~/$RS_UPDATE_SCRIPT_NAME rm -f ~/$RS_UPDATE_UTILS_SCRIPT_NAME } function relayServerFixPortainerAndUpgradeDockerV188() { set +e rsi_numTries=1 rsi_totalTries=5 rsi_retVal=1 rstackIDsQry="" source ~/$RS_UPDATE_UTILS_SCRIPT_NAME pullImage mirror.gcr.io/caddy:2.10.0 pullImage mirror.gcr.io/portainer/portainer-ce:2.33.1-alpine pullImage mirror.gcr.io/adguard/adguardhome:v0.107.64 setPortainerToken while [ \$rsi_numTries -le \$rsi_totalTries ] do rstackIDsQry=\$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":1} "Authorization: Bearer \$PORTAINER_TOKEN") rsi_retVal=\$? if [ \$rsi_retVal -eq 0 ]; then break fi ((rsi_numTries++)) done if [ \$rsi_retVal -ne 0 ]; then echo "ERROR: Could not get list of stack IDs in Portainer..." 1>&2 return 3 fi # Need to use a temp resolver since Adguard goes down for a bit... sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 docker container prune -f echo "Replacing portainer env vars..." # Retain all env vars fixvars=(\$(cat $RELAYSERVER_HSHQ_STACKS_DIR/portainer/portainer.env | cut -d"=" -f1)) for curVar in "\${fixvars[@]}" do if ! [ "\${curVar:0:10}" = "PORTAINER_" ]; then sudo find $RELAYSERVER_HSHQ_STACKS_DIR/portainer/compose -type f -exec sed -i "s/\\\$\$curVar/\\\$PORTAINER_\$curVar/g" {} \; sudo find $RELAYSERVER_HSHQ_STACKS_DIR/portainer/compose -type f -exec sed -i "s/\\\${\$curVar}/\\\${PORTAINER_\$curVar}/g" {} \; fi done set +e upgradeDocker echo "Restarting Docker..." sudo systemctl restart docker cat < $RELAYSERVER_HSHQ_STACKS_DIR/portainer/portainer.env PORTAINER_RELAYSERVER_HSHQ_DATA_DIR=$RELAYSERVER_HSHQ_DATA_DIR PORTAINER_RELAYSERVER_HSHQ_NONBACKUP_DIR=$RELAYSERVER_HSHQ_NONBACKUP_DIR PORTAINER_RELAYSERVER_HSHQ_SCRIPTS_DIR=$RELAYSERVER_HSHQ_SCRIPTS_DIR PORTAINER_RELAYSERVER_HSHQ_SECRETS_DIR=$RELAYSERVER_HSHQ_SECRETS_DIR PORTAINER_RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_STACKS_DIR PORTAINER_RELAYSERVER_HSHQ_SSL_DIR=$RELAYSERVER_HSHQ_SSL_DIR PORTAINER_TZ=$TZ PORTAINER_UID=\$USERID PORTAINER_GID=\$GROUPID EOFPC sed -i "s/\/run\/secrets\/portainer/\/run\/portainer\/portainer/g" $RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml sed -i "s/image:.*/image: mirror.gcr.io\/portainer\/portainer-ce:2.33.1-alpine/" $RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml sed -i "s/${STACK_VERSION_PREFIX}.*/${STACK_VERSION_PREFIX} portainer v4/" $RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml docker compose -f $RELAYSERVER_HSHQ_STACKS_DIR/portainer/docker-compose.yml up -d sleep 10 total_tries=10 num_tries=1 sleep 5 setPortainerToken if ! [ -z "\$adguardStackID" ]; then echo "Starting adguard (\$adguardStackID)..." sudo cp $RELAYSERVER_HSHQ_STACKS_DIR/portainer/compose/\$adguardStackID/docker-compose.yml ~/adguard-compose.yml sudo cp $RELAYSERVER_HSHQ_STACKS_DIR/portainer/compose/\$adguardStackID/stack.env ~/adguard.env sudo chown $RELAYSERVER_REMOTE_USERNAME:$RELAYSERVER_REMOTE_USERNAME ~/adguard-compose.yml sudo chown $RELAYSERVER_REMOTE_USERNAME:$RELAYSERVER_REMOTE_USERNAME ~/adguard.env sed -i "s/image:.*adguard.*/image: mirror.gcr.io\/adguard\/adguardhome:v0.107.64/" ~/adguard-compose.yml sed -i "s/${STACK_VERSION_PREFIX}.*/${STACK_VERSION_PREFIX} adguard v7/" ~/adguard-compose.yml for curVar in "\${fixvars[@]}" do if ! [ "\${curVar:0:10}" = "PORTAINER_" ]; then sed -i "s/\\\$\$curVar/\\\$PORTAINER_\$curVar/g" ~/adguard-compose.yml sed -i "s/\\\${\$curVar}/\\\${PORTAINER_\$curVar}/g" ~/adguard-compose.yml sed -i "s/\\\$\$curVar/\\\$PORTAINER_\$curVar/g" ~/adguard.env sed -i "s/\\\${\$curVar}/\\\${PORTAINER_\$curVar}/g" ~/adguard.env fi done updateStackByID adguard \$adguardStackID ~/adguard-compose.yml ~/adguard.env sleep 3 waitForContainerLogString adguard 3 60 "entering listener loop proto=tls" if [ \$? -eq 0 ]; then sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 fi sudo cp $RELAYSERVER_HSHQ_STACKS_DIR/portainer/compose/\${rstackIDs[\$curID]}/docker-compose.yml ~/\${rstackNames[\$curID]}-compose.yml sudo cp $RELAYSERVER_HSHQ_STACKS_DIR/portainer/compose/\${rstackIDs[\$curID]}/stack.env ~/\${rstackNames[\$curID]}.env sudo chown $RELAYSERVER_REMOTE_USERNAME:$RELAYSERVER_REMOTE_USERNAME ~/\${rstackNames[\$curID]}-compose.yml sudo chown $RELAYSERVER_REMOTE_USERNAME:$RELAYSERVER_REMOTE_USERNAME ~/\${rstackNames[\$curID]}.env for curVar in "\${fixvars[@]}" do if ! [ "\${curVar:0:10}" = "PORTAINER_" ]; then sed -i "s/\\\$\$curVar/\\\$PORTAINER_\$curVar/g" ~/\${rstackNames[\$curID]}-compose.yml sed -i "s/\\\${\$curVar}/\\\${PORTAINER_\$curVar}/g" ~/\${rstackNames[\$curID]}-compose.yml sed -i "s/\\\$\$curVar/\\\$PORTAINER_\$curVar/g" ~/\${rstackNames[\$curID]}.env sed -i "s/\\\${\$curVar}/\\\${PORTAINER_\$curVar}/g" ~/\${rstackNames[\$curID]}.env fi done if [ "\${rstackNames[\$curID]}" = "caddy" ]; then sed -i "s/image:.*caddy.*/image: mirror.gcr.io\/caddy:2.10.0/" ~/\${rstackNames[\$curID]}-compose.yml sed -i "s/${STACK_VERSION_PREFIX}.*/${STACK_VERSION_PREFIX} caddy-rs v5/" ~/\${rstackNames[\$curID]}-compose.yml fi updateStackByID \${rstackNames[\$curID]} \${rstackIDs[\$curID]} ~/\${rstackNames[\$curID]}-compose.yml ~/\${rstackNames[\$curID]}.env sleep 3 done sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < $HOME/$RS_UPDATE_SCRIPT_NAME #!/bin/bash function main() { read -r -s -p "" rspw echo "\$rspw" | sudo -S -v -p "" > /dev/null 2>&1 permsUpdateFile=$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/wgportal/wg_portal.db if [ -f \$permsUpdateFile ]; then sudo chmod 0600 \$permsUpdateFile else echo "ERROR: The WG Portal db (\$permsUpdateFile) does not exist. The permissions were not updated..." fi rm -f ~/$RS_UPDATE_SCRIPT_NAME } main EOFUR updateRelayServerWithScript false if [ $? -ne 0 ]; then echo "ERROR: The update process on the RelayServer encountered an error. Please check the logs and retry." exit fi fi } function version194Update() { homeIntArr=($(sqlite3 $HSHQ_DB "select InterfaceName from connections where NetworkType = 'home_network';")) for curHomeInt in "${homeIntArr[@]}" do updateStackEnv caddy-home-${curHomeInt} modFunFixCaddyHostInterfaceVar done } function version202Update() { sudo sed -i "/HOMESERVER_ABBREV=.*/a HSHQ_APP_TYPE=home" $HSHQ_PLAINTEXT_ROOT_CONFIG HSHQ_APP_TYPE=home } function version204Update() { set +e sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 performAptInstall php-cli > /dev/null 2>&1 } function version205Update() { set +e sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 performAptInstall ruby > /dev/null 2>&1 performAptInstall ruby-dev > /dev/null 2>&1 sudo gem install argon2 > /dev/null 2>&1 set -e } function version218Update() { set +e fixMailuV6 fixMatrixV8 fixMastodonV9 fixPeertubeV7 set +e upgradeHostOS performAptInstall gnupg2 performAptInstall software-properties-common performAptInstall apt-transport-https outputDockerDaemonJson echo -e "========================================================================" echo -e " The docker service is now being updated. This process will stop" echo -e " all stacks in Portainer, perform the update, then restart the" echo -e " stacks. This process will take around 10-15 minutes to complete," echo -e " so please be patient." echo -e "========================================================================" sleep 3 restartAllStacks "" false upgradeDocker false doNothing } function version219Update() { sudo mkdir -p $HSHQ_SCRIPTS_DIR/source sudo chmod 700 $HSHQ_SCRIPTS_DIR/source sudo chown -R ${USERID}:${GROUPID} $HSHQ_SCRIPTS_DIR/source } function version220Update() { outputPerformPostDockerBootActionsScript outputUpdateIPTablesBeforeNetworkBootscript outputPerformNetworkingChecks set +e docker network inspect dock-aipriv > /dev/null 2>&1 if [ $? -ne 0 ]; then docker network create --driver=bridge dock-aipriv > /dev/null 2>&1 docker_subnet=$(getDockerSubnet dock-aipriv) docker network rm dock-aipriv > /dev/null 2>&1 docker network create -o com.docker.network.bridge.name=$NET_AIPRIVATE_BRIDGE_NAME --driver=bridge --subnet $docker_subnet --internal dock-aipriv > /dev/null fi sudo sed -i "/^SVCD_AISTACK_MINDSDB_APP=/d" $CONFIG_FILE > /dev/null 2>&1 sudo sed -i "/^SVCD_AISTACK_LANGFUSE=/d" $CONFIG_FILE > /dev/null 2>&1 sudo sed -i "/^SVCD_AISTACK_OPENWEBUI=/d" $CONFIG_FILE > /dev/null 2>&1 curl -fsSL https://gvisor.dev/archive.key | sudo gpg --yes --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null 2>&1 sudo apt-get update > /dev/null 2>&1 performAptInstall runsc > /dev/null 2>&1 performAptInstall python3-werkzeug > /dev/null 2>&1 echo "Adding readonly users to all running databases..." addAllReadonlyDBUsers echo "All readonly users added succesfully!" CADDY_SNIPPET_SAFEHEADERCORSPREFLIGHT=safe-header-cors-preflight outputCaddyHeaders } function version221Update() { return } function version223Update() { set +e addFirewallSubnet "dock-ext-net" "${NET_EXTERNAL_SUBNET_PREFIX}.0" "21" > /dev/null 2>&1 } function version224Update() { set +e sed -i "s/adminusers_auth/${LDAP_ADMIN_USER_GROUP_NAME}_auth/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml grep -q "langfuse_legacy" $HSHQ_STACKS_DIR/authelia/config/configuration.yml if [ $? -ne 0 ]; then updauth=$(cat << EOFML claims_policies: langfuse_legacy: id_token: - email - name - groups - preferred_username authorization_policies: EOFML ) updauth=$(echo "$updauth" | sed ':a;N;$!ba;s/\n/\\n/g') sed -i "s/authorization_policies:.*/$updauth/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml fi docker container restart authelia > /dev/null 2>&1 } function version226Update() { sed -i "s/^SVCD_LEMONADE_SERVER=.*/SVCD_LEMONADE_SERVER=\"lemonade,lemonade-server,primary,user,Lemonade Server,lemonade-server,hshq\"/" $CONFIG_FILE outputBootScripts outputPerformNetworkingChecks } function version229Update() { mkdir -p $HSHQ_AIMODELS_DIR/huggingface } function version230Update() { echo "========================================================================" echo " This update will apply system updates to the host OS." echo " Additionally, the reserved private network ranges are being expanded." echo " This requires all stacks to be restarted as a result. It can take up" echo " to 30 minutes for this process to complete, so please be patient." echo "========================================================================" echo "Updating OS..." upgradeHostOS > /dev/null 2>&1 echo "Applying docker network updates..." DOCKER_NETWORK_RESERVED_RANGE=172.16.0.0/14 updatePlaintextRootConfigVar DOCKER_NETWORK_RESERVED_RANGE $DOCKER_NETWORK_RESERVED_RANGE sudo sed -i "s/172\.16\.0\.0\/15/172\.16\.0\.0\/14/" /etc/docker/daemon.json sudo sqlite3 $HSHQ_DB "update customfwsubnet set Subnet='$NET_EXTERNAL_SUBNET' where Name='dock-ext-net' and Subnet='172.16.1.0/24';" sed -i "s/address: udp:.*/address: udp:\/\/${NET_PRIVATEIP_SUBNET_PREFIX}\.1:123/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml sudo tee $HSHQ_STACKS_DIR/mailu/overrides/postfix/postfix.cf >/dev/null </dev/null < /dev/null 2>&1 done echo "All network changes complete!" } function version231Update() { set +e sudo groupadd -g 82 nextwrite >/dev/null 2>&1 sudo usermod -a -G 82 $USERNAME >/dev/null 2>&1 mkdir -p $HSHQ_STACKS_DIR/shared/{Images,SharedConsume,SharedProcessed,PersonalConsume,PersonalProcessed,PersonalTranscribeOutput} mkdir -p $HSHQ_STACKS_DIR/shared/KnowledgeBases sudo chown -R 1000:82 $HSHQ_STACKS_DIR/shared sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/SharedProcessed sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/PersonalProcessed sudo chown -R 1000:82 $HSHQ_STACKS_DIR/shared/PersonalTranscribeOutput sudo chmod -R 775 $HSHQ_STACKS_DIR/shared addUserShareDirectories $LDAP_ADMIN_USER_USERNAME addUserShareDirectories $NEXTCLOUD_ADMIN_USERNAME addUserShareDirectories $SPEAKR_ADMIN_USERNAME outputUpdateShareDirectories appendToRoonCron "* * * * * bash $HSHQ_SCRIPTS_DIR/root/updateShareDirectories.sh >/dev/null 2>&1" } function version233Update() { CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED=safe-header-cors-automated CADDY_SNIPPET_BASEHEADER=base-header CADDY_SNIPPET_DEFAULTCSP=default-csp outputCaddyHeaders sed -i "s/$CADDY_SNIPPET_SAFEHEADERALLOWCORS/$CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED/g" hshq/data/stacks/caddy-common/snippets/svcs.snip sed -i "/safe-header-cors-preflight/d " hshq/data/stacks/caddy-common/snippets/svcs.snip inner_block="" inner_block=$inner_block">>https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AUTHELIA $MANAGETLS_AUTHELIA "$is_integrate_hshq" $NETDEFAULT_AUTHELIA "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_COLLABORA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>encode gzip\n" inner_block=$inner_block">>>>@is_css path *.css\n" inner_block=$inner_block">>>>header @is_css Content-Type \"text/css\"\n" inner_block=$inner_block">>>>@is_js path *.js\n" inner_block=$inner_block">>>>header @is_js Content-Type \"application/javascript\"\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://collabora:9980 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_COLLABORA $MANAGETLS_COLLABORA "$is_integrate_hshq" $NETDEFAULT_COLLABORA "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://excalidraw-server {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_EXCALIDRAW_SERVER $MANAGETLS_EXCALIDRAW_SERVER "$is_integrate_hshq" $NETDEFAULT_EXCALIDRAW_SERVER "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_NCTALKHPB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>route /standalone-signaling/* {\n" inner_block=$inner_block">>>>>>>>uri strip_prefix /standalone-signaling\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-talkhpb:8081 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>>>header_up X-Real-IP {remote_host}\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NCTALKHPB $MANAGETLS_NCTALKHPB "$is_integrate_hshq" $NETDEFAULT_NCTALKHPB "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_NCTALKRECORD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://nextcloud-talkrecord:1234 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NCTALKRECORD $MANAGETLS_NCTALKRECORD "$is_integrate_hshq" $NETDEFAULT_NCTALKRECORD "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_BASEHEADER\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' *.$HOMESERVER_DOMAIN data:; frame-src 'self' *.$HOMESERVER_DOMAIN www.youtube-nocookie.com www.youtube.com data: blob:; connect-src 'self' *.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests;\"\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://openwebui-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENWEBUI_APP $MANAGETLS_OPENWEBUI_APP "$is_integrate_hshq" $NETDEFAULT_OPENWEBUI_APP "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_BASEHEADER\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com cdn.tailwindcss.com unpkg.com cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com unpkg.com; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com cdn.tailwindcss.com unpkg.com; img-src 'self' *.${HOMESERVER_DOMAIN} data:; font-src 'self' cdnjs.cloudflare.com data:; frame-src 'self' *.${HOMESERVER_DOMAIN} data: blob:; connect-src 'self' *.${HOMESERVER_DOMAIN} data: api.github.com; object-src 'none'; upgrade-insecure-requests; frame-ancestors 'self' *.${HOMESERVER_DOMAIN};\"\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://paperless-ai:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PAPERLESS_AI_WEB $MANAGETLS_PAPERLESS_AI_WEB "$is_integrate_hshq" $NETDEFAULT_PAPERLESS_AI_WEB "$inner_block" restartAllCaddyContainers } function version234Update() { performAptInstall inotify-tools > /dev/null 2>&1 outputNextcloudInotifyScan mkdir -p $HSHQ_STACKS_DIR/shared/PersonalTranscribeOutput sudo chown -R 1000:82 $HSHQ_STACKS_DIR/shared sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/SharedProcessed sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/PersonalProcessed sudo chown -R 1000:82 $HSHQ_STACKS_DIR/shared/PersonalTranscribeOutput addUserShareDirectories $SPEAKR_ADMIN_USERNAME sed -i "s/langfuse_legacy/cp_legacy/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml docker container restart authelia > /dev/null 2>&1 set +e docker ps | grep nextcloud-app > /dev/null 2>&1 if [ $? -eq 0 ]; then startStopStack nextcloud stop sleep 1 startStopStack nextcloud start fi grep -q "^IS_AUTO_INSTALL" $CONFIG_FILE if [ $? -ne 0 ]; then sed -i "s|^# General Info END|IS_AUTO_INSTALL=\"false\"\n# General Info END|g" $CONFIG_FILE fi } function version235Update() { checkAddServiceToConfig "clientdns-user1" "CLIENTDNS_USER1_ADMIN_USERNAME=,CLIENTDNS_USER1_ADMIN_PASSWORD=" $CONFIG_FILE false } function version236Update() { outputCaddyHeaders } function version237Update() { CADDY_SNIPPET_RELAXEDCSP=relaxed-csp outputCaddyHeaders } function version238Update() { sudo mkdir -p $HSHQ_STACKS_DIR/shared/PersonalTranscribeInput sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/PersonalTranscribeInput sudo mkdir -p $HSHQ_STACKS_DIR/shared/PersonalTranscribeInput/$SPEAKR_ADMIN_USERNAME sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/PersonalTranscribeInput/$SPEAKR_ADMIN_USERNAME sudo mkdir -p $HSHQ_STACKS_DIR/shared/PersonalTranscribeInput/$NEXTCLOUD_ADMIN_USERNAME sudo chown -R 82:82 $HSHQ_STACKS_DIR/shared/PersonalTranscribeInput/$NEXTCLOUD_ADMIN_USERNAME sudo rm -fr $HSHQ_STACKS_DIR/shared/KnowledgeBases/{Bible,YouTube,Paperless,Speakr,WebScrapes,Email,HSHQ} sudo mkdir -p $HSHQ_STACKS_DIR/shared/KnowledgeBases sudo chown -R $USERID:82 $HSHQ_STACKS_DIR/shared/KnowledgeBases sudo chown $USERID:82 $HSHQ_STACKS_DIR/shared outputNextcloudInotifyScan set +e docker ps | grep -q paperless-app > /dev/null 2>&1 if [ $? -eq 0 ]; then PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME="Personal Email" PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID=$(docker exec paperless-app curl -s -X GET "http://localhost:8000/api/tags/?page_size=100" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" | jq -r --arg n "$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME" '.results[] | select(.name == $n) | .id' | head -n1) if [ -z "$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID" ] || [ "$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID" = "null" ]; then jsonbody="{ \"name\": \"$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME\", \"color\": \"#299aa5\" }" PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/tags/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') fi updateConfigVar PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME "$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME" updateConfigVar PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID "$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID" PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME="Shared Email" PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID=$(docker exec paperless-app curl -s -X GET "http://localhost:8000/api/tags/?page_size=100" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" | jq -r --arg n "$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME" '.results[] | select(.name == $n) | .id' | head -n1) if [ -z "$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID" ] || [ "$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID" = "null" ]; then jsonbody="{ \"name\": \"$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME\", \"color\": \"#ff7f7f\" }" PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/tags/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') fi updateConfigVar PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME "$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME" updateConfigVar PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID "$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID" PAPERLESS_KNOWLEDGEBASE_TAG_NAME="Knowledge Base" PAPERLESS_KNOWLEDGEBASE_TAG_ID=$(docker exec paperless-app curl -s -X GET "http://localhost:8000/api/tags/?page_size=100" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" | jq -r --arg n "$PAPERLESS_KNOWLEDGEBASE_TAG_NAME" '.results[] | select(.name == $n) | .id' | head -n1) if [ -z "$PAPERLESS_KNOWLEDGEBASE_TAG_ID" ] || [ "$PAPERLESS_KNOWLEDGEBASE_TAG_ID" = "null" ]; then jsonbody="{ \"name\": \"$PAPERLESS_KNOWLEDGEBASE_TAG_NAME\", \"color\": \"#615dff\", \"matching_algorithm\": 6, \"is_insensitive\": true }" PAPERLESS_KNOWLEDGEBASE_TAG_ID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/tags/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') fi updateConfigVar PAPERLESS_KNOWLEDGEBASE_TAG_NAME "$PAPERLESS_KNOWLEDGEBASE_TAG_NAME" updateConfigVar PAPERLESS_KNOWLEDGEBASE_TAG_ID "$PAPERLESS_KNOWLEDGEBASE_TAG_ID" PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME="Transcription" PAPERLESS_TRANSCRIPTION_DOCTYPE_ID=$(docker exec paperless-app curl -s -X GET "http://localhost:8000/api/document_types/?page_size=100" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" | jq -r --arg n "$PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME" '.results[] | select(.name == $n) | .id' | head -n1) if [ -z "$PAPERLESS_TRANSCRIPTION_DOCTYPE_ID" ] || [ "$PAPERLESS_TRANSCRIPTION_DOCTYPE_ID" = "null" ]; then RESPONSE=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/document_types/" \ -H "Authorization: Token $PAPERLESS_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME\", \"matching_algorithm\": 6, \"match\": \"\"}") fi updateConfigVar PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME "$PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME" updateConfigVar PAPERLESS_TRANSCRIPTION_DOCTYPE_ID "$PAPERLESS_TRANSCRIPTION_DOCTYPE_ID" fi initServicesCredentials addUserMailu user $EMAIL_JOINT_USERNAME $HOMESERVER_DOMAIN $EMAIL_JOINT_PASSWORD sendEmail -s "Joint Email Account Info" -b "A new email account has been added to Mailu. The intent of this account to share amongst your team members for common access. Here are the credentials:\n\n Joint Email Address: $EMAIL_JOINT_EMAIL_ADDRESS\nJoint Email Password: $EMAIL_JOINT_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" outputDBExportScripts outputCaddyHeaders restartAllCaddyContainers sudo grep -q "^HOMESERVER_CURRENCY_CODE" $HSHQ_PLAINTEXT_ROOT_CONFIG if [ $? -ne 0 ]; then HOMESERVER_CURRENCY_CODE="USD" sudo sed -i "s|^# General Info END|HOMESERVER_CURRENCY_CODE=\"USD\"\n# General Info END|g" $HSHQ_PLAINTEXT_ROOT_CONFIG fi grep -q "linkwarden_claim" $HSHQ_STACKS_DIR/authelia/config/configuration.yml if [ $? -ne 0 ]; then updauth=$(cat << EOFML linkwarden_claim: id_token: - email - name - preferred_username - email_verified authorization_policies: EOFML ) updauth=$(echo "$updauth" | sed ':a;N;$!ba;s/\n/\\n/g') sed -i "s/authorization_policies:.*/$updauth/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml docker container restart authelia > /dev/null 2>&1 fi docker ps | grep -q linkwarden-app > /dev/null 2>&1 if [ $? -eq 0 ]; then cat < $HOME/linkwarden.oidc # Authelia OIDC Client linkwarden BEGIN - client_id: linkwarden client_name: Linkwarden client_secret: '$(htpasswd -bnBC 10 "" $LINKWARDEN_OIDC_CLIENT_SECRET | tr -d ':\n')' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth consent_mode: implicit claims_policy: linkwarden_claim scopes: - openid - email - profile redirect_uris: - https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN/api/v1/auth/callback/authelia userinfo_signed_response_alg: none # Authelia OIDC Client linkwarden END EOFIM oidcBlock=$(cat $HOME/linkwarden.oidc) rm -f $HOME/linkwarden.oidc insertOIDCClientAuthelia linkwarden "$oidcBlock" fi grep -q "full_verified_claim" $HSHQ_STACKS_DIR/authelia/config/configuration.yml if [ $? -ne 0 ]; then updauth=$(cat << EOFML full_verified_claim: id_token: - email - name - groups - preferred_username - email_verified authorization_policies: EOFML ) updauth=$(echo "$updauth" | sed ':a;N;$!ba;s/\n/\\n/g') sed -i "s/authorization_policies:.*/$updauth/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml docker container restart authelia > /dev/null 2>&1 fi docker ps | grep -q speakr-app > /dev/null 2>&1 if [ $? -eq 0 ]; then SPEAKR_OIDC_CLIENT_SECRET_HASH=$(docker run --rm $IMG_AUTHELIA authelia crypto hash generate pbkdf2 --variant sha512 --password $SPEAKR_OIDC_CLIENT_SECRET | cut -d" " -f2) cat < $HOME/speakr.oidc # Authelia OIDC Client speakr BEGIN - client_id: $SPEAKR_OIDC_CLIENT_ID client_name: Speakr client_secret: $SPEAKR_OIDC_CLIENT_SECRET_HASH public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth require_pkce: false pkce_challenge_method: '' redirect_uris: - https://$SUB_SPEAKR_APP.$HOMESERVER_DOMAIN/auth/sso/callback scopes: - openid - profile - email claims_policy: full_verified_claim response_types: - code grant_types: - authorization_code access_token_signed_response_alg: none userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client speakr END EOFIM oidcBlock=$(cat $HOME/speakr.oidc) rm -f $HOME/speakr.oidc insertOIDCClientAuthelia speakr "$oidcBlock" fi } function version239Update() { set +e sudo chown -R $USERID:82 $HSHQ_STACKS_DIR/shared/KnowledgeBases sudo chown $USERID:82 $HSHQ_STACKS_DIR/shared chmod 0444 $HSHQ_LIB_DIR/$LOCK_UTILS_FILENAME CADDY_SNIPPET_LOG_TRUE=log_true CADDY_SNIPPET_LOG_FALSE=log_false sudo sqlite3 $HSHQ_DB "PRAGMA table_info(hsvpn_connections);" | grep -q "VPNRoutingTable" || sudo sqlite3 $HSHQ_DB "ALTER TABLE hsvpn_connections ADD COLUMN VPNRoutingTable integer;" for conf in $HSHQ_WIREGUARD_DIR/vpn/*.conf do sudo grep -q "PostUp" "$conf" > /dev/null 2>&1 if [ $? -ne 0 ]; then curAbbrev=$(basename $conf | cut -d"-" -f2 | cut -d"." -f1) curTableInDB=$(sqlite3 $HSHQ_DB "select VPNRoutingTable from hsvpn_connections where CA_Abbrev='$curAbbrev' and CA_IP is not null;") if [ -z "$curTableInDB" ] || [ "$(checkValidNumber $curTableInDB)" = "false" ]; then if [ "$curAbbrev" = "$HOMESERVER_ABBREV" ]; then #Special case for our primary vpn sudo sqlite3 $HSHQ_DB "update hsvpn_connections set VPNRoutingTable=1100 where CA_Abbrev='$curAbbrev' and CA_IP is not null;" else maxTable=$(getNextHSVPNRoutingTable) sudo sqlite3 $HSHQ_DB "update hsvpn_connections set VPNRoutingTable=$maxTable where CA_Abbrev='$curAbbrev' and CA_IP is not null;" fi fi curDBID=$(sqlite3 $HSHQ_DB "select ID from hsvpn_connections where CA_Abbrev='$curAbbrev' and CA_IP is not null;") curTableInDB=$(sqlite3 $HSHQ_DB "select VPNRoutingTable from hsvpn_connections where ID=$curDBID;") curCAIP=$(sqlite3 $HSHQ_DB "select CA_IP from hsvpn_connections where ID=$curDBID;") curVPNRange=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections where ID=$curDBID;") curInterfaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$curDBID;") sudo sed -i "/^MTU =/a Table = off\nPostUp = ip route add 10.0.0.0\/8 dev %i table $curTableInDB; ip route add $curVPNRange dev %i; ip rule add fwmark $curTableInDB table $curTableInDB pref 20000; iptables -t mangle -A PREROUTING -i %i -j CONNMARK --set-mark $curTableInDB; iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark; iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff\nPreDown = iptables -t mangle -D OUTPUT -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff; iptables -t mangle -D PREROUTING -j CONNMARK --restore-mark; iptables -t mangle -D PREROUTING -i %i -j CONNMARK --set-mark $curTableInDB; ip rule del fwmark $curTableInDB table $curTableInDB pref 20000; ip route del $curVPNRange dev %i; ip route del 10.0.0.0\/8 dev %i table $curTableInDB" "$conf" sudo sed -i "s/^AllowedIPs =.*/AllowedIPs = 10.0.0.0\/8/" "$conf" sudo systemctl stop wg-quick@${curInterfaceName}.service sudo cp -f "$conf" /etc/wireguard/ sudo systemctl start wg-quick@${curInterfaceName}.service fi done clientdns_arr=($(docker ps -a --filter name=clientdns.*wireguard --format "{{.Names}}")) for curCDNS in "${clientdns_arr[@]}" do curStackName=$(echo "$curCDNS" | rev | cut -d"-" -f2- | rev) if ! [ -d "$HSHQ_STACKS_DIR/$curStackName" ] || ! [ -f "$HSHQ_STACKS_DIR/$curStackName/${curStackName}.conf" ]; then continue fi sudo sed -i "s/^AllowedIPs =.*/AllowedIPs = 10.0.0.0\/8/" "$HSHQ_STACKS_DIR/$curStackName/${curStackName}.conf" startStopStack "$curStackName" stop startStopStack "$curStackName" start done mkdir -p $HSHQ_STACKS_DIR/shared/caddylogs find $HSHQ_STACKS_DIR/caddy-common/caddyfiles -type f -exec sed -i "s/$CADDY_SNIPPET_SAFEHEADERALLOWCORS/$CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED/g" {} + find $HSHQ_STACKS_DIR/caddy-common/caddyfiles -type f -exec sed -i "/$CADDY_SNIPPET_SAFEHEADERCORSPREFLIGHT/d" {} + outputCaddyHeaders caddy_arr=($(docker ps -a --filter name=caddy- --format "{{.Names}}")) for curCH in "${caddy_arr[@]}" do updateStackEnv $curCH modFunCaddyAddLogMount done performClearIPTables true checkUpdateAllIPTables versionUpdate if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then echo "========================================================================" echo " Performing updates on RelayServer." echo "========================================================================" sleep 5 rm -f $HOME/$RS_UPDATE_SCRIPT_NAME cat < $HOME/$RS_UPDATE_SCRIPT_NAME #!/bin/bash function main() { read -r -s -p "" rspw echo "\$rspw" | sudo -S -v -p "" > /dev/null 2>&1 set +e source ~/$RS_UPDATE_UTILS_SCRIPT_NAME RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_STACKS_DIR default_iface=\$(getDefaultIface) sudo tee \$RELAYSERVER_HSHQ_STACKS_DIR/wireguard/server/wgupdown.sh >/dev/null < /dev/null 2>&1 echo "Updating RelayServer host, please wait..." sudo apt update > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1 echo "RelayServer update complete!" rm -f ~/$RS_UPDATE_UTILS_SCRIPT_NAME rm -f ~/$RS_UPDATE_SCRIPT_NAME } main EOFUR updateRelayServerWithScript true if [ $? -ne 0 ]; then echo "ERROR: The update process on the RelayServer encountered an error. Please check the logs and retry." exit fi fi } function version242Update() { outputCaddyHeaders } function version243Update() { set +e CADDY_SNIPPET_INTERNAL_GUARD=internal_guard sudo mkdir -p /var/log/caddy outputCaddyHeaders grep -q "internal_guard" $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip if [ $? -ne 0 ]; then sed -i '/ handle @subnet {/s/$/\n import internal_guard/' $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip sed -i '/respond "\/\.well-known\/matrix\/server" 200 {/s/^/ import internal_guard\n/' $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip sed -i '/ respond "Good"/s/^/ import internal_guard\n/' $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip fi caddy_arr=($(docker ps -a --filter name=caddy- --format "{{.Names}}")) for curCH in "${caddy_arr[@]}" do updateStackEnv $curCH modFunCaddyAddCrowdSec done } function version244Update() { updateSysctl } function version247Update() { setAuthKeyLiteLLM } function getNextHSVPNRoutingTable() { curTableList=($(sqlite3 $HSHQ_DB "select VPNRoutingTable from hsvpn_connections where CA_IP is not null;")) maxTableNum=1101 while [ $maxTableNum -lt 2100 ] do isTblFound=false for curTN in "${curTableList[@]}" do if ! [ -z "$maxTableNum" ] && [ $curTN -eq $maxTableNum ]; then isTblFound=true break fi done if [ "$isTblFound" = "false" ]; then echo $maxTableNum return fi ((maxTableNum++)) done echo $maxTableNum } function pruneAndUpdateDocker() { set +e docker network prune --force > /dev/null 2>&1 sudo ip rule delete priority 2000 while [ $? -eq 0 ] do sudo ip rule delete priority 2000 done upgradeDocker } function updateRelayServerWithScript() { isUploadUtils="$1" # This function assumes that a script file # called $HOME/$RS_UPDATE_SCRIPT_NAME has already # been created before executing this function. if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then chmod 500 $HOME/$RS_UPDATE_SCRIPT_NAME set +e promptTestRelayServerPassword perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "rm -f ~/$RS_UPDATE_UTILS_SCRIPT_NAME;rm -f ~/$RS_UPDATE_SCRIPT_NAME" -f -i "$USER_RELAY_SUDO_PW" -r 1 -b 1 rsRet=$? if [ $rsRet -ne 0 ]; then rm -f $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME rm -f $HOME/$RS_UPDATE_SCRIPT_NAME return $rsRet fi if [ "$isUploadUtils" = "true" ]; then outputRSUtilsScript perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":~" -f rsRet=$? if [ $rsRet -ne 0 ]; then rm -f $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME rm -f $HOME/$RS_UPDATE_SCRIPT_NAME return $rsRet fi fi perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a $HOME/$RS_UPDATE_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":~" -f rsRet=$? if [ $rsRet -ne 0 ]; then rm -f $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME rm -f $HOME/$RS_UPDATE_SCRIPT_NAME return $rsRet fi perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "bash ~/$RS_UPDATE_SCRIPT_NAME" -f -i "$USER_RELAY_SUDO_PW" -r 1 -b 1 rsRet=$? if [ $rsRet -ne 0 ]; then rm -f $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME rm -f $HOME/$RS_UPDATE_SCRIPT_NAME return $rsRet fi fi set +e rm -f $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME rm -f $HOME/$RS_UPDATE_SCRIPT_NAME } function outputRSUtilsScript() { rm -f $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME cat < $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME #!/bin/bash function init() { USERID=\$(id -u) GROUPID=\$(id -g) loadVersionVars } function getDefaultIface() { echo \$(ip route | grep -e "^default" | head -n 1 | awk -F'dev ' '{print \$2}' | xargs | cut -d" " -f1) } function setPortainerToken() { cur_timeout=5 ptok_retVal=1 ptok_numTries=1 while [ \$ptok_numTries -le 5 ] do ptok_full=\$(http --check-status --ignore-stdin --timeout=\$cur_timeout --verify=no https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/auth username=$RELAYSERVER_PORTAINER_ADMIN_USERNAME password=$RELAYSERVER_PORTAINER_ADMIN_PASSWORD) ptok_retVal=\$? if [ \$ptok_retVal -eq 0 ] && ! [ -z "\$ptok_full" ]; then # Do a sample query PORTAINER_TOKEN=\$(echo \$ptok_full | jq -r .jwt) qry=\$(http --check-status --ignore-stdin --verify=no --timeout=\$cur_timeout --print="b" GET https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":1} "Authorization: Bearer \$PORTAINER_TOKEN") if [ \$? -eq 0 ]; then break fi ptok_retVal=1 fi echo "Could not obtain Portainer token (\$ptok_numTries of 5), retrying in 5 seconds..." 1>&2 sleep 5 ((ptok_numTries++)) done } function createStackJson() { echo "{\"Name\":\"\$1\",""\$( jq -Rscjr '{StackFileContent: . }' \$2 | tail -c +2 | head -c -1 )"",\"Env\":\$(envToJson \$3)}" } function envToJson() { if [ -z "\$1" ]; then echo "[]" return fi OLDIFS=\$IFS IFS=\$(echo -en "\n\b") lines=\$(sed "s/\$(printf '\t')/\\\\t/g" \$1) jsonstring="[" for line in \$lines do key="\$(sed 's/=.*//' <<< "\$line")" value="\$(sed 's/^[^=]*=//' <<< "\$line")" value="\$(echo \$value | sed 's/\"/\\\\\"/g')" jsonstring="\$jsonstring{\"name\":\"\$key\",\"value\":\"\$value\"}," done jsonstring="\${jsonstring%?}]" IFS=\$OLDIFS echo "\$jsonstring" } function getStackID() { stackID="NA" stackName=\$1 if [ -z "\$PORTAINER_TOKEN" ]; then setPortainerToken fi qry=\$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":1} "Authorization: Bearer \$PORTAINER_TOKEN") for row in \$(echo "\${qry}" | jq -r '.[] | @base64'); do _jq() { echo \${row} | base64 --decode | jq -r \${1} } if [ "\$(_jq '.Name')" = "\$stackName" ]; then stackID=\$(_jq '.Id') break fi done if ! [ "\$stackID" = "NA" ]; then echo \$stackID fi } function startStopStack() { stackName=\$1 startStop=\$2 if [ -z "\$PORTAINER_TOKEN" ]; then setPortainerToken fi stackID=\$(getStackID \$stackName) http --check-status --ignore-stdin --verify=no --timeout=300 POST https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks/\$stackID/\$startStop endpointId==1 "Authorization: Bearer \$PORTAINER_TOKEN" > /dev/null } function startStopStackByID() { stackID=\$1 startStop=\$2 set +e if [ -z "\$PORTAINER_TOKEN" ]; then setPortainerToken fi sss_numTries=1 sss_totalTries=5 sss_retVal=1 while [ \$sss_numTries -le \$sss_totalTries ] do if [ \$sss_numTries -eq \$sss_totalTries ]; then http --check-status --ignore-stdin --verify=no --timeout=300 POST https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks/\$stackID/\$startStop endpointId==1 "Authorization: Bearer \$PORTAINER_TOKEN" else http --check-status --ignore-stdin --verify=no --timeout=300 POST https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks/\$stackID/\$startStop endpointId==1 "Authorization: Bearer \$PORTAINER_TOKEN" > /dev/null fi sss_retVal=\$? if [ \$sss_retVal -eq 0 ]; then break fi sleep 1 sudo docker container prune -f sleep 1 ((sss_numTries++)) done if [ \$sss_retVal -ne 0 ]; then echo "ERROR: Could not $startStop stack (ID=$stackID) in Portainer..." 1>&2 return \$sss_retVal fi } function createStackJson() { echo "{\"Name\":\"\$1\",""\$( jq -Rscjr '{StackFileContent: . }' \$2 | tail -c +2 | head -c -1 )"",\"Env\":\$(envToJson \$3)}" } function updateStackByID() { update_stack_name=\$1 update_stack_id=\$2 update_compose_file=\$3 update_env_file=\$4 if [ -z "\$PORTAINER_TOKEN" ]; then setPortainerToken fi echo "\$(createStackJson \$update_stack_name \$update_compose_file "\$update_env_file")" > \$HOME/\${update_stack_name}-json.tmp usid_numTries=1 usid_totalTries=5 usid_retVal=1 while [ \$usid_numTries -le \$usid_totalTries ] do http --check-status --ignore-stdin --verify=no --timeout=300 PUT https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks/\$update_stack_id endpointId==1 "Authorization: Bearer \$PORTAINER_TOKEN" @\$HOME/\${update_stack_name}-json.tmp > /dev/null 2>&1 usid_retVal=\$? if [ \$usid_retVal -eq 0 ]; then break fi ((usid_numTries++)) done if [ \$usid_retVal -ne 0 ]; then echo "ERROR: Could not update stack in Portainer..." 1>&2 return fi rm -f \$update_compose_file \$update_env_file \$HOME/\${update_stack_name}-json.tmp } function installStack() { stack_name=\$1 container_name=\$2 stack_search_string=\$3 envfile=\$4 installLogNotify "Installing Stack (\$stack_name)" sudo -v if [ -z "\$PORTAINER_TOKEN" ]; then setPortainerToken fi echo echo "Creating stack: \$stack_name" echo "\$(createStackJson \$stack_name \$HOME/\$stack_name-compose.yml "\$envfile")" > \$HOME/\$stack_name-json.tmp http --check-status --ignore-stdin --verify=no --timeout=300 https://127.0.0.1:$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT/api/stacks/create/standalone/string endpointId==1 "Authorization: Bearer \$PORTAINER_TOKEN" @\$HOME/\$stack_name-json.tmp > /dev/null search=\$stack_search_string isFound=false i=0 set +e while [ \$i -le 300 ] do findtext=\$(docker logs \$container_name 2>&1 | grep "\$search") if ! [ -z "\$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 1 second, total wait=\$i seconds..." sleep 1 i=\$((i+1)) done if ! [ "\$isFound" = "true" ]; then echo "\$stack_name did not start up correctly..." fi sleep 5 echo rm -f \$HOME/\$stack_name-json.tmp rm -f \$HOME/\$stack_name-compose.yml rm -f \$envfile } function waitForContainerLogString() { container_name="\$1" sleep_interval="\$2" max_checks="\$3" search_string="\$4" isFound=false curCheckIter=0 totalWait=0 set +e echo "Checking container log (\$container_name)..." while [ \$curCheckIter -le \$max_checks ] do findtext=\$(docker logs \$container_name 2>&1 | grep "\$search_string") if ! [ -z "\$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping \$sleep_interval seconds, total wait=\$totalWait seconds..." sleep \$sleep_interval totalWait=\$((\$totalWait+\$sleep_interval)) done if ! [ "\$isFound" = "true" ]; then echo "String not found within alotted time for container \$container_name: \$search_string" return 1 fi } function pullImage() { img_and_version=\$1 echo "Pulling Image: \$img_and_version" is_success=1 num_tries=1 set +e while [ \$is_success -ne 0 ] && [ \$num_tries -lt $MAX_DOCKER_PULL_TRIES ] do # Refresh the sudo timestamp sudo -v docker pull \$img_and_version docker image inspect \$img_and_version > /dev/null 2>&1 is_success=\$? ((num_tries++)) if [ \$is_success -ne 0 ]; then echo "Error pulling image, retrying in 15 seconds..." sleep 15 fi done if [ \$is_success -ne 0 ]; then echo "Error pulling docker image: \$img_and_version" return 5 fi } function performAptInstall() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' \$1 } function loadVersionVars() { DISTRO_ID=\$(cat /etc/*-release | grep "^ID=" | cut -d"=" -f2 | xargs | tr '[:upper:]' '[:lower:]') DISTRO_NAME=\$(cat /etc/*-release | grep "^NAME=" | cut -d"=" -f2 | xargs) DISTRO_VERSION=\$(cat /etc/*-release | grep "^VERSION=" | cut -d"=" -f2 | xargs) DISTRO_CODENAME=\$(cat /etc/*-release 2> /dev/null | grep "^VERSION_CODENAME=" | cut -d"=" -f2 | xargs) } function upgradeDocker() { echo "Upgrading docker, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null sudo apt-mark unhold docker-ce sudo apt-mark unhold docker-ce-cli case "\$DISTRO_ID" in "ubuntu") if [[ "\$DISTRO_VERSION" =~ ^22\.04. ]]; then upgradeDockerUbuntu2204 elif [[ "\$DISTRO_VERSION" =~ ^24\.04. ]]; then upgradeDockerUbuntu2404 elif [[ "\$DISTRO_VERSION" =~ ^26\.04. ]]; then upgradeDockerUbuntu2604 else echo "Linux version not found when upgrading Docker, exiting..." fi ;; "debian") if [[ "\$DISTRO_VERSION" =~ ^12. ]]; then upgradeDockerDebian12 else echo "Linux version not found when upgrading Docker, exiting..." fi ;; "linuxmint") if [[ "\$DISTRO_VERSION" =~ ^22. ]]; then upgradeDockerUbuntu2404 elif [[ "\$DISTRO_VERSION" =~ ^6. ]]; then upgradeDockerDebian12 else echo "Linux version not found when upgrading Docker, exiting..." fi ;; *) ;; esac sudo apt-mark hold docker-ce sudo apt-mark hold docker-ce-cli echo "Docker succesfully upgraded!" } function upgradeDockerUbuntu2204() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2204 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2204 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 } function upgradeDockerUbuntu2404() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2404 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2404 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 } function upgradeDockerUbuntu2604() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2604 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2604 containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null 2>&1 } function upgradeDockerDebian12() { sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_DEBIAN_12 docker-ce-cli=$DOCKER_VERSION_DEBIAN_12 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 } init EOFRS chmod 500 $HOME/$RS_UPDATE_UTILS_SCRIPT_NAME } function addAllReadonlyDBUsers() { checkAddVarsToServiceConfig "ActivePieces" "ACTIVEPIECES_DATABASE_READONLYUSER=,ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Adminer" "ADMINER_DATABASE_READONLYUSER=,ADMINER_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Automatisch" "AUTOMATISCH_DATABASE_READONLYUSER=,AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Budibase" "BUDIBASE_DATABASE_READONLYUSER=,BUDIBASE_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Calcom" "CALCOM_DATABASE_READONLYUSER=,CALCOM_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Discourse" "DISCOURSE_DATABASE_READONLYUSER=,DISCOURSE_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Dolibarr" "DOLIBARR_DATABASE_READONLYUSER=,DOLIBARR_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "EasyAppointments" "EASYAPPOINTMENTS_DATABASE_READONLYUSER=,EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "EspoCRM" "ESPOCRM_DATABASE_READONLYUSER=,ESPOCRM_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "FireflyIII" "FIREFLY_DATABASE_READONLYUSER=,FIREFLY_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "FrappeHR" "FRAPPE_HR_DATABASE_READONLYUSER=,FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "FreshRSS" "FRESHRSS_DATABASE_READONLYUSER=,FRESHRSS_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Ghost" "GHOST_DATABASE_READONLYUSER=,GHOST_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Gitea" "GITEA_DATABASE_READONLYUSER=,GITEA_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Gitlab" "GITLAB_DATABASE_READONLYUSER=,GITLAB_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Guacamole" "GUACAMOLE_DATABASE_READONLYUSER=,GUACAMOLE_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "HomeAssistant" "HOMEASSISTANT_DATABASE_READONLYUSER=,HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Huginn" "HUGINN_DATABASE_READONLYUSER=,HUGINN_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Immich" "IMMICH_DATABASE_READONLYUSER=,IMMICH_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Invidious" "INVIDIOUS_DATABASE_READONLYUSER=,INVIDIOUS_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "InvoiceNinja" "INVOICENINJA_DATABASE_READONLYUSER=,INVOICENINJA_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "InvoiceShelf" "INVOICESHELF_DATABASE_READONLYUSER=,INVOICESHELF_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Kanboard" "KANBOARD_DATABASE_READONLYUSER=,KANBOARD_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Keila" "KEILA_DATABASE_READONLYUSER=,KEILA_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "KillBill" "KILLBILL_KB_DATABASE_READONLYUSER=,KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "KillBill" "KILLBILL_KAUI_DATABASE_READONLYUSER=,KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Linkwarden" "LINKWARDEN_DATABASE_READONLYUSER=,LINKWARDEN_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mastodon" "MASTODON_DATABASE_READONLYUSER=,MASTODON_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Matomo" "MATOMO_DATABASE_READONLYUSER=,MATOMO_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Matrix" "MATRIX_DATABASE_READONLYUSER=,MATRIX_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mealie" "MEALIE_DATABASE_READONLYUSER=,MEALIE_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "MeshCentral" "MESHCENTRAL_DATABASE_READONLYUSER=,MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Metabase" "METABASE_DATABASE_READONLYUSER=,METABASE_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "MintHCM" "MINTHCM_DATABASE_READONLYUSER=,MINTHCM_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "n8n" "N8N_DATABASE_READONLYUSER=,N8N_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Nextcloud" "NEXTCLOUD_DATABASE_READONLYUSER=,NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Odoo" "ODOO_DATABASE_READONLYUSER=,ODOO_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Ombi" "OMBI_DATABASE_READONLYUSER=,OMBI_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "OpenProject" "OPENPROJECT_DATABASE_READONLYUSER=,OPENPROJECT_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Paperless" "PAPERLESS_DATABASE_READONLYUSER=,PAPERLESS_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Pastefy" "PASTEFY_DATABASE_READONLYUSER=,PASTEFY_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "PeerTube" "PEERTUBE_DATABASE_READONLYUSER=,PEERTUBE_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Penpot" "PENPOT_DATABASE_READONLYUSER=,PENPOT_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "PhotoPrism" "PHOTOPRISM_DATABASE_READONLYUSER=,PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Piped" "PIPED_DATABASE_READONLYUSER=,PIPED_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Pixelfed" "PIXELFED_DATABASE_READONLYUSER=,PIXELFED_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Rallly" "RALLLY_DATABASE_READONLYUSER=,RALLLY_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Shlink" "SHLINK_DATABASE_READONLYUSER=,SHLINK_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "StandardNotes" "STANDARDNOTES_DATABASE_READONLYUSER=,STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "SpeedtestTrackerLocal" "SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER=,SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "SpeedtestTrackerVPN" "SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER=,SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Twenty" "TWENTY_DATABASE_READONLYUSER=,TWENTY_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Vaultwarden" "VAULTWARDEN_DATABASE_READONLYUSER=,VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Wallabag" "WALLABAG_DATABASE_READONLYUSER=,WALLABAG_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Wikijs" "WIKIJS_DATABASE_READONLYUSER=$WIKIJS_DATABASE_READONLYUSER,WIKIJS_DATABASE_READONLYUSER_PASSWORD=$WIKIJS_DATABASE_READONLYUSER_PASSWORD" $CONFIG_FILE false checkAddVarsToServiceConfig "Wordpress" "WORDPRESS_DATABASE_READONLYUSER=,WORDPRESS_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Yamtrack" "YAMTRACK_DATABASE_READONLYUSER=,YAMTRACK_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Zammad" "ZAMMAD_DATABASE_READONLYUSER=,ZAMMAD_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Zulip" "ZULIP_DATABASE_READONLYUSER=,ZULIP_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Taiga" "TAIGA_DATABASE_READONLYUSER=,TAIGA_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "DocuSeal" "DOCUSEAL_DATABASE_READONLYUSER=,DOCUSEAL_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "ControlR" "CONTROLR_DATABASE_READONLYUSER=,CONTROLR_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Akaunting" "AKAUNTING_DATABASE_READONLYUSER=,AKAUNTING_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Axelor" "AXELOR_DATABASE_READONLYUSER=,AXELOR_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Langflow" "LANGFLOW_DATABASE_READONLYUSER=,LANGFLOW_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "LibreChat" "LIBRECHAT_DATABASE_READONLYUSER=,LIBRECHAT_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "OpenWebUI" "OPENWEBUI_DATABASE_READONLYUSER=,OPENWEBUI_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Khoj" "KHOJ_DATABASE_READONLYUSER=,KHOJ_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "LobeChat" "LOBECHAT_DATABASE_READONLYUSER=,LOBECHAT_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "RAGFlow" "RAGFLOW_DATABASE_READONLYUSER=,RAGFLOW_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Dify" "DIFY_DATABASE_READONLYUSER=,DIFY_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Wekan" "WEKAN_DATABASE_READONLYUSER=,WEKAN_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Revolt" "REVOLT_DATABASE_READONLYUSER=,REVOLT_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "OpenSign" "OPENSIGN_DATABASE_READONLYUSER=,OPENSIGN_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "LibreChat" "LIBRECHAT_MONGODB_READONLYUSER=,LIBRECHAT_MONGODB_READONLYUSER_PASSWORD=" $CONFIG_FILE false initServicesCredentials addReadOnlyUserToDatabase ActivePieces postgres activepieces-db $ACTIVEPIECES_DATABASE_NAME $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/activepieces/dbexport $ACTIVEPIECES_DATABASE_READONLYUSER $ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Adminer mysql adminer-db $ADMINER_DATABASE_NAME root $ADMINER_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/adminer/dbexport $ADMINER_DATABASE_READONLYUSER $ADMINER_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Automatisch postgres automatisch-db $AUTOMATISCH_DATABASE_NAME $AUTOMATISCH_DATABASE_USER $AUTOMATISCH_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/automatisch/dbexport $AUTOMATISCH_DATABASE_READONLYUSER $AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Budibase mysql budibase-db $BUDIBASE_DATABASE_NAME root $BUDIBASE_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/budibase/dbexport $BUDIBASE_DATABASE_READONLYUSER $BUDIBASE_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Calcom postgres calcom-db $CALCOM_DATABASE_NAME $CALCOM_DATABASE_USER $CALCOM_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/calcom/dbexport $CALCOM_DATABASE_READONLYUSER $CALCOM_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Discourse postgres discourse-db $DISCOURSE_DATABASE_NAME $DISCOURSE_DATABASE_USER $DISCOURSE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/discourse/dbexport $DISCOURSE_DATABASE_READONLYUSER $DISCOURSE_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Dolibarr mysql dolibarr-db $DOLIBARR_DATABASE_NAME root $DOLIBARR_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/dolibarr/dbexport $DOLIBARR_DATABASE_READONLYUSER $DOLIBARR_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase EasyAppointments mysql easyappointments-db $EASYAPPOINTMENTS_DATABASE_NAME root $EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/easyappointments/dbexport $EASYAPPOINTMENTS_DATABASE_READONLYUSER $EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase EspoCRM mysql espocrm-db $ESPOCRM_DATABASE_NAME root $ESPOCRM_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/espocrm/dbexport $ESPOCRM_DATABASE_READONLYUSER $ESPOCRM_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase FireflyIII postgres firefly-db $FIREFLY_DATABASE_NAME $FIREFLY_DATABASE_USER $FIREFLY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/firefly/dbexport $FIREFLY_DATABASE_READONLYUSER $FIREFLY_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase FrappeHR mysql frappe-hr-db $FRAPPE_HR_DATABASE_NAME root $FRAPPE_HR_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/frappe-hr/dbexport $FRAPPE_HR_DATABASE_READONLYUSER $FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase FreshRSS postgres freshrss-db $FRESHRSS_DATABASE_NAME $FRESHRSS_DATABASE_USER $FRESHRSS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/freshrss/dbexport $FRESHRSS_DATABASE_READONLYUSER $FRESHRSS_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Ghost mysql ghost-db $GHOST_DATABASE_NAME root $GHOST_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/ghost/dbexport $GHOST_DATABASE_READONLYUSER $GHOST_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Gitea postgres gitea-db $GITEA_DATABASE_NAME $GITEA_DATABASE_USER $GITEA_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/gitea/dbexport $GITEA_DATABASE_READONLYUSER $GITEA_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Gitlab postgres gitlab-db $GITLAB_DATABASE_NAME $GITLAB_DATABASE_USER $GITLAB_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/gitlab/dbexport $GITLAB_DATABASE_READONLYUSER $GITLAB_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Guacamole mysql guacamole-db $GUACAMOLE_DATABASE_NAME root $GUACAMOLE_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/guacamole/dbexport $GUACAMOLE_DATABASE_READONLYUSER $GUACAMOLE_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase HomeAssistant postgres homeassistant-db $HOMEASSISTANT_DATABASE_NAME $HOMEASSISTANT_DATABASE_USER $HOMEASSISTANT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/homeassistant/dbexport $HOMEASSISTANT_DATABASE_READONLYUSER $HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Huginn postgres huginn-db $HUGINN_DATABASE_NAME $HUGINN_DATABASE_USER $HUGINN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/huginn/dbexport $HUGINN_DATABASE_READONLYUSER $HUGINN_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Immich postgres immich-db $IMMICH_DATABASE_NAME $IMMICH_DATABASE_USER $IMMICH_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/immich/dbexport $IMMICH_DATABASE_READONLYUSER $IMMICH_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Invidious postgres invidious-db $INVIDIOUS_DATABASE_NAME $INVIDIOUS_DATABASE_USER $INVIDIOUS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/invidious/dbexport $INVIDIOUS_DATABASE_READONLYUSER $INVIDIOUS_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase InvoiceNinja mysql invoiceninja-db $INVOICENINJA_DATABASE_NAME root $INVOICENINJA_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/invoiceninja/dbexport $INVOICENINJA_DATABASE_READONLYUSER $INVOICENINJA_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase InvoiceShelf postgres invoiceshelf-db $INVOICESHELF_DATABASE_NAME $INVOICESHELF_DATABASE_USER $INVOICESHELF_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/invoiceshelf/dbexport $INVOICESHELF_DATABASE_READONLYUSER $INVOICESHELF_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Kanboard postgres kanboard-db $KANBOARD_DATABASE_NAME $KANBOARD_DATABASE_USER $KANBOARD_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/kanboard/dbexport $KANBOARD_DATABASE_READONLYUSER $KANBOARD_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Keila postgres keila-db $KEILA_DATABASE_NAME $KEILA_DATABASE_USER $KEILA_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/keila/dbexport $KEILA_DATABASE_READONLYUSER $KEILA_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase KillBill mysql killbill-db $KILLBILL_KB_DATABASE_NAME root $KILLBILL_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/killbill/dbexport $KILLBILL_KB_DATABASE_READONLYUSER $KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase KillBill mysql killbill-db $KILLBILL_KAUI_DATABASE_NAME root $KILLBILL_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/killbill/dbexport $KILLBILL_KAUI_DATABASE_READONLYUSER $KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Linkwarden postgres linkwarden-db $LINKWARDEN_DATABASE_NAME $LINKWARDEN_DATABASE_USER $LINKWARDEN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/linkwarden/dbexport $LINKWARDEN_DATABASE_READONLYUSER $LINKWARDEN_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Mastodon postgres mastodon-db $MASTODON_DATABASE_NAME $MASTODON_DATABASE_USER $MASTODON_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/mastodon/dbexport $MASTODON_DATABASE_READONLYUSER $MASTODON_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Matomo mysql matomo-db $MATOMO_DATABASE_NAME root $MATOMO_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/matomo/dbexport $MATOMO_DATABASE_READONLYUSER $MATOMO_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Matrix postgres matrix-db $MATRIX_DATABASE_NAME $MATRIX_DATABASE_USER $MATRIX_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/matrix/dbexport $MATRIX_DATABASE_READONLYUSER $MATRIX_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Mealie postgres mealie-db $MEALIE_DATABASE_NAME $MEALIE_DATABASE_USER $MEALIE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/mealie/dbexport $MEALIE_DATABASE_READONLYUSER $MEALIE_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase MeshCentral mysql meshcentral-db $MESHCENTRAL_DATABASE_NAME root $MESHCENTRAL_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/meshcentral/dbexport $MESHCENTRAL_DATABASE_READONLYUSER $MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Metabase postgres metabase-db $METABASE_DATABASE_NAME $METABASE_DATABASE_USER $METABASE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/metabase/dbexport $METABASE_DATABASE_READONLYUSER $METABASE_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase MintHCM mysql minthcm-db $MINTHCM_DATABASE_NAME root $MINTHCM_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/minthcm/dbexport $MINTHCM_DATABASE_READONLYUSER $MINTHCM_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase n8n postgres n8n-db $N8N_DATABASE_NAME $N8N_DATABASE_USER $N8N_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/n8n/dbexport $N8N_DATABASE_READONLYUSER $N8N_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Nextcloud postgres nextcloud-db $NEXTCLOUD_DATABASE_NAME $NEXTCLOUD_DATABASE_USER $NEXTCLOUD_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/nextcloud/dbexport $NEXTCLOUD_DATABASE_READONLYUSER $NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Odoo postgres odoo-db $ODOO_DATABASE_NAME $ODOO_DATABASE_USER $ODOO_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/odoo/dbexport $ODOO_DATABASE_READONLYUSER $ODOO_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Ombi mysql ombi-db $OMBI_DATABASE_NAME root $OMBI_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/ombi/dbexport $OMBI_DATABASE_READONLYUSER $OMBI_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase OpenProject postgres openproject-db $OPENPROJECT_DATABASE_NAME $OPENPROJECT_DATABASE_USER $OPENPROJECT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/openproject/dbexport $OPENPROJECT_DATABASE_READONLYUSER $OPENPROJECT_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Paperless postgres paperless-db $PAPERLESS_DATABASE_NAME $PAPERLESS_DATABASE_USER $PAPERLESS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/paperless/dbexport $PAPERLESS_DATABASE_READONLYUSER $PAPERLESS_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Pastefy mysql pastefy-db $PASTEFY_DATABASE_NAME root $PASTEFY_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/pastefy/dbexport $PASTEFY_DATABASE_READONLYUSER $PASTEFY_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase PeerTube postgres peertube-db $PEERTUBE_DATABASE_NAME $PEERTUBE_DATABASE_USER $PEERTUBE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/peertube/dbexport $PEERTUBE_DATABASE_READONLYUSER $PEERTUBE_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Penpot postgres penpot-db $PENPOT_DATABASE_NAME $PENPOT_DATABASE_USER $PENPOT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/penpot/dbexport $PENPOT_DATABASE_READONLYUSER $PENPOT_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase PhotoPrism mysql photoprism-db $PHOTOPRISM_DATABASE_NAME root $PHOTOPRISM_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/photoprism/dbexport $PHOTOPRISM_DATABASE_READONLYUSER $PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Piped postgres piped-db $PIPED_DATABASE_NAME $PIPED_DATABASE_USER $PIPED_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/piped/dbexport $PIPED_DATABASE_READONLYUSER $PIPED_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Pixelfed mysql pixelfed-db $PIXELFED_DATABASE_NAME root $PIXELFED_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/pixelfed/dbexport $PIXELFED_DATABASE_READONLYUSER $PIXELFED_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Rallly postgres rallly-db $RALLLY_DATABASE_NAME $RALLLY_DATABASE_USER $RALLLY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/rallly/dbexport $RALLLY_DATABASE_READONLYUSER $RALLLY_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Shlink postgres shlink-db $SHLINK_DATABASE_NAME $SHLINK_DATABASE_USER $SHLINK_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/shlink/dbexport $SHLINK_DATABASE_READONLYUSER $SHLINK_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase StandardNotes mysql standardnotes-db $STANDARDNOTES_DATABASE_NAME root $STANDARDNOTES_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/standardnotes/dbexport $STANDARDNOTES_DATABASE_READONLYUSER $STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase SpeedtestTrackerLocal postgres speedtest-tracker-local-db $SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/speedtest-tracker-local/dbexport $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase SpeedtestTrackerVPN postgres speedtest-tracker-vpn-db $SPEEDTEST_TRACKER_VPN_DATABASE_NAME $SPEEDTEST_TRACKER_VPN_DATABASE_USER $SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/speedtest-tracker-vpn/dbexport $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Twenty postgres twenty-db $TWENTY_DATABASE_NAME $TWENTY_DATABASE_USER $TWENTY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/twenty/dbexport $TWENTY_DATABASE_READONLYUSER $TWENTY_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Vaultwarden postgres vaultwarden-db $VAULTWARDEN_DATABASE_NAME $VAULTWARDEN_DATABASE_USER $VAULTWARDEN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/vaultwarden/dbexport $VAULTWARDEN_DATABASE_READONLYUSER $VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Wallabag postgres wallabag-db $WALLABAG_DATABASE_NAME $WALLABAG_DATABASE_USER $WALLABAG_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/wallabag/dbexport $WALLABAG_DATABASE_READONLYUSER $WALLABAG_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Wiki.js postgres wikijs-db $WIKIJS_DATABASE_NAME $WIKIJS_DATABASE_USER $WIKIJS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/wikijs/dbexport $WIKIJS_DATABASE_READONLYUSER $WIKIJS_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Wordpress mysql wordpress-db $WORDPRESS_DATABASE_NAME root $WORDPRESS_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/wordpress/dbexport $WORDPRESS_DATABASE_READONLYUSER $WORDPRESS_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Yamtrack postgres yamtrack-db $YAMTRACK_DATABASE_NAME $YAMTRACK_DATABASE_USER $YAMTRACK_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/yamtrack/dbexport $YAMTRACK_DATABASE_READONLYUSER $YAMTRACK_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Zammad postgres zammad-db $ZAMMAD_DATABASE_NAME $ZAMMAD_DATABASE_USER $ZAMMAD_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/zammad/dbexport $ZAMMAD_DATABASE_READONLYUSER $ZAMMAD_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Zulip postgres zulip-db $ZULIP_DATABASE_NAME $ZULIP_DATABASE_USER $ZULIP_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/zulip/dbexport $ZULIP_DATABASE_READONLYUSER $ZULIP_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Taiga postgres taiga-db $TAIGA_DATABASE_NAME $TAIGA_DATABASE_USER $TAIGA_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/taiga/dbexport $TAIGA_DATABASE_READONLYUSER $TAIGA_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase DocuSeal postgres docuseal-db $DOCUSEAL_DATABASE_NAME $DOCUSEAL_DATABASE_USER $DOCUSEAL_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/docuseal/dbexport $DOCUSEAL_DATABASE_READONLYUSER $DOCUSEAL_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase ControlR postgres controlr-db $CONTROLR_DATABASE_NAME $CONTROLR_DATABASE_USER $CONTROLR_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/controlr/dbexport $CONTROLR_DATABASE_READONLYUSER $CONTROLR_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Akaunting mysql akaunting-db $AKAUNTING_DATABASE_NAME root $AKAUNTING_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/akaunting/dbexport $AKAUNTING_DATABASE_READONLYUSER $AKAUNTING_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Axelor postgres axelor-db $AXELOR_DATABASE_NAME $AXELOR_DATABASE_USER $AXELOR_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/axelor/dbexport $AXELOR_DATABASE_READONLYUSER $AXELOR_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Langflow postgres langflow-db $LANGFLOW_DATABASE_NAME $LANGFLOW_DATABASE_USER $LANGFLOW_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/langflow/dbexport $LANGFLOW_DATABASE_READONLYUSER $LANGFLOW_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase LibreChat postgres librechat-db $LIBRECHAT_DATABASE_NAME $LIBRECHAT_DATABASE_USER $LIBRECHAT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/librechat/dbexport $LIBRECHAT_DATABASE_READONLYUSER $LIBRECHAT_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase OpenWebUI postgres openwebui-db $OPENWEBUI_DATABASE_NAME $OPENWEBUI_DATABASE_USER $OPENWEBUI_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/openwebui/dbexport $OPENWEBUI_DATABASE_READONLYUSER $OPENWEBUI_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Khoj postgres khoj-db $KHOJ_DATABASE_NAME $KHOJ_DATABASE_USER $KHOJ_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/khoj/dbexport $KHOJ_DATABASE_READONLYUSER $KHOJ_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase LobeChat postgres lobechat-db $LOBECHAT_DATABASE_NAME $LOBECHAT_DATABASE_USER $LOBECHAT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/lobechat/dbexport $LOBECHAT_DATABASE_READONLYUSER $LOBECHAT_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase RAGFlow mysql ragflow-db $RAGFLOW_DATABASE_NAME root $RAGFLOW_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/ragflow/dbexport $RAGFLOW_DATABASE_READONLYUSER $RAGFLOW_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Dify postgres dify-db $DIFY_DATABASE_NAME $DIFY_DATABASE_USER $DIFY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/dify/dbexport $DIFY_DATABASE_READONLYUSER $DIFY_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Wekan mongodb wekan-db $WEKAN_DATABASE_NAME $WEKAN_DATABASE_USER $WEKAN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/wekan/dbexport $WEKAN_DATABASE_READONLYUSER $WEKAN_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase Revolt mongodb revolt-db $REVOLT_DATABASE_NAME $REVOLT_DATABASE_USER $REVOLT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/revolt/dbexport $REVOLT_DATABASE_READONLYUSER $REVOLT_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase OpenSign mongodb opensign-db $OPENSIGN_DATABASE_NAME $OPENSIGN_DATABASE_USER $OPENSIGN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/opensign/dbexport $OPENSIGN_DATABASE_READONLYUSER $OPENSIGN_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase LibreChat mongodb librechat-mongodb $LIBRECHAT_MONGODB_DATABASE $LIBRECHAT_MONGODB_USER $LIBRECHAT_MONGODB_USER_PASSWORD $HSHQ_STACKS_DIR/librechat/mongoexport $LIBRECHAT_MONGODB_READONLYUSER $LIBRECHAT_MONGODB_READONLYUSER_PASSWORD } function fixPortainerAndUpgradeDockerV188() { set +e startStopStack uptimekuma stop rsi_numTries=1 rsi_totalTries=5 rsi_retVal=1 rstackIDsQry="" if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi if [ $? -ne 0 ]; then echo "ERROR: Could not get Portainer auth token..." 1>&2 releaseLock networkchecks restartAllStacks false return 2 fi while [ $rsi_numTries -le $rsi_totalTries ] do rstackIDsQry=$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN") rsi_retVal=$? if [ $rsi_retVal -eq 0 ]; then break fi ((rsi_numTries++)) done if [ $rsi_retVal -ne 0 ]; then echo "ERROR: Could not get list of stack IDs in Portainer..." 1>&2 releaseLock networkchecks restartAllStacks false return 3 fi # Need to use a temp resolver since Adguard goes down for a bit... sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 docker container prune -f removeDockerNetworks echo "Replacing portainer env vars..." # Retain all env vars fixvars=($(cat $HSHQ_STACKS_DIR/portainer/portainer.env | cut -d"=" -f1)) for curVar in "${fixvars[@]}" do if ! [ "${curVar:0:10}" = "PORTAINER_" ]; then sudo find $HSHQ_STACKS_DIR/portainer/compose -type f -exec sed -i "s/\$$curVar/\$PORTAINER_$curVar/g" {} \; sudo find $HSHQ_STACKS_DIR/portainer/compose -type f -exec sed -i "s/\${$curVar}/\${PORTAINER_$curVar}/g" {} \; fi done upgradeDocker echo "Restarting Docker..." sudo systemctl restart docker createDockerNetworks outputEnvPortainer sed -i "s/\/run\/secrets\/portainer/\/run\/portainer\/portainer/g" $HSHQ_STACKS_DIR/portainer/docker-compose.yml sed -i "s/image:.*/image: mirror.gcr.io\/portainer\/portainer-ce:2.33.1-alpine/" $HSHQ_STACKS_DIR/portainer/docker-compose.yml sed -i "s/${STACK_VERSION_PREFIX}.*/${STACK_VERSION_PREFIX} portainer v4/" $HSHQ_STACKS_DIR/portainer/docker-compose.yml startPortainer if [ $? -ne 0 ]; then echo "There was an error restarting portainer. You will need to determine the problem and restart all of the stacks manually." sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 if [ $? -ne 0 ]; then echo "$addBlock" >> $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip echo "import sn-sub-${SUB_BIND_IP}" >> $HSHQ_STACKS_DIR/caddy-common/caddyfiles/CaddyfileBody-Home fi caddy_arr=($(docker ps -a --filter name=caddy-home --format "{{.Names}}")) for curCH in "${caddy_arr[@]}" do docker container restart $curCH > /dev/null 2>&1 done } function modFunFixCaddyHostInterfaceVar() { echo "Updating caddy stack: ${updateStackName}" sed -i "s/\${HOMESERVER_HOST_/\${PORTAINER_HOMESERVER_HOST_/g" $HOME/${updateStackName}.env sed -i "s/CADDY_HSHQ_BIND_IP=.*/CADDY_HSHQ_BIND_IP=\${$(getHSHostIPVarName $curHomeInt)}/" $HOME/${updateStackName}.env } function modFunCaddyHomeBindIPFix() { set +e grep "CADDY_HSHQ_BIND_IP" $HOME/${updateStackName}.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "CADDY_HSHQ_BIND_IP=$fixBindIP" >> $HOME/${updateStackName}.env fi } function modFunCaddyAddLogMount() { set +e grep -q "caddylogs" $HOME/${updateStackName}-compose.yml > /dev/null 2>&1 if [ $? -ne 0 ]; then sed -i -E 's/^([[:space:]]*)(.*\/config:\/config)$/\1\2\n\1- \${PORTAINER_HSHQ_STACKS_DIR}\/shared\/caddylogs:\/logs/' $HOME/${updateStackName}-compose.yml fi grep -q "ENABLE_LOGGING" $HOME/${updateStackName}.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ENABLE_LOGGING=$CADDY_SNIPPET_LOG_FALSE" >> $HOME/${updateStackName}.env echo "CADDY_STACK_NAME=$updateStackName" >> $HOME/${updateStackName}.env fi } function modFunCaddyAddCrowdSec() { set +e grep -q "crowdsec_global" $HSHQ_STACKS_DIR/${updateStackName}/Caddyfile > /dev/null 2>&1 if [ $? -ne 0 ]; then sed -i 's|\(renew_interval {\$CERT_RENEW_INTERVAL}\)|\1\n import /snippets/crowdsec_global_{\$ENABLE_CROWDSEC_BOUNCER:false}.snip|g' $HSHQ_STACKS_DIR/${updateStackName}/Caddyfile fi sed -i 's|- \${PORTAINER_HSHQ_STACKS_DIR}/shared/caddylogs:/logs|- /var/log/caddy:/logs|g' $HOME/${updateStackName}-compose.yml sed -i 's|ENABLE_LOGGING=.*|ENABLE_ACCESS_LOGGING=false|g' $HOME/${updateStackName}.env grep -q "CROWDSEC_API_KEY" $HOME/${updateStackName}.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "CROWDSEC_API_KEY=$CROWDSEC_API_KEY" >> $HOME/${updateStackName}.env echo "ENABLE_CROWDSEC_BOUNCER=false" >> $HOME/${updateStackName}.env echo "ENABLE_CROWDSEC_APPSEC=false" >> $HOME/${updateStackName}.env fi } function modFunAutheliaConfigFilterVar() { set +e grep "X_AUTHELIA_CONFIG_FILTERS" $HOME/${updateStackName}.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "X_AUTHELIA_CONFIG_FILTERS=template" >> $HOME/${updateStackName}.env fi } function moveVarsToPlaintextFile() { updatePlaintextRootConfigVar HOMENET_ADDITIONAL_IPS "$HOMENET_ADDITIONAL_IPS" updatePlaintextRootConfigVar HSHQ_VERSION $HSHQ_VERSION updatePlaintextRootConfigVar HOMESERVER_DOMAIN $HOMESERVER_DOMAIN updatePlaintextRootConfigVar HOMESERVER_NAME "$HOMESERVER_NAME" updatePlaintextRootConfigVar HOMESERVER_ABBREV $HOMESERVER_ABBREV updatePlaintextRootConfigVar EMAIL_ADMIN_EMAIL_ADDRESS $EMAIL_ADMIN_EMAIL_ADDRESS updatePlaintextRootConfigVar EXT_DOMAIN_PREFIX $EXT_DOMAIN_PREFIX updatePlaintextRootConfigVar INT_DOMAIN_PREFIX $INT_DOMAIN_PREFIX updatePlaintextRootConfigVar USERID "$USERID" updatePlaintextRootConfigVar GROUPID "$GROUPID" updatePlaintextRootConfigVar TZ "$TZ" updatePlaintextRootConfigVar SSH_PORT $SSH_PORT updatePlaintextRootConfigVar CURRENT_SSH_PORT $CURRENT_SSH_PORT updatePlaintextRootConfigVar ADMIN_USERNAME_BASE $ADMIN_USERNAME_BASE updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE updatePlaintextRootConfigVar DESKTOP_ENV $DESKTOP_ENV updatePlaintextRootConfigVar HSHQ_BASE_DIR $HSHQ_BASE_DIR updatePlaintextRootConfigVar DISABLED_SERVICES "$DISABLED_SERVICES" updatePlaintextRootConfigVar ADMIN_COLOR_CODE $ADMIN_COLOR_CODE updatePlaintextRootConfigVar USERS_COLOR_CODE $USERS_COLOR_CODE updatePlaintextRootConfigVar HOMESERVERS_COLOR_CODE $HOMESERVERS_COLOR_CODE updatePlaintextRootConfigVar RELAYSERVER_COLOR_CODE $RELAYSERVER_COLOR_CODE updatePlaintextRootConfigVar WIREGUARD_DNS_REFRESH_RATE $WIREGUARD_DNS_REFRESH_RATE updatePlaintextRootConfigVar LECERTS_REFRESH_RATE $LECERTS_REFRESH_RATE updatePlaintextRootConfigVar DOCKER_NETWORK_RESERVED_RANGE $DOCKER_NETWORK_RESERVED_RANGE updatePlaintextRootConfigVar DOCKER_NETWORK_SIZE $DOCKER_NETWORK_SIZE updatePlaintextRootConfigVar DOCKER_METRICS_PORT $DOCKER_METRICS_PORT updatePlaintextRootConfigVar IS_AUTO_UPDATE_NETWORK true updatePlaintextRootConfigVar CERTS_IS_CA_INIT $CERTS_IS_CA_INIT updatePlaintextRootConfigVar CERTS_ROOT_CA_NAME "$CERTS_ROOT_CA_NAME" updatePlaintextRootConfigVar CERTS_EMAIL_ADDRESS "$CERTS_EMAIL_ADDRESS" updatePlaintextRootConfigVar CERTS_INTERNAL_DHPARAMS_KEYLENGTH $CERTS_INTERNAL_DHPARAMS_KEYLENGTH updatePlaintextRootConfigVar CERTS_INTERNAL_COUNTRY "$CERTS_INTERNAL_COUNTRY" updatePlaintextRootConfigVar CERTS_INTERNAL_STATE "$CERTS_INTERNAL_STATE" updatePlaintextRootConfigVar CERTS_INTERNAL_LOCALITY "$CERTS_INTERNAL_LOCALITY" updatePlaintextRootConfigVar CERTS_INTERNAL_ORG_NAME "$CERTS_INTERNAL_ORG_NAME" updatePlaintextRootConfigVar CERTS_INTERNAL_OU_NAME "$CERTS_INTERNAL_OU_NAME" updatePlaintextRootConfigVar CERTS_INTERNAL_ROOT_CN "$CERTS_INTERNAL_ROOT_CN" updatePlaintextRootConfigVar CERTS_INTERNAL_INTERMEDIATE_CN "$CERTS_INTERNAL_INTERMEDIATE_CN" updatePlaintextRootConfigVar CERTS_INTERNAL_CA_DAYS $CERTS_INTERNAL_CA_DAYS updatePlaintextRootConfigVar PORTAINER_ADMIN_USERNAME $PORTAINER_ADMIN_USERNAME updatePlaintextRootConfigVar PORTAINER_ADMIN_PASSWORD $PORTAINER_ADMIN_PASSWORD updatePlaintextRootConfigVar PORTAINER_LOCAL_HTTPS_PORT $PORTAINER_LOCAL_HTTPS_PORT updatePlaintextRootConfigVar SCRIPTSERVER_LOCALHOST_PORT $SCRIPTSERVER_LOCALHOST_PORT updatePlaintextRootConfigVar ADGUARD_LOCALHOST_PORT $ADGUARD_LOCALHOST_PORT updatePlaintextRootConfigVar ADGUARD_ADMIN_USERNAME $ADGUARD_ADMIN_USERNAME updatePlaintextRootConfigVar ADGUARD_ADMIN_PASSWORD $ADGUARD_ADMIN_PASSWORD updatePlaintextRootConfigVar HOMEASSISTANT_LOCALHOST_PORT $HOMEASSISTANT_LOCALHOST_PORT updatePlaintextRootConfigVar HOMEASSISTANT_DB_LOCALHOST_PORT $HOMEASSISTANT_DB_LOCALHOST_PORT sudo sed -i "/^RELAYSERVER_WG_VPN_SUBNET=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMENET_ADDITIONAL_IPS=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_HOST_PRIMARY_INTERFACE_IP=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_HOST_ISPRIVATE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_HOST_ISPRIVATE_IP=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_HOST_RANGE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^WAZUH_AGENT_VERSION=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^DEFAULT_NETWORK_POOL=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^DEFAULT_NETWORK_SIZE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HSHQ_VERSION=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_DOMAIN=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_NAME=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_ABBREV=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^EMAIL_ADMIN_EMAIL_ADDRESS=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^EXT_DOMAIN_PREFIX=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^INT_DOMAIN_PREFIX=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^USERID=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^GROUPID=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^TZ=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^SSH_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CURRENT_SSH_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^ADMIN_USERNAME_BASE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^PRIMARY_VPN_SETUP_TYPE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^DESKTOP_ENV=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HSHQ_BASE_DIR=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^DISABLED_SERVICES=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^ADMIN_COLOR_CODE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^USERS_COLOR_CODE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVERS_COLOR_CODE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^RELAYSERVER_COLOR_CODE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^WIREGUARD_DNS_REFRESH_RATE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^LECERTS_REFRESH_RATE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^DOCKER_NETWORK_RESERVED_RANGE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^DOCKER_NETWORK_SIZE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^IS_AUTO_UPDATE_NETWORK=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_IS_CA_INIT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_ROOT_CA_NAME=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_EMAIL_ADDRESS=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_DHPARAMS_KEYLENGTH=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_COUNTRY=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_STATE=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_LOCALITY=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_ORG_NAME=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_OU_NAME=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_ROOT_CN=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_INTERMEDIATE_CN=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^CERTS_INTERNAL_CA_DAYS=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^# Certs BEGIN/d" $CONFIG_FILE >/dev/null sudo sed -i "/^# Certs END/d" $CONFIG_FILE >/dev/null sudo sed -i "/^PORTAINER_ADMIN_USERNAME=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^PORTAINER_ADMIN_PASSWORD=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^PORTAINER_LOCAL_HTTPS_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^SCRIPTSERVER_LOCALHOST_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^ADGUARD_LOCALHOST_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^ADGUARD_ADMIN_USERNAME=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^ADGUARD_ADMIN_PASSWORD=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMEASSISTANT_LOCALHOST_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMEASSISTANT_DB_LOCALHOST_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^DOCKER_METRICS_PORT=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^JITSI_ADVERTISE_IPS=/d" $CONFIG_FILE >/dev/null sudo sed -i "/^# Jitsi (Service Details) BEGIN/d" $CONFIG_FILE >/dev/null sudo sed -i "/^# Jitsi (Service Details) END/d" $CONFIG_FILE >/dev/null sudo sed -i "/^# Adguard (Service Details) BEGIN/d" $CONFIG_FILE >/dev/null sudo sed -i "/^# Adguard (Service Details) END/d" $CONFIG_FILE >/dev/null sudo sed -i "/^HOMESERVER_HOST_IP=/d" $CONFIG_FILE >/dev/null cat -s $CONFIG_FILE > $HSHQ_CONFIG_DIR/tmpfile mv $HSHQ_CONFIG_DIR/tmpfile $CONFIG_FILE } function fixNetplanBackport() { set +e if sudo test -f /etc/netplan/00-installer-config.yaml; then sudo grep "optional:" /etc/netplan/00-installer-config.yaml > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo sed -i '/nameservers:/i\ optional: true' /etc/netplan/00-installer-config.yaml sudo netplan apply > /dev/null 2>&1 fi fi } function fixInterfaceNames() { if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then if [ "${RELAYSERVER_WG_VPN_NETNAME: -4}" = "-vpn" ] && ! [ "${RELAYSERVER_WG_VPN_NETNAME:0:4}" = "vpn-" ]; then old_vpn_name=$RELAYSERVER_WG_VPN_NETNAME new_vpn_name="vpn-${RELAYSERVER_WG_VPN_NETNAME%????}" if sudo test -f $HSHQ_WIREGUARD_DIR/users/${old_vpn_name}-user1.conf; then sudo mv $HSHQ_WIREGUARD_DIR/users/${old_vpn_name}-user1.conf $HSHQ_WIREGUARD_DIR/users/${new_vpn_name}-user1.conf fi RELAYSERVER_WG_VPN_NETNAME=$new_vpn_name updateConfigVar RELAYSERVER_WG_VPN_NETNAME $RELAYSERVER_WG_VPN_NETNAME fi if [ "${RELAYSERVER_WG_INTERNET_NETNAME: -4}" = "-ext" ] && ! [ "${RELAYSERVER_WG_INTERNET_NETNAME:0:4}" = "ext-" ]; then old_ext_name=$RELAYSERVER_WG_INTERNET_NETNAME new_ext_name="ext-${RELAYSERVER_WG_INTERNET_NETNAME%????}" RELAYSERVER_WG_INTERNET_NETNAME=$new_ext_name updateConfigVar RELAYSERVER_WG_INTERNET_NETNAME $RELAYSERVER_WG_INTERNET_NETNAME fi fi check_vpnIDs=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType = 'homeserver_vpn' and NetworkType in ('primary','other');")) for curVPNID in "${check_vpnIDs[@]}" do curVPN=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID = '$curVPNID';") curIP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID = '$curVPNID';") curIsPrimary=$(sqlite3 $HSHQ_DB "select IsPrimary from hsvpn_connections where ID = '$curVPNID';") curCA_abbrev=$(sqlite3 $HSHQ_DB "select CA_Abbrev from hsvpn_connections where ID = '$curVPNID';") curCA_URL=$(sqlite3 $HSHQ_DB "select CA_URL from hsvpn_connections where ID = '$curVPNID';") curCA_Subdomain=$(sqlite3 $HSHQ_DB "select CA_Subdomain from hsvpn_connections where ID = '$curVPNID';") curCA_IP=$(sqlite3 $HSHQ_DB "select CA_IP from hsvpn_connections where ID = '$curVPNID';") primary_string=other if [ $curIsPrimary = 1 ]; then primary_string=primary fi if [ "${curVPN: -4}" = "-vpn" ] && ! [ "${curVPN:0:4}" = "vpn-" ]; then old_vpn_name=$curVPN new_vpn_name="vpn-${curVPN%????}" if sudo test -f /etc/wireguard/${old_vpn_name}.conf; then sudo cp /etc/wireguard/${old_vpn_name}.conf /etc/wireguard/${new_vpn_name}.conf removeWGInterfaceQuick ${old_vpn_name} enableWGInterfaceQuick ${new_vpn_name} sudo mv $HSHQ_WIREGUARD_DIR/vpn/${old_vpn_name}.conf $HSHQ_WIREGUARD_DIR/vpn/${new_vpn_name}.conf fi docker ps | grep caddy-${old_vpn_name} > /dev/null 2>&1 if [ $? -eq 0 ]; then deleteStack caddy-${old_vpn_name} sudo mv $HSHQ_STACKS_DIR/caddy-${old_vpn_name} $HSHQ_STACKS_DIR/caddy-${new_vpn_name} outputConfigCaddy ${new_vpn_name} caddy-${new_vpn_name} $primary_string ${new_vpn_name} $curIP $curCA_abbrev $curCA_URL $curCA_Subdomain $curCA_IP installStack caddy-${new_vpn_name} caddy-${new_vpn_name} "serving initial configuration" $HOME/caddy-${new_vpn_name}.env fi sudo sqlite3 $HSHQ_DB "update connections set InterfaceName = '${new_vpn_name}' where InterfaceName = '${old_vpn_name}';" fi done check_ext=($(sqlite3 $HSHQ_DB "select InterfaceName from connections where ConnectionType = 'homeserver_internet' and NetworkType in ('primary','other');")) for curEXT in "${check_ext[@]}" do if [ "${curEXT: -4}" = "-ext" ] && ! [ "${curEXT:0:4}" = "ext-" ]; then old_ext_name=$curEXT new_ext_name="ext-${curEXT%????}" if sudo test -f $HSHQ_WIREGUARD_DIR/internet/${old_ext_name}.conf; then sudo cp $HSHQ_WIREGUARD_DIR/internet/${old_ext_name}.conf $HSHQ_WIREGUARD_DIR/internet/${new_ext_name}.conf updateConfigVarInFile "#NETNAME" "${new_ext_name}" $HSHQ_WIREGUARD_DIR/internet/${new_ext_name}.conf root sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${old_ext_name}.conf down sudo rm -f $HSHQ_WIREGUARD_DIR/internet/${old_ext_name}.conf sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${new_ext_name}.conf up fi sudo sqlite3 $HSHQ_DB "update connections set InterfaceName = '${new_ext_name}' where InterfaceName = '${old_ext_name}';" fi done } function fixMailuNetworkNames() { startStopStack mailu stop docker network rm dock-mailu-ext > /dev/null docker network rm dock-mailu-int > /dev/null docker network create -o com.docker.network.bridge.name=$NET_MAILU_EXT_BRIDGE_NAME --driver=bridge --subnet $NET_MAILU_EXT_SUBNET dock-mailu-ext > /dev/null docker network create -o com.docker.network.bridge.name=$NET_MAILU_INT_BRIDGE_NAME --driver=bridge --subnet $NET_MAILU_INT_SUBNET --internal dock-mailu-int > /dev/null startStopStack mailu start } function mfFixCACertPath() { sed -i "s/\/usr\/local\/share\/ca-certificates\/${CERTS_ROOT_CA_NAME}.crt/\${PORTAINER_HSHQ_SSL_DIR}\/${CERTS_ROOT_CA_NAME}.crt/g" $HOME/${updateStackName}-compose.yml } function sendRSExposeScripts() { cat < $HOME/addLECertDomains.sh #!/bin/bash subdomlist=\$1 RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_STACKS_DIR function main() { set +e leCertsArr=(\$(echo "\$subdomlist" | tr "," "\n")) for subdom in "\${leCertsArr[@]}" do if [ -z "\$subdom" ]; then continue; fi grep "# LE certs path \$subdom BEGIN" \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile >/dev/null if [ \$? -ne 0 ]; then strBlock="" strBlock=\$strBlock"# LE certs path \$subdom BEGIN\n" strBlock=\$strBlock"http://\$subdom {\n" strBlock=\$strBlock" handle /.well-known/acme-challenge/* {\n" strBlock=\$strBlock" reverse_proxy \$subdom {\n" strBlock=\$strBlock" import sn-resolver\n" strBlock=\$strBlock" }\n" strBlock=\$strBlock" }\n" strBlock=\$strBlock"}\n" strBlock=\$strBlock"# LE certs path \$subdom END\n" echo -e "\$strBlock" >> \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile fi done docker container restart caddy } main "\$@" EOFCD chmod 0500 $HOME/addLECertDomains.sh cat < $HOME/removeLECertDomains.sh #!/bin/bash set +e RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_STACKS_DIR function main() { subdomlist="\$1" leCertsArr=(\$(echo "\$subdomlist" | tr "," "\n")) for subdom in "\${leCertsArr[@]}" do if [ -z "\$subdom" ]; then continue; fi sed -i "/# LE certs path \$subdom BEGIN/,/# LE certs path \$subdom END/d" \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile done cat -s \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile > \$HOME/tmpcadfile mv \$HOME/tmpcadfile \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile docker container restart caddy } main "\$@" EOFEX chmod 500 $HOME/removeLECertDomains.sh cat < $HOME/addExposeDomains.sh #!/bin/bash RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_STACKS_DIR function main() { subdomlist="\$1" exposeArr=(\$(echo "\$subdomlist" | tr "," "\n")) for subdom in "\${exposeArr[@]}" do if [ -z "\$subdom" ]; then continue; fi grep "# Expose domain \$subdom BEGIN" \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile >/dev/null if [ \$? -ne 0 ]; then strBlock="" strBlock=\$strBlock"# Expose domain \$subdom BEGIN\n" strBlock=\$strBlock"https://\$subdom {\n" strBlock=\$strBlock" import safe-header\n" strBlock=\$strBlock" reverse_proxy https://\$subdom {\n" strBlock=\$strBlock" import sn-resolver\n" strBlock=\$strBlock" }\n" strBlock=\$strBlock"}\n" strBlock=\$strBlock"# Expose domain \$subdom END\n" echo -e "\$strBlock" >> \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile fi done docker container restart caddy } main "\$@" EOFEX chmod 500 $HOME/addExposeDomains.sh cat < $HOME/removeExposeDomains.sh #!/bin/bash set +e RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_STACKS_DIR function main() { subdomlist="\$1" exposeArr=(\$(echo "\$subdomlist" | tr "," "\n")) for subdom in "\${exposeArr[@]}" do if [ -z "\$subdom" ]; then continue; fi sed -i "/# Expose domain \$subdom BEGIN/,/# Expose domain \$subdom END/d" \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile done cat -s \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile > \$HOME/tmpcadfile mv \$HOME/tmpcadfile \$RELAYSERVER_HSHQ_STACKS_DIR/caddy/Caddyfile docker container restart caddy } main "\$@" EOFEX chmod 500 $HOME/removeExposeDomains.sh perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "rm -f $RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addLECertDomains.sh $RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeLECertDomains.sh $RELAYSERVER_HSHQ_SCRIPTS_DIR/user/addExposeDomains.sh $RELAYSERVER_HSHQ_SCRIPTS_DIR/user/removeExposeDomains.sh" -f perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a "$HOME/addLECertDomains.sh" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/" -f perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a "$HOME/removeLECertDomains.sh" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/" -f perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a "$HOME/addExposeDomains.sh" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/" -f perfRemoteAction -m scp -p $RELAYSERVER_SSH_PORT -a "$HOME/removeExposeDomains.sh" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c ":$RELAYSERVER_HSHQ_SCRIPTS_DIR/user/" -f rm -f $HOME/addLECertDomains.sh rm -f $HOME/removeLECertDomains.sh rm -f $HOME/addExposeDomains.sh rm -f $HOME/removeExposeDomains.sh } function checkFixPortainerEnv() { # A bug was introduced in the version 22 update. set +e grep "HSHQ_RELAYSERVER_DIR" $HSHQ_STACKS_DIR/portainer/portainer.env > /dev/null 2>&1 if [ $? -ne 0 ]; then stopPortainer outputConfigPortainer startPortainer startStopStack duplicati stop startStopStack syncthing stop sleep 5 startStopStack duplicati start startStopStack syncthing start fi } function fixAutheliaConfig() { inputfile=$HSHQ_STACKS_DIR/authelia/config/configuration.yml set +e grep "# Authelia bypass BEGIN" $inputfile > /dev/null 2>&1 if [ $? -eq 0 ]; then return fi outres="" OLDIFS=$IFS IFS=$(echo -en "\n\r") readarray -t inputArr < $inputfile curnum=0 while [ $curnum -lt ${#inputArr[@]} ] do while [ $curnum -lt ${#inputArr[@]} ] do outres="${outres}${inputArr[$curnum]}\n" if [[ "${inputArr[$curnum]}" =~ access_control ]]; then break; fi curnum=$((curnum+1)) done curnum=$((curnum+1)) while [ $curnum -lt ${#inputArr[@]} ] do outres="${outres}${inputArr[$curnum]}\n" if [[ "${inputArr[$curnum]}" =~ domain: ]]; then break; fi curnum=$((curnum+1)) done curnum=$((curnum+1)) outres="${outres}# Authelia bypass BEGIN\n" while [ $curnum -lt ${#inputArr[@]} ] do if [[ "${inputArr[$curnum]}" =~ policy: ]]; then break; fi outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) done outres="${outres}# Authelia bypass END\n" outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) while [ $curnum -lt ${#inputArr[@]} ] do outres="${outres}${inputArr[$curnum]}\n" if [[ "${inputArr[$curnum]}" =~ domain: ]]; then break; fi curnum=$((curnum+1)) done curnum=$((curnum+1)) outres="${outres}# Authelia $LDAP_BASIC_USER_GROUP_NAME BEGIN\n" while [ $curnum -lt ${#inputArr[@]} ] do if [[ "${inputArr[$curnum]}" =~ policy: ]]; then break; fi outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) done outres="${outres}# Authelia $LDAP_BASIC_USER_GROUP_NAME END\n" outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) while [ $curnum -lt ${#inputArr[@]} ] do outres="${outres}${inputArr[$curnum]}\n" if [[ "${inputArr[$curnum]}" =~ domain: ]]; then break; fi curnum=$((curnum+1)) done curnum=$((curnum+1)) outres="${outres}# Authelia ${LDAP_PRIMARY_USER_GROUP_NAME} BEGIN\n" while [ $curnum -lt ${#inputArr[@]} ] do if [[ "${inputArr[$curnum]}" =~ policy: ]]; then break; fi outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) done outres="${outres}# Authelia ${LDAP_PRIMARY_USER_GROUP_NAME} END\n" outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) while [ $curnum -lt ${#inputArr[@]} ] do outres="${outres}${inputArr[$curnum]}\n" if [[ "${inputArr[$curnum]}" =~ domain: ]]; then break; fi curnum=$((curnum+1)) done curnum=$((curnum+1)) outres="${outres}# Authelia ${LDAP_ADMIN_USER_GROUP_NAME} BEGIN\n" while [ $curnum -lt ${#inputArr[@]} ] do if [[ "${inputArr[$curnum]}" =~ policy: ]]; then break; fi outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) done outres="${outres}# Authelia ${LDAP_ADMIN_USER_GROUP_NAME} END\n" outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) while [ $curnum -lt ${#inputArr[@]} ] do outres="${outres}${inputArr[$curnum]}\n" curnum=$((curnum+1)) done done IFS=$OLDIFS echo -e "$outres" > $inputfile docker container restart authelia > /dev/null 2>&1 docker ps | grep codeserver > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart codeserver > /dev/null 2>&1 fi } function checkImageList() { imageList=$1 composeFile="$2" imageListArr=($(echo $imageList | tr "," "\n")) for curImg in "${imageListArr[@]}" do if ! [ -z "$curImg" ]; then sudo grep "$curImg$" "$composeFile" > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "false" return fi fi done echo "true" } function insertVersionNumber() { stackName=$1 versionNum="$2" imageList=$3 composeFile="$4" check_images="$(checkImageList $imageList $composeFile)" if [ "$check_images" = "false" ]; then echo "false" return fi if ! [ "$(checkStackHSHQManaged $(sudo sed -n 1p $composeFile))" = "true" ]; then sudo sed -i "1 i$STACK_VERSION_PREFIX $stackName $versionNum" $composeFile else sudo sed -i "1s|.*|$STACK_VERSION_PREFIX $stackName $versionNum|" $composeFile fi echo "true" } function setVersionOnStacks() { # This function modifies the variable strSetVersionReport to report the results to the caller # The caller is responsible for doing something with it. set +e refreshSudo strSetVersionReport="" stackListArr=($(echo ${HSHQ_REQUIRED_STACKS}","${HSHQ_OPTIONAL_STACKS} | tr "," "\n")) if [ -z "$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi for curStack in "${stackListArr[@]}" do echo "Versioning ${curStack} stack..." stackID=$(getStackID $curStack) if ! [ "$curStack" = "portainer" ] && [ -z "$stackID" ]; then continue fi curCompose=$HSHQ_STACKS_DIR/portainer/compose/$stackID/docker-compose.yml case "$curStack" in portainer) curVersion=v1 curImageList=portainer/portainer-ce:2.19.3-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $HSHQ_STACKS_DIR/portainer/docker-compose.yml)" = "true" ]; then continue;fi curVersion=v2 curImageList=portainer/portainer-ce:2.19.4-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $HSHQ_STACKS_DIR/portainer/docker-compose.yml)" = "true" ]; then continue;fi strSetVersionReport="${strSetVersionReport}\n $curStack stack version not found. All images from this stack:\n$(sudo grep image: $HSHQ_STACKS_DIR/portainer/docker-compose.yml)" continue ;; adguard) curVersion=v1 curImageList=adguard/adguardhome:v0.107.41 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=adguard/adguardhome:v0.107.43 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; sysutils) curVersion=v1 curImageList=grafana/grafana-oss:9.5.8,prom/prometheus:v2.46.0,prom/node-exporter:v1.6.1,influxdb:2.7.1-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=grafana/grafana-oss:9.5.15,prom/prometheus:v2.48.1,prom/node-exporter:v1.7.0,influxdb:2.7.4-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=grafana/grafana-oss:10.3.1,prom/prometheus:v2.49.1,prom/node-exporter:v1.7.0,influxdb:2.7.5-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; openldap) curVersion=v1 curImageList=osixia/openldap:1.5.0,osixia/phpldapadmin:stable,wheelybird/ldap-user-manager:v1.11 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; mailu) curVersion=v1 curImageList=bitnami/redis:7.0.5,ghcr.io/mailu/admin:2.0,ghcr.io/mailu/rspamd:2.0,ghcr.io/mailu/clamav:2.0,ghcr.io/mailu/fetchmail:2.0,ghcr.io/mailu/nginx:2.0,ghcr.io/mailu/dovecot:2.0,ghcr.io/mailu/oletools:2.0,ghcr.io/mailu/postfix:2.0,ghcr.io/mailu/unbound:2.0,ghcr.io/mailu/radicale:2.0,ghcr.io/mailu/webmail:2.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=bitnami/redis:7.0.5,ghcr.io/mailu/admin:2.0.37,ghcr.io/mailu/rspamd:2.0.37,ghcr.io/mailu/clamav:2.0.37,ghcr.io/mailu/fetchmail:2.0.37,ghcr.io/mailu/nginx:2.0.37,ghcr.io/mailu/dovecot:2.0.37,ghcr.io/mailu/oletools:2.0.37,ghcr.io/mailu/postfix:2.0.37,ghcr.io/mailu/unbound:2.0.37,ghcr.io/mailu/radicale:2.0.37,ghcr.io/mailu/webmail:2.0.37 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; wazuh) curVersion=v1 curImageList=wazuh/wazuh-manager:4.6.0,wazuh/wazuh-indexer:4.6.0,wazuh/wazuh-dashboard:4.6.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=wazuh/wazuh-manager:4.7.1,wazuh/wazuh-indexer:4.7.1,wazuh/wazuh-dashboard:4.7.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=wazuh/wazuh-manager:4.7.2,wazuh/wazuh-indexer:4.7.2,wazuh/wazuh-dashboard:4.7.2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; collabora) curVersion=v1 curImageList=collabora/code:23.05.5.3.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=collabora/code:23.05.6.4.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=collabora/code:23.05.8.2.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; nextcloud) curVersion=v1 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:27.1.3-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.23.2-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:27.1.5-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:27.1.6-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; jitsi) curVersion=v1 curImageList=jitsi/jicofo:stable-8719,jitsi/jvb:stable-8719,jitsi/prosody:stable-8719,jitsi/web:stable-8719 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=jitsi/jicofo:stable-9111,jitsi/jvb:stable-9111,jitsi/prosody:stable-9111,jitsi/web:stable-9111 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=jitsi/jicofo:stable-9220,jitsi/jvb:stable-9220,jitsi/prosody:stable-9220,jitsi/web:stable-9220 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; matrix) curVersion=v1 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.90.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.40 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.98.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.52 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.100.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.57 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; wikijs) curVersion=v1 curImageList=postgres:15.0-bullseye,requarks/wiki:2.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; duplicati) curVersion=v1 curImageList=linuxserver/duplicati:2.0.7 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; mastodon) curVersion=v1 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.1.6,nginx:1.23.2-alpine,elasticsearch:8.8.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.2.3,nginx:1.25.3-alpine,elasticsearch:8.11.3 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.2.3,nginx:1.25.3-alpine,elasticsearch:8.12.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; dozzle) curVersion=v1 curImageList=amir20/dozzle:v4.10.26 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=amir20/dozzle:v5.8.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=amir20/dozzle:v6.0.8 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v4 curImageList=amir20/dozzle:v6.1.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; searxng) curVersion=v1 curImageList=searxng/searxng:2023.8.19-018b0a932 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=searxng/searxng:2023.12.29-27e26b3d6 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; jellyfin) curVersion=v1 curImageList=jellyfin/jellyfin:10.8.10 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=jellyfin/jellyfin:10.8.13 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; filebrowser) curVersion=v1 curImageList=filebrowser/filebrowser:v2.24.2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=filebrowser/filebrowser:v2.26.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=filebrowser/filebrowser:v2.27.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; photoprism) curVersion=v1 curImageList=mariadb:10.7.3,photoprism/photoprism:220901-bullseye if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; guacamole) curVersion=v1 curImageList=guacamole/guacd:1.5.3,guacamole/guacamole:1.5.3 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=guacamole/guacd:1.5.4,guacamole/guacamole:1.5.4 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; authelia) curVersion=v1 curImageList=authelia/authelia:4.37.5,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; wordpress) curVersion=v1 curImageList=mariadb:10.7.3,wordpress:php8.2-apache if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=mariadb:10.7.3,wordpress:php8.3-apache if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; ghost) curVersion=v1 curImageList=mariadb:10.7.3,ghost:5.59.1-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=mariadb:10.7.3,ghost:5.75.2-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=mariadb:10.7.3,ghost:5.78.0-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; peertube) curVersion=v1 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v5.2.0-bullseye,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v6.0.2-bookworm,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; homeassistant) curVersion=v1 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2023.8,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.1.3,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.1.5,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v4 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.1.6,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; gitlab) curVersion=v1 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.2.4-ce.0,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.7.0-ce.0,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.8.1-ce.0,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; vaultwarden) curVersion=v1 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.29.1-alpine,thegeeklab/vaultwarden-ldap:0.6.2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.30.1-alpine,thegeeklab/vaultwarden-ldap:0.6.2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.30.2-alpine,thegeeklab/vaultwarden-ldap:0.6.2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; discourse) curVersion=v1 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.0.6,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.1.3,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.1.4,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; syncthing) curVersion=v1 curImageList=syncthing/syncthing:1.23.7 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=syncthing/syncthing:1.27.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=syncthing/syncthing:1.27.2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; codeserver) curVersion=v1 curImageList=codercom/code-server:4.16.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=codercom/code-server:4.20.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=codercom/code-server:4.20.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; shlink) curVersion=v1 curImageList=postgres:15.0-bullseye,shlinkio/shlink:3.6.3,shlinkio/shlink-web-client:3.10.2,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,shlinkio/shlink:3.7.2,shlinkio/shlink-web-client:3.10.2,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,shlinkio/shlink:3.7.3,shlinkio/shlink-web-client:4.0.0,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; firefly) curVersion=v1 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.0.20,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.1.1,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.1.7,bitnami/redis:7.0.5 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; excalidraw) curVersion=v1 curImageList=excalidraw/excalidraw-room,kiliandeca/excalidraw-storage-backend,kiliandeca/excalidraw if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; drawio) curVersion=v1 curImageList=jgraph/drawio:21.0.2,jgraph/plantuml-server,jgraph/export-server if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=jgraph/drawio:23.1.0,jgraph/plantuml-server,jgraph/export-server if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; invidious) curVersion=v1 curImageList=postgres:15.0-bullseye,quay.io/invidious/invidious if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; ittools) curVersion=v1 curImageList=ghcr.io/corentinth/it-tools:latest if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; gitea) curVersion=v1 curImageList=postgres:15.0-bullseye,gitea/gitea:1.20.3 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,gitea/gitea:1.21.3 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,gitea/gitea:1.21.4 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; mealie) curVersion=v1 curImageList=hkotel/mealie:v0.5.6 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v1.0.0-RC2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v1.1.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; kasm) curVersion=v1 curImageList=lscr.io/linuxserver/kasm:1.13.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=lscr.io/linuxserver/kasm:1.14.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; ntfy) curVersion=v1 curImageList=binwiederhier/ntfy:v2.7.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=binwiederhier/ntfy:v2.8.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; remotely) curVersion=v1 curImageList=immybot/remotely:69 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; calibre) curVersion=v1 curImageList=linuxserver/calibre:7.3.0,linuxserver/calibre-web:0.6.21 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=linuxserver/calibre:7.4.0,linuxserver/calibre-web:0.6.21 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; netdata) curVersion=v1 curImageList=netdata/netdata:v1.44.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; linkwarden) curVersion=v1 curImageList=postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.4.8 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; stirlingpdf) curVersion=v1 curImageList=frooodle/s-pdf:0.19.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=frooodle/s-pdf:0.20.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; bar-assistant) curVersion=v1 curImageList=barassistant/server:v3,getmeili/meilisearch:v1.4,bitnami/redis:7.0.5,barassistant/salt-rim:v2,nginx:1.25.3-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=barassistant/server:v3,getmeili/meilisearch:v1.6,bitnami/redis:7.0.5,barassistant/salt-rim:v2,nginx:1.25.3-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; freshrss) curVersion=v1 curImageList=postgres:15.0-bullseye,freshrss/freshrss:1.23.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; keila) curVersion=v1 curImageList=postgres:15.0-bullseye,pentacent/keila:0.13.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=postgres:15.0-bullseye,pentacent/keila:0.14.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; wallabag) curVersion=v1 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,wallabag/wallabag:2.6.8 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; heimdall) curVersion=v1 curImageList=linuxserver/heimdall:2.4.13 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; ofelia) curVersion=v1 curImageList=mcuadros/ofelia:v0.3.7 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=mcuadros/ofelia:v0.3.9 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; sqlpad) curVersion=v1 curImageList=sqlpad/sqlpad:7.1.2 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=sqlpad/sqlpad:7.2.0 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v3 curImageList=sqlpad/sqlpad:7.3.1 if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; uptimekuma) curVersion=v1 curImageList=louislam/uptime-kuma:1.23.0-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=louislam/uptime-kuma:1.23.11-alpine if [ "$(insertVersionNumber $curStack $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi ;; esac strSetVersionReport="${strSetVersionReport}\n $curStack stack version not found. All images from this stack:\n$(sudo grep image: $curCompose)" done # Special case for Caddy qry=$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN") caddy_stack_ids=($(echo $qry | jq '.[] | select (.Name | startswith("caddy-")) | .Id')) caddy_stack_names=($(echo $qry | jq -r '.[] | select (.Name | startswith("caddy-")) | .Name')) i=-1 for curStackID in "${caddy_stack_ids[@]}" do i=$((i+1)) echo "Versioning ${caddy_stack_names[i]} stack..." curCompose=$HSHQ_STACKS_DIR/portainer/compose/$curStackID/docker-compose.yml curVersion=v1 curImageList=caddy:2.7.4 if [ "$(insertVersionNumber ${caddy_stack_names[i]} $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi curVersion=v2 curImageList=caddy:2.7.6 if [ "$(insertVersionNumber ${caddy_stack_names[i]} $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi strSetVersionReport="${strSetVersionReport}\n caddy stack version not found. All images from this stack:\n$(sudo grep image: $curCompose)" done # Special case for ClientDNS clientdns_stack_ids=($(echo $qry | jq '.[] | select (.Name | startswith("clientdns-")) | .Id')) clientdns_stack_names=($(echo $qry | jq -r '.[] | select (.Name | startswith("clientdns-")) | .Name')) i=-1 for curStackID in "${clientdns_stack_ids[@]}" do i=$((i+1)) echo "Versioning ${clientdns_stack_names[i]} stack..." curCompose=$HSHQ_STACKS_DIR/portainer/compose/$curStackID/docker-compose.yml curVersion=v1 curImageList=jpillora/dnsmasq:1.1,linuxserver/wireguard:1.0.20210914 if [ "$(insertVersionNumber ${clientdns_stack_names[i]} $curVersion $curImageList $curCompose)" = "true" ]; then continue;fi strSetVersionReport="${strSetVersionReport}\n clientdns stack version not found. All images from this stack:\n$(sudo grep image: $curCompose)" done } function installHostNTPServer() { if [[ "$(isProgramInstalled ntpq)" = "false" ]]; then echo "Installing ntp server, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 performAptInstall ntpsec > /dev/null 2>&1 fi set +e grep "127.127.1.0" /etc/ntp.conf > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "server 127.127.1.0" | sudo tee -a /etc/ntp.conf >/dev/null echo "fudge 127.127.1.0 stratum 10" | sudo tee -a /etc/ntp.conf >/dev/null fi sudo systemctl enable ntpsec > /dev/null 2>&1 sudo systemctl restart ntpsec > /dev/null 2>&1 } function notifyRSLogin() { echo -e "\n\nThe RelayServer requires an update which requires root privileges." echo -e "You will be prompted for your sudo password on the RelayServer.\n" echo -e "Enter either 'update' to perform the update or 'bypass' to ignore it." echo -e "Only choose bypass if you no longer have access to the RelayServer." echo -e "Otherwise, you'll have to manually perform the necessary updates." is_continue="" while ! [ "$is_continue" = "update" ] && ! [ "$is_continue" = "bypass" ] do read -r -p "Enter 'update' to perform the update or 'bypass' to ignore: " is_continue if [ "$is_continue" = "bypass" ]; then is_confirm="" while ! [ "$is_confirm" = "confirm" ] && ! [ "$is_confirm" = "cancel" ] do echo -e " @@@@@@@@@@ !!! WARNING !!! @@@@@@@@@@" echo -e "This could cause unforseen side effects to your RelayServer." echo -e "Certain functions with your network, etc. might not work properly." echo -e "Only select this option if you no longer have access to this" echo -e "server, and you intend to remove it and create a new one.\n\n" read -r -p "Enter 'confirm' or 'cancel': " is_confirm done if [ "$is_confirm" = "cancel" ]; then is_continue="" fi fi done if [ "$is_continue" = "update" ]; then return 0 elif [ "$is_continue" = "bypass" ]; then return 1 else return 2 fi } function checkAddServiceToConfig() { casc_curE=${-//[^e]/} service_name="$1" variable_list="$2" conf_file="$3" isRoot="$4" set +e if ! [ -z "$isRoot" ] && [ "$isRoot" = "true" ]; then sudo grep "# $service_name (Service Details)" $conf_file > /dev/null 2>&1 else grep "# $service_name (Service Details)" $conf_file > /dev/null 2>&1 fi if [ $? -ne 0 ]; then replace_block="# $service_name (Service Details) BEGIN\n" varListArr=($(echo $variable_list | tr "," "\n")) for curVar in "${varListArr[@]}" do replace_block=$replace_block"${curVar}\n" done replace_block=$replace_block"# $service_name (Service Details) END\n\n# Service Details END" if ! [ -z "$isRoot" ] && [ "$isRoot" = "true" ]; then sudo sed -i "s|# Service Details END|$replace_block|g" $conf_file else sed -i "s|# Service Details END|$replace_block|g" $conf_file fi fi set +e if ! [ -z "$casc_curE" ]; then set -e fi } function checkAddVarsToServiceConfig() { cavc_curE=${-//[^e]/} service_name="$1" variable_list="$2" conf_file="$3" isRoot="$4" set +e varListArr=($(echo $variable_list | tr "," "\n")) for curVar in "${varListArr[@]}" do curVarCheck=$(echo $curVar | cut -d"=" -f1)"=" if ! [ -z "$isRoot" ] && [ "$isRoot" = "true" ]; then sudo grep "$curVarCheck" $conf_file > /dev/null 2>&1 else grep "$curVarCheck" $conf_file > /dev/null 2>&1 fi if [ $? -ne 0 ]; then replace_block="$curVar\n# $service_name (Service Details) END" if ! [ -z "$isRoot" ] && [ "$isRoot" = "true" ]; then sudo sed -i "s|# $service_name (Service Details) END|$replace_block|g" $conf_file else sed -i "s|# $service_name (Service Details) END|$replace_block|g" $conf_file fi fi done set +e if ! [ -z "$cavc_curE" ]; then set -e fi } function removeServiceFromConfig() { rsfc_curE=${-//[^e]/} service_name=$1 set +e grep "# $service_name (Service Details)" $CONFIG_FILE > /dev/null 2>&1 if [ $? -eq 0 ]; then removeTextBlockInFile "# $service_name (Service Details) BEGIN" "# $service_name (Service Details) END" $CONFIG_FILE fi set +e if ! [ -z "$rsfc_curE" ]; then set -e fi } function outputScripts() { outputBootScripts outputDBExportScripts outputWireGuardScripts outputMaintenanceScripts } function nukeHSHQ() { set +e if [ -z "$HSHQ_BASE_DIR" ]; then HSHQ_BASE_DIR=$HOME/hshq; fi if [ -z "$HSHQ_WIREGUARD_DIR" ]; then HSHQ_WIREGUARD_DIR=$HOME/hshq/data/wireguard; fi if [ -z "$HSHQ_SCRIPTS_DIR" ]; then HSHQ_SCRIPTS_DIR=$HOME/hshq/data/scripts; fi clear echo echo echo echo echo "============================================================" echo "This script will entirely remove HSHQ and all artifacts." echo "It will permanently delete ALL data except for the backup" echo "directory and reboot upon completion." echo "It will not delete any previously downloaded docker images." echo "In order to remove them as well, type 'docker image prune -f'" echo "Please type 'nuclear' in order to continue." echo "============================================================" echo echo read -r -p "Type 'nuclear' (no quotes): " isnuke if ! [ "$isnuke" = "nuclear" ]; then echo "String does not match, exiting..." return 1 fi sudo -k sudo crontab -r killall ssh-agent > /dev/null 2>&1 sudo docker ps -q | xargs sudo docker stop sleep 5 sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternetDownAll.sh sudo $HSHQ_SCRIPTS_DIR/root/clearRoutingTable.sh sudo rm -f /etc/networkd-dispatcher/routable.d/performNetworkingChecks.sh sudo rm -f /etc/NetworkManager/dispatcher.d/performNetworkingChecks.sh sudo systemctl stop runOnBootRootBeforeNetwork sudo systemctl disable runOnBootRootBeforeNetwork sudo rm -f /etc/systemd/system/runOnBootRootBeforeNetwork.service sudo systemctl stop runOnBootRootAfterDocker sudo systemctl disable runOnBootRootAfterDocker sudo rm -f /etc/systemd/system/runOnBootRootAfterDocker.service sudo systemctl stop runScriptServer sudo systemctl disable runScriptServer sudo rm -f /etc/systemd/system/runScriptServer.service performClearIPTables false sudo rm -f /etc/sysctl.d/88-hshq.conf sudo sysctl --system > /dev/null 2>&1 sudo docker container prune -f sudo docker volume rm $(sudo docker volume ls -q) sudo docker network prune -f sudo ls /etc/wireguard | while read fname do bname=$(basename $fname .conf) removeWGInterfaceQuick $bname done sudo rm -f /etc/resolv.conf > /dev/null 2>&1 echo "nameserver 9.9.9.9" | sudo tee /etc/resolv.conf sudo systemctl status systemd-resolved > /dev/null 2>&1 if [ $? -ne 4 ]; then sudo systemctl unmask systemd-resolved > /dev/null 2>&1 sudo systemctl enable systemd-resolved > /dev/null 2>&1 sudo systemctl start systemd-resolved > /dev/null 2>&1 sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf fi sudo systemctl restart docker sudo docker container prune -f sudo docker volume rm $(sudo docker volume ls -q) sudo docker network prune -f removeWazuhAgent sudo rm -f /usr/local/share/ca-certificates/* sudo update-ca-certificates sudo systemctl stop docker sudo systemctl start docker sudo rm -fr $HSHQ_BASE_DIR/data sudo rm -fr $HSHQ_BASE_DIR/nonbackup sudo rm -fr $HSHQ_BASE_DIR/*.log sudo rm -fr $HSHQ_INSTALL_CFG sudo rm -fr $HOME/.ssh/* sudo rm -f /etc/update-motd.d/88-hshq if [ -f /etc/motd.old ]; then sudo mv /etc/motd.old /etc/motd else sudo chmod +x /etc/update-motd.d/* fi for curLock in $ALL_LOCKS do releaseLock $curLock "nukem" true done if [ "$IS_DESKTOP_ENV" = "true" ]; then rm -f "/home/$USERNAME/Desktop/$HSHQ_INSTALL_NOTES_FILENAME" rm -f ~/Desktop/HomePage.desktop rm -f ~/Desktop/HSHQConsole.desktop rm -f ~/Desktop/HSHQScriptServer.desktop sudo rm -f /usr/share/applications/HomePage.desktop sudo rm -f /usr/share/applications/HSHQConsole.desktop sudo rm -f /usr/share/applications/HSHQScriptServer.desktop which terminator > /dev/null 2>&1 if [ $? -eq 0 ]; then # Specify terminator as console if possible cat < ~/Desktop/InstallHSHQ.desktop [Desktop Entry] Name=Install HSHQ Exec=/usr/bin/terminator -x bash -ic "~/hshq.sh" Comment=Starts the HSHQ installation script Icon=/usr/share/icons/HSHQ/InstallHSHQ.png Type=Application Categories=HomeServerHQ EOFHP else cat < ~/Desktop/InstallHSHQ.desktop [Desktop Entry] Name=Install HSHQ Exec=bash -ic "~/hshq.sh" Terminal=true Comment=Starts the HSHQ installation script Icon=/usr/share/icons/HSHQ/InstallHSHQ.png Type=Application Categories=HomeServerHQ EOFHP fi chmod 755 ~/Desktop/InstallHSHQ.desktop trustDesktopIcon "InstallHSHQ.desktop" sudo cp -f ~/Desktop/InstallHSHQ.desktop /usr/share/applications/ tryDeleteRootCAFirefox fi rm -f "/home/$USERNAME/$HSHQ_INSTALL_NOTES_FILENAME" exit 2 } function outputBootScripts() { sudo mkdir -p $HSHQ_SCRIPTS_DIR/boot/beforenetwork sudo rm -f $HSHQ_SCRIPTS_DIR/boot/onBootRootBeforeNetwork.sh sudo tee $HSHQ_SCRIPTS_DIR/boot/onBootRootBeforeNetwork.sh >/dev/null </dev/null </dev/null </dev/null < /dev/null 2>&1 if [ $? -eq 0 ]; then isLogInfo=false fi if [ "$isLogInfo" = "true" ]; then logHSHQEvent info "checkUpdateAllIPTables ($netstate) - BEGIN" fi # If one wants to run any script(s) prior to this function, # then create the following bash script accordingly. There # is also a post script hook at the end of this function. if [ -f $HSHQ_CUSTOM_PRE_IPTABLES_SCRIPT ]; then bash $HSHQ_CUSTOM_PRE_IPTABLES_SCRIPT "$netstate" fi isInit=false sudo iptables -t raw -n -L chain-icmp > /dev/null 2>&1 if [ $? -ne 0 ]; then isInit=true fi sudo iptables -t raw -n -L chain-bad_tcp > /dev/null 2>&1 if [ $? -ne 0 ]; then isInit=true fi sudo iptables -t raw -n -L chain-ipspoof > /dev/null 2>&1 if [ $? -ne 0 ]; then isInit=true fi sudo iptables -t filter -n -L DOCKER-USER > /dev/null 2>&1 if [ $? -ne 0 ]; then isInit=true fi if [ "$netstate" = "prenetwork" ] || [ "$isInit" = "true" ]; then logHSHQEvent info "checkUpdateAllIPTables ($netstate) - Initializing chains..." sudo iptables -t raw -F PREROUTING > /dev/null 2>&1 sudo iptables -t raw -F chain-icmp > /dev/null 2>&1 sudo iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 sudo iptables -t raw -F chain-ipspoof > /dev/null 2>&1 sudo iptables -t raw -X chain-icmp > /dev/null 2>&1 sudo iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 sudo iptables -t raw -X chain-ipspoof > /dev/null 2>&1 sudo iptables -t raw -N chain-icmp > /dev/null 2>&1 sudo iptables -t raw -N chain-bad_tcp > /dev/null 2>&1 sudo iptables -t raw -N chain-ipspoof > /dev/null 2>&1 comment="HSHQ_BEGIN INPUT Temp Keep Established HSHQ_END" sudo iptables -I INPUT -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "$comment" -j ACCEPT > /dev/null 2>&1 while true; do sudo iptables -L INPUT -n --line-numbers | cut -d" " -f1 | grep "2" > /dev/null 2>&1 if [ $? -eq 0 ]; then sudo iptables -D INPUT 2 > /dev/null 2>&1 else break fi done sudo iptables -t filter -F DOCKER-USER > /dev/null 2>&1 sudo iptables -t filter -N DOCKER-USER > /dev/null 2>&1 sudo iptables -t raw -A PREROUTING -p icmp -j chain-icmp > /dev/null 2>&1 sudo iptables -t raw -A PREROUTING -j chain-ipspoof > /dev/null 2>&1 sudo iptables -t raw -A PREROUTING -p tcp -m tcp -j chain-bad_tcp > /dev/null 2>&1 sudo iptables -t raw -P PREROUTING ACCEPT > /dev/null 2>&1 fi # Docker likes to append this during boot sudo iptables -D DOCKER-USER -j RETURN > /dev/null 2>&1 sudo iptables -t filter -n -L > /tmp/ipt.txt sudo iptables -t raw -n -L >> /tmp/ipt.txt sudo rm -f /tmp/newRules.txt touch /tmp/newRules.txt # See https://gist.github.com/mattia-beta/bd5b1c68e3d51db933181d8a3dc0ba64?permalink_comment_id=3728715#gistcomment-3728715 # chain-icmp comment="HSHQ_BEGIN chain-icmp echo-reply HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type echo-reply -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp network-unreachable HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type network-unreachable -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp host-unreachable HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type host-unreachable -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp protocol-unreachable HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type protocol-unreachable -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp port-unreachable HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type port-unreachable -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp fragmentation-needed HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type fragmentation-needed -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp echo-request all HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type echo-request -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp time-exceeded HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type time-exceeded -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp parameter-problem HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type parameter-problem -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN chain-icmp any HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-icmp -p icmp -m icmp --icmp-type any -m comment --comment "$comment" -j DROP' # chain-bad_tcp comment="HSHQ_BEGIN chain-bad_tcp 01 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 02 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,SYN FIN,SYN -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 03 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,RST SYN,RST -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 04 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags SYN,FIN SYN,FIN -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 05 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,RST FIN,RST -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 06 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags FIN,ACK FIN -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 07 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,URG URG -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 08 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,FIN FIN -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 09 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ACK,PSH PSH -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 10 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL ALL -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 11 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL NONE -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 12 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL FIN,PSH,URG -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 13 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-bad_tcp 14 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-bad_tcp -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -m comment --comment "$comment" -j DROP' # chain-ipspoof comment="HSHQ_BEGIN chain-ipspoof tcp multiport HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof -p tcp -m tcp -m multiport --ports 0 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof udp multiport HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof -p udp -m udp -m multiport --ports 0 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 192.0.0.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -s 192.0.0.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 192.0.2.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -s 192.0.2.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 198.51.100.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -s 198.51.100.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 203.0.113.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -s 203.0.113.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -d 192.0.0.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -d 192.0.0.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -d 192.0.2.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -d 192.0.2.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -d 198.51.100.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -d 198.51.100.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -d 203.0.113.0/24 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -d 203.0.113.0/24 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -d 0.0.0.0/8 HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -d 0.0.0.0/8 -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 127.0.0.0/8 ! -i lo HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -s 127.0.0.0/8 ! -i lo -m comment --comment "$comment" -j DROP' # INPUT # Insert these at the top # Drop fragments comment="HSHQ_BEGIN INPUT fragments HSHQ_END" checkAddRule "$comment" 'sudo iptables -I INPUT -f -m comment --comment "$comment" -j DROP' # Allow established connections comment="HSHQ_BEGIN INPUT --ctstate ESTABLISHED,RELATED HSHQ_END" checkAddRule "$comment" 'sudo iptables -I INPUT -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "$comment" -j ACCEPT' # Drop SYN packets with suspicious MSS value comment="HSHQ_BEGIN INPUT suspicious MSS HSHQ_END" checkAddRule "$comment" 'sudo iptables -I INPUT -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -m comment --comment "$comment" -j DROP' # Drop invalid packets comment="HSHQ_BEGIN INPUT --ctstate INVALID HSHQ_END" checkAddRule "$comment" 'sudo iptables -I INPUT -m conntrack --ctstate INVALID -m comment --comment "$comment" -j DROP' # Limit connections per source IP comment="HSHQ_BEGIN INPUT --connlimit-above 200 HSHQ_END" checkAddRule "$comment" 'sudo iptables -I INPUT -p tcp --syn -m connlimit --connlimit-above 200 -m comment --comment "$comment" -j REJECT --reject-with tcp-reset' # Configure loopback comment="HSHQ_BEGIN INPUT allow loopback HSHQ_END" checkAddRule "$comment" 'sudo iptables -I INPUT -i lo -m comment --comment "$comment" -j ACCEPT' # Append any remaining to the bottom # Allow ICMP comment="HSHQ_BEGIN INPUT allow ICMP HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p icmp -m comment --comment "$comment" -j ACCEPT' # Allow SSH comment="HSHQ_BEGIN INPUT -p tcp -m tcp --dport $SSH_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp --dport $SSH_PORT -m comment --comment "$comment" -j ACCEPT' # Special case for HomeAssistant since it is using host networking comment="HSHQ_BEGIN INPUT -p tcp -m tcp -i $NET_EXTERNAL_BRIDGE_NAME -s $NET_EXTERNAL_SUBNET --dport $HOMEASSISTANT_LOCALHOST_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp -i $NET_EXTERNAL_BRIDGE_NAME -s $NET_EXTERNAL_SUBNET --dport $HOMEASSISTANT_LOCALHOST_PORT -m comment --comment "$comment" -j ACCEPT' # Special case for Script-server since it is using host networking comment="HSHQ_BEGIN INPUT -p tcp -m tcp -i $NET_EXTERNAL_BRIDGE_NAME -s $NET_EXTERNAL_SUBNET --dport $SCRIPTSERVER_LOCALHOST_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp -i $NET_EXTERNAL_BRIDGE_NAME -s $NET_EXTERNAL_SUBNET --dport $SCRIPTSERVER_LOCALHOST_PORT -m comment --comment "$comment" -j ACCEPT' # Special case for Docker metrics comment="HSHQ_BEGIN INPUT -p tcp -m tcp -i $NET_PRIVATEIP_BRIDGE_NAME -s $NET_PRIVATEIP_SUBNET --dport $DOCKER_METRICS_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp -i $NET_PRIVATEIP_BRIDGE_NAME -s $NET_PRIVATEIP_SUBNET --dport $DOCKER_METRICS_PORT -m comment --comment "$comment" -j ACCEPT' # Allow HTTPS for Docker networks comment="HSHQ_BEGIN INPUT -p tcp -m tcp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $CADDY_HTTPS_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $CADDY_HTTPS_PORT -m comment --comment "$comment" -j ACCEPT' # Allow NTP for Docker networks comment="HSHQ_BEGIN INPUT -s $DOCKER_NETWORK_RESERVED_RANGE -p udp --dport 123 HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -s $DOCKER_NETWORK_RESERVED_RANGE -p udp --dport 123 -m comment --comment "$comment" -j ACCEPT' # Allow Coturn Primary TCP for Docker networks comment="HSHQ_BEGIN INPUT -p tcp -m tcp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_PRIMARY_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_PRIMARY_PORT -m comment --comment "$comment" -j ACCEPT' # Allow Coturn Primary UDP for Docker networks comment="HSHQ_BEGIN INPUT -p udp -m udp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_PRIMARY_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p udp -m udp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_PRIMARY_PORT -m comment --comment "$comment" -j ACCEPT' # Allow Coturn Secondary TCP for Docker networks comment="HSHQ_BEGIN INPUT -p tcp -m tcp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_SECONDARY_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_SECONDARY_PORT -m comment --comment "$comment" -j ACCEPT' # Allow Coturn Secondary UDP for Docker networks comment="HSHQ_BEGIN INPUT -p udp -m udp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_SECONDARY_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p udp -m udp -s $DOCKER_NETWORK_RESERVED_RANGE --dport $COTURN_SECONDARY_PORT -m comment --comment "$comment" -j ACCEPT' if ! [ -z "$IS_INSTALLED" ] && [ "$IS_INSTALLED" = "false" ] && ! [ -z "$CURRENT_SSH_PORT" ] && ! [ "$CURRENT_SSH_PORT" = "$SSH_PORT" ]; then comment="HSHQ_BEGIN Temp allow SSH port $CURRENT_SSH_PORT HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -p tcp -m tcp --dport $CURRENT_SSH_PORT -m comment --comment "$comment" -j ACCEPT' fi # HomeServer Host dbIDArr=($(sqlite3 $HSHQ_DB "select ID from connections where NetworkType='home_network' order by NetworkType;")) for curDBID in "${dbIDArr[@]}" do curInterfaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$curDBID;") curIPAddress=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$curDBID;") curSubnet=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections where ID=$curDBID;") curIsExposeToNetwork=$(sqlite3 $HSHQ_DB "select IsExposeToNetwork from connections where ID=$curDBID;") curInputAllowPorts=$(sqlite3 $HSHQ_DB "select InputAllowPorts from connections where ID=$curDBID;") curDockerUserAllowPorts=$(sqlite3 $HSHQ_DB "select DockerUserAllowPorts from connections where ID=$curDBID;") curNetworkType=$(sqlite3 $HSHQ_DB "select NetworkType from connections where ID=$curDBID;") curConnectionType=$(sqlite3 $HSHQ_DB "select ConnectionType from connections where ID=$curDBID;") curIsIPPrivate="$(checkIsIPPrivate $curIPAddress)" if ! [ "$(checkValidIPAddress $curIPAddress)" = "true" ]; then continue fi if [ "$curIPAddress" = "$DEFAULT_UNFOUND_IP_ADDRESS" ] || [ "$curIPAddress" = "127.0.0.1" ]; then curIsIPPrivate=true else if [ "$curIsIPPrivate" = "true" ]; then if [ "$curIsExposeToNetwork" = "1" ]; then hn_ip_list=$curSubnet else hn_ip_list=${curIPAddress}/32 fi else hn_ip_list=${curIPAddress}/32,${HOMENET_ADDITIONAL_IPS} fi appendINPUTBySubnetAndPortsList "$hn_ip_list" "$curInputAllowPorts" addDOCKERUSERBySubnetAndPortsList "$hn_ip_list" "$curDockerUserAllowPorts" fi # Add some anti-spoofing measures comment="HSHQ_BEGIN chain-ipspoof -s $DOCKER_NETWORK_RESERVED_RANGE -i $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof 3 -s $DOCKER_NETWORK_RESERVED_RANGE -i $curInterfaceName -m comment --comment "$comment" -j DROP' if [ "$curIsIPPrivate" = "false" ]; then # Add special rules when HomeServer interface is on non-private network, i.e. cloud-server, etc. # Insert these at the 3rd position down comment="HSHQ_BEGIN chain-ipspoof -s 10.0.0.0/8 -i $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof 3 -s 10.0.0.0/8 -i $curInterfaceName -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 172.16.0.0/12 -i $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof 3 -s 172.16.0.0/12 -i $curInterfaceName -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 192.168.0.0/16 -i $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof 3 -s 192.168.0.0/16 -i $curInterfaceName -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 169.254.0.0/16 -i $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof 3 -s 169.254.0.0/16 -i $curInterfaceName -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof -s 224.0.0.0/4 -i $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-ipspoof 3 -s 224.0.0.0/4 -i $curInterfaceName -m comment --comment "$comment" -j DROP' # Insert this to top of chain-icmp comment="HSHQ_BEGIN chain-icmp echo-request primary $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -I chain-icmp -p icmp -m icmp --icmp-type echo-request -i $curInterfaceName -m comment --comment "$comment" -j DROP' fi done # WireGuard interfaces dbIDArr=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType in ('primary','other') order by NetworkType;")) for curDBID in "${dbIDArr[@]}" do curInterfaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$curDBID;") curIPAddress=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID=$curDBID;") curSubnet=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections where ID=$curDBID;") curIsExposeToNetwork=$(sqlite3 $HSHQ_DB "select IsExposeToNetwork from connections where ID=$curDBID;") curInputAllowPorts=$(sqlite3 $HSHQ_DB "select InputAllowPorts from connections where ID=$curDBID;") curDockerUserAllowPorts=$(sqlite3 $HSHQ_DB "select DockerUserAllowPorts from connections where ID=$curDBID;") curNetworkType=$(sqlite3 $HSHQ_DB "select NetworkType from connections where ID=$curDBID;") curConnectionType=$(sqlite3 $HSHQ_DB "select ConnectionType from connections where ID=$curDBID;") appendINPUTByInterfaceAndPortsList "$curInterfaceName" "$curInputAllowPorts" addDOCKERUSERByInterfaceAndPortsList "$curInterfaceName" "$curDockerUserAllowPorts" # Add some anti-spoofing measures comment="HSHQ_BEGIN chain-ipspoof ! -s 10.0.0.0/8 -i $curInterfaceName HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof ! -s 10.0.0.0/8 -i $curInterfaceName -m comment --comment "$comment" -j DROP' done # Custom rules dbIDArr=($(sqlite3 $HSHQ_DB "select ID from customfwsubnet;")) for curDBID in "${dbIDArr[@]}" do curSubnet=$(sqlite3 $HSHQ_DB "select Subnet from customfwsubnet where ID=$curDBID;") curInputAllowPorts=$(sqlite3 $HSHQ_DB "select InputAllowPorts from customfwsubnet where ID=$curDBID;") curDockerUserAllowPorts=$(sqlite3 $HSHQ_DB "select DockerUserAllowPorts from customfwsubnet where ID=$curDBID;") appendINPUTBySubnetAndPortsList "$curSubnet" "$curInputAllowPorts" addDOCKERUSERBySubnetAndPortsList "$curSubnet" "$curDockerUserAllowPorts" done # This following section is problematic for unnamed bridges, as it will # cause the user problems when stopping and starting stacks. We'll leave # the code in here for now, and perhaps loop back to this idea later. # Query docker networks and add some more anti-spoofing rules # if ! [ "$netstate" = "prenetwork" ]; then # timeout 10 docker ps > /dev/null 2>&1 # if [ $? -eq 0 ]; then # networksArr=($(docker network ls -q)) # for curNet in "${networksArr[@]}" # do # brName=$(docker network inspect $curNet | jq -r '.[] | .Options."com.docker.network.bridge.name"') # netName=$(docker network inspect $curNet | jq -r '.[] | .Name') # if [ "$netName" = "host" ] || [ "$netName" = "none" ]; then # continue # fi # if [ -z "$brName" ] || [ "$brName" = "null" ]; then # brName="br-${curNet}" # fi # ip a | grep $brName > /dev/null 2>&1 # if [ $? -ne 0 ]; then # logHSHQEvent error "checkUpdateAllIPTables ($netstate) - Could not find bridge interface associated with docker network: $curNet" # continue # fi # brSubnet=$(docker network inspect $curNet | jq -r '.[] | .IPAM.Config[0].Subnet') # addIPTablesSpoofRule "$brName" "$brSubnet" # done # fi # fi # Compare rules on this pass with all exisitng rules, # Then delete any that do not match OLDIFS=$IFS IFS=$(echo -en "\n\b") tableArr=(raw filter) for curTable in "${tableArr[@]}" do allIPTCommentsArr=($(sudo iptables -t $curTable -n -L | grep -o "HSHQ_BEGIN.*HSHQ_END")) for curComment in "${allIPTCommentsArr[@]}" do grep "$curComment" /tmp/newRules.txt > /dev/null 2>&1 if [ $? -ne 0 ]; then curChain=$(echo $curComment | cut -d" " -f2 | xargs) deleteIPTableEntryByChainAndComment "$curTable" "$curChain" "$curComment" fi done unset allIPTCommentsArr allIPTCommentsArr="" done IFS=$OLDIFS # Policy drop for input and forward sudo iptables -P INPUT DROP sudo iptables -P FORWARD DROP # Policy drop all ipv6 traffic sudo ip6tables -P INPUT DROP sudo ip6tables -P FORWARD DROP sudo ip6tables -P OUTPUT DROP sudo ip6tables -t raw -P PREROUTING DROP sudo ip6tables -t raw -P OUTPUT DROP # If one wants to run a script right after this function, # then define the following bash script accordingly. if sudo test -f $HSHQ_CUSTOM_POST_IPTABLES_SCRIPT; then source $HSHQ_CUSTOM_POST_IPTABLES_SCRIPT "$netstate" fi sudo rm -f /tmp/ipt.txt sudo rm -f /tmp/newRules.txt if [ "$isLogInfo" = "true" ]; then logHSHQEvent info "checkUpdateAllIPTables ($netstate) - END" fi } function checkAddRule() { comment="$1" ip_cmd="$2" echo "$comment" >> /tmp/newRules.txt sudo grep "$comment" /tmp/ipt.txt > /dev/null 2>&1 if [ $? -ne 0 ]; then eval " $ip_cmd" if [ $? -ne 0 ]; then logHSHQEvent error "checkAddRule ($netstate) - Error adding rule: $ip_cmd" fi echo "$comment" >> /tmp/ipt.txt logHSHQEvent info "checkAddRule ($netstate) - Added rule: $comment" fi } function performClearIPTables() { set +e isAllowExisting="$1" if [ -z "$isAllowExisting" ]; then isAllowExisting=false fi sudo iptables -P INPUT ACCEPT sudo ip6tables -P INPUT ACCEPT sudo ip6tables -P FORWARD ACCEPT sudo ip6tables -P OUTPUT ACCEPT if [ "$isAllowExisting" = "true" ]; then comment="HSHQ_BEGIN INPUT Temp Keep Established HSHQ_END" sudo iptables -I INPUT -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "$comment" -j ACCEPT > /dev/null 2>&1 while true; do sudo iptables -L INPUT -n --line-numbers | cut -d" " -f1 | grep "2" > /dev/null 2>&1 if [ $? -eq 0 ]; then sudo iptables -D INPUT 2 > /dev/null 2>&1 else break fi done else sudo iptables -t filter -F INPUT fi sudo iptables -t filter -F DOCKER-USER sudo iptables -t raw -F PREROUTING sudo iptables -t raw -F chain-icmp > /dev/null 2>&1 sudo iptables -t raw -F chain-bad_tcp > /dev/null 2>&1 sudo iptables -t raw -F chain-ipspoof > /dev/null 2>&1 sudo iptables -t raw -X chain-icmp > /dev/null 2>&1 sudo iptables -t raw -X chain-bad_tcp > /dev/null 2>&1 sudo iptables -t raw -X chain-ipspoof > /dev/null 2>&1 } function appendINPUTBySubnetAndPortsList() { subnetList="$1" portList="$2" if [ -z "$subnetList" ] || [ -z "$portList" ]; then return fi checkValidPortsList "$portList" if [ $? -ne 0 ]; then strMsg="There was an error with the ports list: $portList" logHSHQEvent error "appendINPUTBySubnetAndPortsList ($netstate) (appendINPUTBySubnetAndPortsList) - $strMsg" echo "ERROR: $strMsg" return fi portsArr=($(echo $portList | tr "," "\n")) subnetArr=($(echo $subnetList | tr "," "\n")) for cur_port in "${portsArr[@]}" do port_no=$(echo "$cur_port" | cut -d"/" -f1 | xargs) port_prot=$(echo "$cur_port" | cut -d"/" -f2 | xargs) for cur_subnet in "${subnetArr[@]}" do case $port_prot in tcp|udp) comment="HSHQ_BEGIN INPUT -s $cur_subnet -p $port_prot --dport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -s $cur_subnet -p $port_prot --dport $port_no -m comment --comment "$comment" -j ACCEPT' ;; both) comment="HSHQ_BEGIN INPUT -s $cur_subnet -p tcp --dport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -s $cur_subnet -p tcp --dport $port_no -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN INPUT -s $cur_subnet -p udp --dport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -s $cur_subnet -p udp --dport $port_no -m comment --comment "$comment" -j ACCEPT' ;; *) ;; esac done done } function addDOCKERUSERBySubnetAndPortsList() { subnetList="$1" portList="$2" if [ -z "$subnetList" ] || [ -z "$portList" ]; then return fi checkValidPortsList "$portList" if [ $? -ne 0 ]; then strMsg="There was an error with the ports list: $portList" logHSHQEvent error "addDOCKERUSERBySubnetAndPortsList ($netstate) (addDOCKERUSERBySubnetAndPortsList) - $strMsg" echo "ERROR: $strMsg" return fi portsArr=($(echo $portList | tr "," "\n")) subnetArr=($(echo $subnetList | tr "," "\n")) for cur_port in "${portsArr[@]}" do # Protocol doesn't matter here, but just in case the user added it port_no=$(echo "$cur_port" | cut -d"/" -f1 | xargs) port_prot=$(echo "$cur_port" | cut -d"/" -f2 | xargs) for cur_subnet in "${subnetArr[@]}" do comment="HSHQ_BEGIN DOCKER-USER -s $cur_subnet -m conntrack --ctorigdstport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -I DOCKER-USER -s $cur_subnet -m conntrack --ctorigdstport $port_no --ctdir ORIGINAL -m comment --comment "$comment" -j RETURN' comment="HSHQ_BEGIN DOCKER-USER -s $DOCKER_NETWORK_RESERVED_RANGE -m conntrack --ctorigdstport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -I DOCKER-USER -s $DOCKER_NETWORK_RESERVED_RANGE -m conntrack --ctorigdstport $port_no --ctdir ORIGINAL -m comment --comment "$comment" -j RETURN' comment="HSHQ_BEGIN DOCKER-USER -m conntrack --ctorigdstport $port_no DROP HSHQ_END" checkAddRule "$comment" 'sudo iptables -A DOCKER-USER -m conntrack --ctorigdstport $port_no --ctdir ORIGINAL -m comment --comment "$comment" -j DROP' done done } function appendINPUTByInterfaceAndPortsList() { interfaceName="$1" portList="$2" if [ -z "$interfaceName" ] || [ -z "$portList" ]; then return fi checkValidPortsList "$portList" if [ $? -ne 0 ]; then strMsg="There was an error with the ports list: $portList" logHSHQEvent error "appendINPUTBySubnetAndPortsList ($netstate) (appendINPUTBySubnetAndPortsList) - $strMsg" echo "ERROR: $strMsg" return fi portsArr=($(echo $portList | tr "," "\n")) for cur_port in "${portsArr[@]}" do port_no=$(echo "$cur_port" | cut -d"/" -f1 | xargs) port_prot=$(echo "$cur_port" | cut -d"/" -f2 | xargs) case $port_prot in tcp|udp) comment="HSHQ_BEGIN INPUT -i $interfaceName -p $port_prot --dport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -i $interfaceName -p $port_prot --dport $port_no -m comment --comment "$comment" -j ACCEPT' ;; both) comment="HSHQ_BEGIN INPUT -i $interfaceName -p tcp --dport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -i $interfaceNamet -p tcp --dport $port_no -m comment --comment "$comment" -j ACCEPT' comment="HSHQ_BEGIN INPUT -i $interfaceName -p udp --dport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -A INPUT -i $interfaceName -p udp --dport $port_no -m comment --comment "$comment" -j ACCEPT' ;; *) ;; esac done } function addDOCKERUSERByInterfaceAndPortsList() { interfaceName="$1" portList="$2" if [ -z "$interfaceName" ] || [ -z "$portList" ]; then return fi checkValidPortsList "$portList" if [ $? -ne 0 ]; then strMsg="There was an error with the ports list: $portList" logHSHQEvent error "addDOCKERUSERBySubnetAndPortsList ($netstate) (addDOCKERUSERBySubnetAndPortsList) - $strMsg" echo "ERROR: $strMsg" return fi portsArr=($(echo $portList | tr "," "\n")) for cur_port in "${portsArr[@]}" do # Protocol doesn't matter here, but just in case the user added it port_no=$(echo "$cur_port" | cut -d"/" -f1 | xargs) port_prot=$(echo "$cur_port" | cut -d"/" -f2 | xargs) comment="HSHQ_BEGIN DOCKER-USER -i $interfaceName -m conntrack --ctorigdstport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -I DOCKER-USER -i $interfaceName -m conntrack --ctorigdstport $port_no --ctdir ORIGINAL -m comment --comment "$comment" -j RETURN' comment="HSHQ_BEGIN DOCKER-USER -s $DOCKER_NETWORK_RESERVED_RANGE -m conntrack --ctorigdstport $port_no HSHQ_END" checkAddRule "$comment" 'sudo iptables -I DOCKER-USER -s $DOCKER_NETWORK_RESERVED_RANGE -m conntrack --ctorigdstport $port_no --ctdir ORIGINAL -m comment --comment "$comment" -j RETURN' comment="HSHQ_BEGIN DOCKER-USER -m conntrack --ctorigdstport $port_no DROP HSHQ_END" checkAddRule "$comment" 'sudo iptables -A DOCKER-USER -m conntrack --ctorigdstport $port_no --ctdir ORIGINAL -m comment --comment "$comment" -j DROP' done } function deleteIPTableEntryByChainAndComment() { ipt_table="$1" ipt_chain="$2" ipt_comment="$3" OLDIFS=$IFS IFS=$(echo -en "\n\b") ipt_list=($(sudo iptables -t $ipt_table -n -L $ipt_chain --line-numbers | grep "$ipt_comment")) unset rule_list rule_list="" for cur_ipt in "${ipt_list[@]}" do rule_list+=( $(echo $cur_ipt | cut -d" " -f1 | xargs) ) done sorted_list=($(echo "${rule_list[@]}" | tr ' ' '\n' | sort -r)) isRuleFound=false for cur_rule in "${sorted_list[@]}" do sudo iptables -t $ipt_table -D $ipt_chain $cur_rule isRuleFound=true logHSHQEvent info "deleteIPTableEntryByChainAndComment ($netstate) - Removed rule: $ipt_comment" done if [ "$isRuleFound" = "false" ]; then logHSHQEvent error "deleteIPTableEntryByChainAndComment ($netstate) - Could not find rule to remove: $ipt_comment" fi IFS=$OLDIFS } function addIPTablesSpoofRule() { return ipt_interface_name="$1" ipt_source_subnet="$2" comment="HSHQ_BEGIN chain-ipspoof -s $ipt_source_subnet ! -i $ipt_interface_name HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof -s $ipt_source_subnet ! -i $ipt_interface_name -m comment --comment "$comment" -j DROP' comment="HSHQ_BEGIN chain-ipspoof ! -s $ipt_source_subnet -i $ipt_interface_name HSHQ_END" checkAddRule "$comment" 'sudo iptables -t raw -A chain-ipspoof ! -s $ipt_source_subnet -i $ipt_interface_name -m comment --comment "$comment" -j DROP' } function restartStacksOnBoot() { # This script handles the chicken/egg paradox between Docker and WireGuard during boot. # Each Caddy instance is bound to a specific interface - in most cases, a WireGuard interface. # Each WireGuard VPN interface uses a domain name as opposed to an IP address for the endpoint. # The domain name requires a DNS lookup, which is handled by AdguardHome - in a Docker container. # Thus, given this setup, the WireGuard interfaces will not come up until the AdGuard container is up. # Unfortunately, this causes a race condition with the Caddy containers that are bound to # specific WireGuard interfaces - they will fail to start if the interface doesn't exist. # Attempts were made within the systemd service files to start one after the other, but it still results # in a circular reference problem. Even manually starting the VPN interfaces before docker.service with # wg rather than wg-quick fails due to wg setconf errors on an unknown endpoint. So this script, which runs # at boot, will wait until the interfaces are up, then start/restart the Caddy containers. # This script also restarts some other stacks that have issues after a reboot. set +e num_tries=1 total_tries=20 logHSHQEvent info "restartStacksOnBoot - BEGIN" PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" retVal=$? while [ $retVal -ne 0 ] && [ $num_tries -lt $total_tries ] do sleep 10 ((num_tries++)) PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" retVal=$? done logHSHQEvent info "restartStacksOnBoot - Got Portainer token..." if [ $retVal -eq 0 ]; then logHSHQEvent info "restartStacksOnBoot - Restarting HomeAssistant stack (if running)..." restartStackIfRunning homeassistant 15 > /dev/null if [ $retVal -ne 0 ]; then logHSHQEvent error "restartStacksOnBoot - Problem restarting HomeAssistant stack..." fi else logHSHQEvent error "restartStacksOnBoot - Error obtaining Portainer token..." fi logHSHQEvent info "restartStacksOnBoot - Pinging WireGuard connections..." ips_arr=($(sqlite3 $HSHQ_DB "select IPAddress from connections where ConnectionType='homeserver_vpn' and NetworkType in ('primary','other');")) ns_sleep=5 ping_timeout=5 max_tries=15 is_error=false for cur_ip in "${ips_arr[@]}" do is_up=false cur_tries=1 while [ $cur_tries -le $max_tries ] && [ "$is_up" = "false" ] do timeout $ping_timeout ping -c 1 $cur_ip > /dev/null 2>&1 if [ $? -eq 0 ]; then is_up=true else ((cur_tries++)) sleep $ns_sleep fi done done logHSHQEvent info "restartStacksOnBoot - Restarting caddy stacks..." caddy_arr=($(docker ps -a --filter name=caddy- --format "{{.Names}}")) for curcaddy in "${caddy_arr[@]}" do startStopStack $curcaddy stop docker container stop $curcaddy docker container rm $curcaddy sleep 1 startStopStack $curcaddy start done isCoturn=$(docker ps -a --filter name=coturn --format "{{.Names}}") if ! [ -z "$isCoturn" ]; then logHSHQEvent info "restartStacksOnBoot - Restarting coturn stack..." startStopStack coturn stop > /dev/null 2>&1 sleep 1 startStopStack coturn start > /dev/null 2>&1 fi logHSHQEvent info "restartStacksOnBoot - END" } function outputDBExportScripts() { rm -f $HSHQ_SCRIPTS_DIR/user/exportPostgres.sh cat < $HSHQ_SCRIPTS_DIR/user/exportPostgres.sh #!/bin/bash is_keep_hourly="\$1" set -e PGPASSWORD=\$POSTGRES_PASSWORD if [ -z "\$PGPASSWORD" ]; then echo "ERROR: Password not set" exit 1 fi pg_dump --username \$POSTGRES_USER \$POSTGRES_DB > /dbexport/dbexport.tmp outputfilesize=\$(du -sh /dbexport/dbexport.tmp | xargs | cut -d ' ' -f1) cur_filename=\${POSTGRES_DB}.sql if [ "\$is_keep_hourly" = "true" ]; then cur_filename=\${POSTGRES_DB}-\$(date '+%H').sql fi mv /dbexport/dbexport.tmp /dbexport/\$cur_filename chmod 0400 /dbexport/\$cur_filename echo "Success - Filesize is \$outputfilesize" EOFDB chmod 0555 $HSHQ_SCRIPTS_DIR/user/exportPostgres.sh rm -f $HSHQ_SCRIPTS_DIR/user/exportMySQL.sh cat < $HSHQ_SCRIPTS_DIR/user/exportMySQL.sh #!/bin/bash is_keep_hourly="\$1" set -e mysqldump --user \$MYSQL_USER --password=\$MYSQL_PASSWORD \$MYSQL_DATABASE > /dbexport/dbexport.tmp outputfilesize=\$(du -sh /dbexport/dbexport.tmp | xargs | cut -d ' ' -f1) cur_filename=\${MYSQL_DATABASE}.sql if [ "\$is_keep_hourly" = "true" ]; then cur_filename=\${MYSQL_DATABASE}-\$(date '+%H').sql fi mv /dbexport/dbexport.tmp /dbexport/\$cur_filename chmod 0400 /dbexport/\$cur_filename echo "Success - Filesize is \$outputfilesize" EOFDB chmod 0555 $HSHQ_SCRIPTS_DIR/user/exportMySQL.sh rm -f $HSHQ_SCRIPTS_DIR/user/exportMariaDB.sh cat < $HSHQ_SCRIPTS_DIR/user/exportMariaDB.sh #!/bin/bash is_keep_hourly="\$1" set -e mariadb-dump --user \$MARIADB_USER --password=\$MARIADB_PASSWORD \$MARIADB_DATABASE > /dbexport/dbexport.tmp outputfilesize=\$(du -sh /dbexport/dbexport.tmp | xargs | cut -d ' ' -f1) cur_filename=\${MARIADB_DATABASE}.sql if [ "\$is_keep_hourly" = "true" ]; then cur_filename=\${MARIADB_DATABASE}-\$(date '+%H').sql fi mv /dbexport/dbexport.tmp /dbexport/\$cur_filename chmod 0400 /dbexport/\$cur_filename echo "Success - Filesize is \$outputfilesize" EOFDB chmod 0555 $HSHQ_SCRIPTS_DIR/user/exportMariaDB.sh rm -f $HSHQ_SCRIPTS_DIR/user/exportSurrealDB.sh cat < $HSHQ_SCRIPTS_DIR/user/exportSurrealDB.sh #!/bin/bash is_keep_hourly="\$1" set -e /surreal export --user \$SURREAL_USER --pass \$SURREAL_PASSWORD --ns \$SURREAL_NAMESPACE --db \$SURREAL_DATABASE > /dbexport/dbexport.tmp outputfilesize=\$(du -sh /dbexport/dbexport.tmp | xargs | cut -d ' ' -f1) cur_filename=\${SURREAL_DATABASE}.surql if [ "\$is_keep_hourly" = "true" ]; then cur_filename=\${SURREAL_DATABASE}-\$(date '+%H').surql fi mv /dbexport/dbexport.tmp /dbexport/\$cur_filename chmod 0400 /dbexport/\$cur_filename echo "Success - Filesize is \$outputfilesize" EOFDB chmod 0555 $HSHQ_SCRIPTS_DIR/user/exportSurrealDB.sh } function outputMaintenanceScripts() { if [[ "$(isProgramInstalled networkd-dispatcher)" = "false" ]]; then echo "Installing networkd-dispatcher, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 performAptInstall networkd-dispatcher > /dev/null 2>&1 fi sudo systemctl enable networkd-dispatcher > /dev/null 2>&1 sudo systemctl start networkd-dispatcher > /dev/null 2>&1 sudo rm -f $HSHQ_SCRIPTS_DIR/userasroot/updateLECerts.sh sudo tee $HSHQ_SCRIPTS_DIR/userasroot/updateLECerts.sh >/dev/null </dev/null if [ \$? -ne 0 ]; then startD_curCert=\$(date --date="\$(openssl x509 -noout -startdate -in \$curCert | cut -d"=" -f2)" "+%Y%m%d%H%M%S") startD_leCert=\$(date --date="\$(openssl x509 -noout -startdate -in \$le_dir/\$bname | cut -d"=" -f2)" "+%Y%m%d%H%M%S") if [ \$startD_curCert -gt \$startD_leCert ]; then cp -f \${curCert%.*}.* \$le_dir/ is_new_certs=true fi fi done if [ "\$is_new_certs" = "true" ]; then docker ps --filter name=caddy- -aq | xargs docker restart > /dev/null 2>&1 fi fi } main "\$@" EOFCS sudo chmod 0500 $HSHQ_SCRIPTS_DIR/userasroot/updateLECerts.sh sudo chown root:root $HSHQ_SCRIPTS_DIR/userasroot/updateLECerts.sh sudo rm -f $HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh sudo tee $HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh >/dev/null < /dev/null } function getPortainerToken() { echo \$(http --check-status --ignore-stdin --verify=no https://127.0.0.1:\$PORTAINER_LOCAL_HTTPS_PORT/api/auth username=\$PORTAINER_ADMIN_USERNAME password=\$PORTAINER_ADMIN_PASSWORD | jq -r .jwt) } function getStackID() { stackID="NA" stackName=\$1 stackName="\${stackName//.}" if [ -z "\$PORTAINER_TOKEN" ]; then PORTAINER_TOKEN="\$(getPortainerToken)" fi qry=\$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:\$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":\$PORTAINER_ENDPOINT_ID} "Authorization: Bearer \$PORTAINER_TOKEN") for row in \$(echo "\${qry}" | jq -r '.[] | @base64'); do _jq() { echo \${row} | base64 --decode | jq -r \${1} } if [ \$(_jq '.Name') = \$stackName ]; then stackID=\$(_jq '.Id') break fi done if ! [ \$stackID = "NA" ]; then echo \$stackID fi } main "\$@" EOFMS sudo chmod 0500 $HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh sudo chown root:root $HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh sudo rm -f $HSHQ_SCRIPTS_DIR/userasroot/checkCaddyContainers.sh sudo tee $HSHQ_SCRIPTS_DIR/userasroot/checkCaddyContainers.sh >/dev/null <&1 | grep "\$err_search") if ! [ -z "\$find_err" ]; then echo "Error found with \$curcaddy stack. Clearing data and restarting..." echo "If this problem persists, then contact the VPN owner and ask them to reset their Caddy instance which serves the certificates." $HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh \$curcaddy fi done EOFMS sudo chmod 0500 $HSHQ_SCRIPTS_DIR/userasroot/checkCaddyContainers.sh sudo chown root:root $HSHQ_SCRIPTS_DIR/userasroot/checkCaddyContainers.sh sudo rm -f $HSHQ_SCRIPTS_DIR/root/clearRoutingTable.sh sudo tee $HSHQ_SCRIPTS_DIR/root/clearRoutingTable.sh >/dev/null < /dev/null 2>&1" whileCheckCommand "$CMD" "$CHECK" CMD="sudo ip rule add from $NET_PRIVATEIP_SUBNET table 42 priority 2000" CHECK="sudo ip rule show | grep -w \"from $NET_PRIVATEIP_SUBNET lookup 42\" > /dev/null 2>&1" whileCheckCommand "$CMD" "$CHECK" CMD="sudo ip route add blackhole default metric 3 table 42" CHECK="sudo ip route show table 42 2>/dev/null | grep -w \"blackhole\" > /dev/null 2>&1" whileCheckCommand "$CMD" "$CHECK" } function whileCheckCommand() { eval "$2" RETVAL=$? numIter=1 while [ $RETVAL -ne 0 ] && [ $numIter -lt 100 ]; do eval "$1" > /dev/null 2>&1 RETVAL=$? numIter=$(($numIter+1)) sleep 1 done } function outputPerformNetworkingChecks() { sudo rm -f $HSHQ_SCRIPTS_DIR/root/performNetworkingChecks.sh sudo tee $HSHQ_SCRIPTS_DIR/root/performNetworkingChecks.sh >/dev/null </dev/null <> \$HSHQ_LOG_FILE fi return fi if ! [ "\$IS_DEBUG" = "false" ]; then echo "\$(date '+%Y-%m-%d %H:%M:%S.%3N') [debug] performNetworkingChecks.sh - BEGIN, CurState: \$SYS_STATE, LastCheck: \$(date -d @\$LAST_NETWORK_CHECK +"%Y-%m-%d %T"), IsTooSoon: \$isTooSoon" >> \$HSHQ_LOG_FILE fi source \$HSHQ_LIB_SCRIPT lib tgLock="\$(tryGetLock networkchecks performNetworkingChecks)" if ! [ "\$tgLock" = "true" ]; then checkRes="\$(getLockOpenMsg networkchecks)" totLockAttempts=\$(getIncrementLockAttempts networkchecks) strErr="performNetworkingChecks.sh - Cannot obtain networkchecks lock(\$totLockAttempts): \$checkRes, exiting..." logHSHQEvent warning "\$strErr" if [ \$((\$totLockAttempts % 60)) -eq 30 ]; then source <(sudo cat \$HSHQ_PLAINTEXT_ROOT_CONFIG) source \$HSHQ_PLAINTEXT_USER_CONFIG sendEmail -s "HSHQ Networking Error" -b "There is an issue with your HomeServer networking that needs to be addressed. For more details, you can view the log by running the command 'cat /var/log/hshq.log'. The easiest way to (possibly) fix this problem would be to simply reboot your server. However, if you continue to recieve this message then there might be a real problem that needs to fixed. You can either ask on the forum (https://forum.homeserverhq.com) or you can submit a Github issue (https://github.com/homeserverhq/hshq/issues)." fi exit 7 fi if ! [ "\$IS_DEBUG" = "false" ]; then echo "\$(date '+%Y-%m-%d %H:%M:%S.%3N') [debug] performNetworkingChecks.sh - Got lock, args: \$@" >> \$HSHQ_LOG_FILE fi source <(sudo cat \$HSHQ_PLAINTEXT_ROOT_CONFIG) source \$HSHQ_PLAINTEXT_USER_CONFIG set +e checkAllHSHQNetworking "\$@" retVal=\$? releaseLock networkchecks "performNetworkingChecks" false return \$retVal } function isCheckTooSoon() { last_check="\$1" if [ -z "\$last_check" ]; then last_check=0 fi cur_dt=\$(date +%s) s_diff=\$((\$cur_dt - \$last_check)) if [ \$s_diff -lt \$MIN_NETWORKCHECK_INTERVAL ]; then echo "true" else echo "false" fi } main "\$@" EOFBS sudo chmod 500 $HSHQ_SCRIPTS_DIR/root/performNetworkingChecks.sh sudo rm -f /etc/networkd-dispatcher/routable.d/performNetworkingChecks.sh if sudo test -d "/etc/networkd-dispatcher/routable.d"; then sudo ln -s $HSHQ_SCRIPTS_DIR/root/performNetworkingChecks.sh /etc/networkd-dispatcher/routable.d/performNetworkingChecks.sh sudo chmod 755 /etc/networkd-dispatcher/routable.d/performNetworkingChecks.sh fi sudo rm -f /etc/NetworkManager/dispatcher.d/performNetworkingChecks.sh if sudo test -d "/etc/NetworkManager/dispatcher.d"; then sudo ln -s $HSHQ_SCRIPTS_DIR/root/performNetworkingChecks.sh /etc/NetworkManager/dispatcher.d/performNetworkingChecks.sh sudo chmod 755 /etc/NetworkManager/dispatcher.d/performNetworkingChecks.sh fi } function outputUpdateShareDirectories() { sudo rm -f $HSHQ_SCRIPTS_DIR/root/updateShareDirectories.sh sudo tee $HSHQ_SCRIPTS_DIR/root/updateShareDirectories.sh >/dev/null </dev/null </dev/null < /dev/null 2>&1 logHSHQEvent info "performPostDockerBootActions - Docker is up, continuing..." # Replaces 10-updateIPTablesAfterDocker.sh checkUpdateAllIPTables performPostDockerBootActions logHSHQEvent info "performPostDockerBootActions - updateSysctl" # Replaces 10-updateSysctl.sh sysctl --system > /dev/null 2>&1 logHSHQEvent info "performPostDockerBootActions - pingGateway" # Replaces 15-pingGateway.sh cur_gate=$(ip route | grep -e "^default" | head -n 1 | awk '{print $3}') timeout 30 ping -c 4 $cur_gate > /dev/null logHSHQEvent info "performPostDockerBootActions - restartWG" # Replaces 20-restartWG.sh for curConf in /etc/wireguard/*.conf do if ! test -f "$curConf"; then continue; fi curSVC=$(echo ${curConf%.*} | rev | cut -d"/" -f1 | rev) logHSHQEvent info "performPostDockerBootActions - Checking WG Connection: $curSVC" systemctl is-enabled wg-quick@${curSVC} > /dev/null 2>&1 if [ $? -eq 0 ]; then logHSHQEvent info "performPostDockerBootActions - Restarting WG Connection: $curSVC" timeout 30 sudo systemctl restart wg-quick@${curSVC} > /dev/null 2>&1 fi done logHSHQEvent info "performPostDockerBootActions - createWGDockerNetworks" # Replaces 50-wgDockInternetUpAll.sh createWGDockerNetworks logHSHQEvent info "performPostDockerBootActions - wgDockInternetUpAll" wgDockInternetUpAll logHSHQEvent info "performPostDockerBootActions - restartStacksOnBoot" # Replaces 90-restartSelectedStacks.sh restartStacksOnBoot logHSHQEvent info "performPostDockerBootActions - END" } function checkInitScriptState() { if ! [ -f "$SYSTEM_STATE_FILENAME" ]; then is_dbg=false if [ "$LOG_LEVEL" = "debug" ]; then is_dbg=true fi tee $SYSTEM_STATE_FILENAME >/dev/null </dev/null echo "*/$LECERTS_REFRESH_RATE * * * * bash $HSHQ_SCRIPTS_DIR/userasroot/updateLECerts.sh >/dev/null 2>&1" | sudo tee -a $HOME/rootcron >/dev/null echo "0 */6 * * * bash $HSHQ_SCRIPTS_DIR/userasroot/checkCaddyContainers.sh" | sudo tee -a $HOME/rootcron >/dev/null echo "* * * * * bash $HSHQ_SCRIPTS_DIR/root/performNetworkingChecks.sh cron >/dev/null 2>&1" | sudo tee -a $HOME/rootcron >/dev/null echo "* * * * * bash $HSHQ_SCRIPTS_DIR/root/updateShareDirectories.sh >/dev/null 2>&1" | sudo tee -a $HOME/rootcron >/dev/null sudo crontab $HOME/rootcron sudo rm -f $HOME/rootcron } function appendToRoonCron() { cr_string="$1" sudo crontab -l > $HOME/rootcron set +e grep "$cr_string" $HOME/rootcron > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "$cr_string" | sudo tee -a $HOME/rootcron >/dev/null sudo crontab $HOME/rootcron sudo rm -f $HOME/rootcron fi set -e } function deleteFromRootCron() { cr_string="$1" sudo crontab -l > $HOME/rootcron set +e grep "$cr_string" $HOME/rootcron > /dev/null 2>&1 if [ $? -eq 0 ]; then sudo sed -i "/$cr_string/d" $HOME/rootcron sudo crontab $HOME/rootcron fi sudo rm -f $HOME/rootcron set -e } function pauseCronService() { sudo systemctl stop cron > /dev/null 2>&1 } function resumeCronService() { sudo systemctl restart cron > /dev/null 2>&1 } function updateHomeServerLogoImages() { set +e hsid=$(getHeimdallUserIDFromType homeservers) hs_list=($(sqlite3 $HSHQ_STACKS_DIR/heimdall/config/www/app.sqlite "select url from items where pinned=1 and user_id=$hsid;")) isAnyNew=false for curHS in "${hs_list[@]}" do curDomain=$(getBaseDomain $(echo "$curHS" | cut -d "/" -f3)) curURL="https://images.${curDomain}/${curDomain}.png" echo -e "\nGetting image for ${curDomain}..." wget -q --timeout=10 --tries=1 -O /tmp/tmpimage.png $curURL > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ERROR: Could not obtain image for $curDomain" rm -f /tmp/tmpimage.png continue fi curImgSize=$(du -s /tmp/tmpimage.png | xargs | cut -d" " -f1) if [ $curImgSize -lt 1 ] || [ $curImgSize -gt 1024 ]; then echo "ERROR: There was a problem with the image size for $curDomain" rm -f /tmp/tmpimage.png continue fi isNewImage=false if ! [ -f $HSHQ_ASSETS_DIR/images/${curDomain}.png ]; then echo "No existing image found for ${curDomain}, adding..." isNewImage=true else diff $HSHQ_ASSETS_DIR/images/${curDomain}.png /tmp/tmpimage.png > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "New image detected for ${curDomain}, updating..." isNewImage=true else echo "Same image for ${curDomain}, skipping..." fi fi if [ "$isNewImage" = "true" ]; then isAnyNew=true mv /tmp/tmpimage.png $HSHQ_ASSETS_DIR/images/${curDomain}.png # Add some random string to the filename, to overcome any browser cache issues. rand_string=$(pwgen -c -n 8 1) cp $HSHQ_ASSETS_DIR/images/${curDomain}.png $HSHQ_STACKS_DIR/heimdall/config/www/icons/${curDomain}-${rand_string}.png sqlite3 $HSHQ_STACKS_DIR/heimdall/config/www/app.sqlite "update items set icon='icons/${curDomain}-${rand_string}.png' where url='$curHS';" else rm -f /tmp/tmpimage.png fi done echo "" if [ "$isAnyNew" = "true" ]; then echo "Restarting Heimdall..." docker container restart heimdall > /dev/null 2>&1 else echo "No new images were detected" fi echo "Update HomeServer logo images complete!" } function uploadHomeServerLogo() { img_file="$1" if ! [ "$(file -b --mime-type $img_file)" = "image/png" ]; then echo "ERROR: Image must be a png file" rm -f "$img_file" return fi curImgSize=$(du -s $img_file | xargs | cut -d" " -f1) if [ $curImgSize -lt 1 ]; then echo "ERROR: There was a problem with the image size" rm -f "$img_file" return fi if [ $curImgSize -gt 1024 ]; then echo "ERROR: Image is too large, must be less than 1MB (1024 KB)" rm -f "$img_file" return fi echo "Image is good, updating..." mv "$img_file" $HSHQ_ASSETS_DIR/images/${HOMESERVER_DOMAIN}.png # Add some random string to the filename, to overcome any browser cache issues. rand_string=$(pwgen -c -n 8 1) cp $HSHQ_ASSETS_DIR/images/${HOMESERVER_DOMAIN}.png $HSHQ_STACKS_DIR/heimdall/config/www/icons/${HOMESERVER_DOMAIN}-${rand_string}.png sqlite3 $HSHQ_STACKS_DIR/heimdall/config/www/app.sqlite "update items set icon='icons/${HOMESERVER_DOMAIN}-${rand_string}.png' where url='https://$SUB_HSHQHOME.$HOMESERVER_DOMAIN';" echo "Restarting Heimdall..." docker container restart heimdall > /dev/null 2>&1 # Update desktop icon (if exists) replaceHomePageDesktopIcon "$rand_string" echo "HomeServer logo updated succesfully!" } function replaceHomePageDesktopIcon() { rand_string="$1" if [ -f /usr/share/icons/HSHQ/Homepage.png ] && [ -f $HSHQ_ASSETS_DIR/images/${HOMESERVER_DOMAIN}.png ]; then sudo rm -f /usr/share/icons/HSHQ/${HOMESERVER_DOMAIN}-* sudo cp -f $HSHQ_ASSETS_DIR/images/${HOMESERVER_DOMAIN}.png /usr/share/icons/HSHQ/${HOMESERVER_DOMAIN}-${rand_string}.png if [ -f $HOME/Desktop/HomePage.desktop ]; then sudo sed -i "s|^Icon=.*|Icon=/usr/share/icons/HSHQ/${HOMESERVER_DOMAIN}-${rand_string}.png|" ~/Desktop/HomePage.desktop trustDesktopIcon "HomePage.desktop" fi if sudo test -f /usr/share/applications/HomePage.desktop; then sudo sed -i "s|^Icon=.*|Icon=/usr/share/icons/HSHQ/${HOMESERVER_DOMAIN}-${rand_string}.png|" /usr/share/applications/HomePage.desktop fi fi } function checkAllHSHQNetworking() { # Check/Update All Networking # This function assumes that the networking lock # has already been obtained. Here is a list of # actions performed in this function: # # 1. Check all host interfaces for IP address change # 2. Check if adguard is working # 3. Check all VPN connections for Endpoint IP change # 4. Cheak health of all VPN connections (ping relay) # 5. Check masq IP of WG Internet connections (curl internet) # 6. If no locking errors, update last timestamp # # Notes: # - Ensure at least NETWORK_UPDATE_INTERVAL minutes # have passed since last update (check last timestamp) # - This function is tied into: # - routable.d (if systemd-network) # - dispatcher.d (if NetworkManager) # - cron job every minute, but only actually excutes # after every NETWORK_UPDATE_INTERVAL(def 5) minutes # unless the previous attempt at obtaining a lock # resulted in an error # set +e funRetVal=0 # This is for NetworkManager-dispatcher function calls. # See https://manpages.ubuntu.com/manpages/lunar/man8/NetworkManager-dispatcher.8.html # We'll only focus on 'up' actions. if ! [ -z "$2" ] && ! [ "$2" = "up" ]; then return fi cahn_callerName=unknown if [ "$1" = "cron" ]; then cahn_callerName=cron elif [ "$2" = "up" ]; then cahn_callerName=NetworkManager elif [ -z "$@" ]; then cahn_callerName=networkd fi if [ "$cahn_callerName" = "cron" ] && [ "$(isNetworkCheckIntervalExpired)" = "false" ]; then return fi if [ "$(isNetworkCheckIntervalTooSoon)" = "true" ]; then return fi if ! [ "$cahn_callerName" = "cron" ]; then logHSHQEvent info "checkAllHSHQNetworking ($cahn_callerName) BEGIN" fi isLockError=false isAnyChanged="$(checkHostInterfacesIsChanged)" if [ "$isAnyChanged" = "true" ]; then tgLock="$(tryGetLock hshqopen checkAllHSHQNetworking)" if ! [ "$tgLock" = "true" ]; then checkRes="$(getLockOpenMsg hshqopen)" logHSHQEvent error "checkAllHSHQNetworking ($cahn_callerName) - Unable to obtain lock ($checkRes)" isLockError=true else setSystemState $SS_UPDATING logHSHQEvent info "checkAllHSHQNetworking ($cahn_callerName) - Host interface IP changed..." checkHostAllInterfaceIPChanges true false checkAllHSHQNetworking logHSHQEvent info "checkAllHSHQNetworking ($cahn_callerName) - Releasing lock..." setSystemState $SS_RUNNING releaseLock hshqopen "checkAllHSHQNetworking" false fi fi # Perform curl https://api.ipify.org. If an error, # assume Adguard is not working and restart stack. curIP=$(curl --silent --max-time 5 https://api.ipify.org) if [ $? -ne 0 ]; then logHSHQEvent info "checkAllHSHQNetworking ($cahn_callerName) - Restarting adguard..." startStopStack adguard stop sleep 1 startStopStack adguard start logHSHQEvent info "checkAllHSHQNetworking ($cahn_callerName) - Adguard restarted..." fi logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - checkUpdateDockerPrivateIP (before)" checkUpdateDockerPrivateIP logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - checkUpdateDockerPrivateIP (after)" logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - updateEndpointIPs (before)" updateEndpointIPs logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - updateEndpointIPs (after)" logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - checkNonLockedUnencrytpedConfig (before)" checkNonLockedUnencrytpedConfig "$cahn_callerName" funRetVal=$(($funRetVal + $?)) logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - checkNonLockedUnencrytpedConfig (after)" if [ "$isLockError" = "false" ]; then logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - updateLastNetworkCheck (before)" updateLastNetworkCheck logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - updateLastNetworkCheck (after)" fi logHSHQEvent debug "checkAllHSHQNetworking ($cahn_callerName) - Debug END" if ! [ "$cahn_callerName" = "cron" ]; then logHSHQEvent info "checkAllHSHQNetworking ($cahn_callerName) END" fi return $funRetVal } function checkNonLockedUnencrytpedConfig() { callerName="$1" if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then checkResCNLUC="$(checkIsLockEnabled hshqopen)" if [ -z "$checkResCNLUC" ]; then errMsg="checkNonLockedUnencrytpedConfig ($callerName) - Config file is unencrypted with no lock!" logHSHQEvent error "$errMsg" echo "ERROR: $errMsg" return 1 fi fi } function updateEndpointIPs() { set +e logHSHQEvent debug "updateEndpointIPs - Begin" ping_timeout=5 conn_list=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType in ('homeserver_vpn','homeserver_internet') and NetworkType in ('other','primary');")) for cur_id in "${conn_list[@]}" do logHSHQEvent debug "updateEndpointIPs - Checking ID: $cur_id" is_int=$(sqlite3 $HSHQ_DB "select IsInternet from connections where ID='$cur_id';") iname=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID='$cur_id';") cname=$(sqlite3 $HSHQ_DB "select Name from connections where ID=$cur_id;") if [ "$(checkIPByID $cur_id)" = "true" ]; then logHSHQEvent info "updateEndpointIPs - IP changed, restarting $cname..." if [ $is_int = 0 ]; then timeout 10 sudo systemctl restart wg-quick@$iname > /dev/null 2>&1 else sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${iname}.conf restart > /dev/null 2>&1 fi fi set +e if [ $is_int = 0 ]; then logHSHQEvent debug "updateEndpointIPs - Checking health: $cur_id" # Also need to check health of connection and reset if issues rs_ip=$(sqlite3 $HSHQ_DB "select RS_VPN_IP from hsvpn_connections where ID=$cur_id;") if ! [ -z "$rs_ip" ]; then logHSHQEvent debug "updateEndpointIPs - Pinging $rs_ip" timeout $ping_timeout ping -c 1 $rs_ip > /dev/null 2>&1 if [ $? -ne 0 ]; then logHSHQEvent error "updateEndpointIPs - Connection Error, restarting(HSVPN) $cname ($iname)..." timeout 30 sudo systemctl restart wg-quick@$iname > /dev/null 2>&1 if [ $? -eq 0 ]; then # If applicable, restart the associated caddy Stack checkCaddyInterface=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ConnectionType in ('homeserver_vpn') and NetworkType in ('other','primary') and ID=$cur_id;") if ! [ -z "$checkCaddyInterface" ]; then logHSHQEvent error "updateEndpointIPs - Connection Error, restarting caddy interface (caddy-${checkCaddyInterface})..." forceRestartStack "caddy-${checkCaddyInterface}" 3 fi fi logHSHQEvent error "updateEndpointIPs - Connection Error, restart complete(HSVPN) $cname ($iname)..." fi fi fi done set +e logHSHQEvent debug "updateEndpointIPs - Check/fix ClientDNS" # Check/fix ClientDNS cdns_id=($(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='clientdns' and NetworkType in ('primary','mynetwork');")) for cur_cdns in "${cdns_id[@]}" do cname=$(sqlite3 $HSHQ_DB "select Name from connections where ID=$cur_cdns;") logHSHQEvent debug "updateEndpointIPs - Checking clientDNS: $cname ($cdns_id)" # First, check if container is actually running docker ps | grep ${cname}-wireguard > /dev/null 2>&1 if [ $? -ne 0 ]; then continue fi if ! [ -z "$cur_cdns" ] && [ "$(checkIPByID $cur_cdns)" = "true" ]; then logHSHQEvent info "updateEndpointIPs - IP changed, restarting(ClientDNS) $cname..." restartStackIfRunning "${cname}" 1 fi done set +e logHSHQEvent debug "updateEndpointIPs - Check/fix tunnelled WireGuard internet connections" # Check/fix tunnelled WireGuard internet connections for conf in $HSHQ_WIREGUARD_DIR/internet/*.conf do if ! sudo test -f "$conf"; then continue; fi logHSHQEvent debug "updateEndpointIPs - Checking wgDockInternet: $conf" resOut=$(sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $conf status) retVal=$? logHSHQEvent debug "updateEndpointIPs - wgDockInternet got status: $conf" if [ $retVal -ne 0 ]; then logHSHQEvent error "updateEndpointIPs - Connection Error, restarting(HSInternet)[$retVal - $resOut] $conf..." sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $conf restart > /dev/null 2>&1 logHSHQEvent debug "updateEndpointIPs - Connection Error, restart complete(HSInternet)[$retVal - $resOut] $conf..." fi done logHSHQEvent debug "updateEndpointIPs - End" } function checkIPByID() { dbID=$1 ehost=$(sqlite3 $HSHQ_DB "select EndpointHostname from connections where ID='$dbID';") eip=$(sqlite3 $HSHQ_DB "select EndpointIP from connections where ID='$dbID';") if ! [ -z "$ehost" ]; then check_ip=$(getIPFromHostname $ehost) fi if ! [ -z "$check_ip" ] && ! [ "$check_ip" = "$eip" ]; then curdt=$(date '+%Y-%m-%d %H:%M:%S') sudo sqlite3 $HSHQ_DB "update connections set EndpointIP='$check_ip',LastUpdated='$curdt' where ID='$dbID';" echo "true" else echo "false" fi } function wgDockInternetUpAll() { logHSHQEvent info "wgDockInternetUpAll - BEGIN" checkUpdateDockerPrivateIP for conf in $HSHQ_WIREGUARD_DIR/internet/*.conf do if ! sudo test -f "$conf"; then continue; fi logHSHQEvent info "wgDockInternetUpAll - internet: $conf" sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $conf up done logHSHQEvent info "wgDockInternetUpAll - END" } function outputWireGuardScripts() { outputWGDockInternetScript sudo tee $HSHQ_WIREGUARD_DIR/scripts/wgDockInternetDownAll.sh >/dev/null < /dev/null 2>&1 fi } function outputWGDockInternetScript() { sudo rm -f $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh sudo tee $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh >/dev/null <&2; exit 1 ;; esac } function while_check() { eval "\$2" RETVAL=\$? numIter=1 while [ \$RETVAL -ne 0 ] && [ \$numIter -lt 100 ]; do eval "\$1" > /dev/null 2>&1 RETVAL=\$? numIter=\$((\$numIter+1)) sleep 1 done } function up() { if ! [ "\$(checkValidIPAddress \$EXT_DOMAIN)" = "true" ]; then edip=\$(dig \$EXT_DOMAIN +short | grep '^[0-9]') if [ \$? -ne 0 ]; then # Cannot resolve hostname, this happens in boot process during networkd-dispatcher startup echo "Cannot resolve hostname, returning..." return fi fi if [ -z "\$DOCKER_NETWORK_SUBNET" ]; then echo "Docker network subnet has not been set" return fi CMD="ip rule add from \$DOCKER_NETWORK_SUBNET table \$ROUTING_TABLE_ID priority 2000" CHECK="ip rule show | grep -w \"from \$DOCKER_NETWORK_SUBNET lookup \$ROUTING_TABLE_ID\" > /dev/null 2>&1" while_check "\$CMD" "\$CHECK" CMD="ip route add blackhole default metric 3 table \$ROUTING_TABLE_ID" CHECK="ip route show table \$ROUTING_TABLE_ID 2>/dev/null | grep -w \"blackhole\" > /dev/null 2>&1" while_check "\$CMD" "\$CHECK" if ! [[ "\$IS_ENABLED" = "yes" ]] && ! [[ "\$IS_ENABLED" = "true" ]]; then echo "Config for \$NETWORK_NAME not enabled" return fi ip link | grep " \${NETWORK_NAME}:" > /dev/null if [ \$? -ne 0 ]; then ip link add \$NETWORK_NAME type wireguard wg setconf \$NETWORK_NAME \$CONFIG_FILE wg set \$NETWORK_NAME fwmark \$ROUTING_TABLE_ID ip addr add \$CLIENT_ADDRESS dev \$NETWORK_NAME ip link set mtu \$MTU up dev \$NETWORK_NAME ip link set up dev \$NETWORK_NAME fi CMD="ip route add default via \$CLIENT_ADDR_NO_CIDR metric 2 table \$ROUTING_TABLE_ID" CHECK="ip route show table \$ROUTING_TABLE_ID 2>/dev/null | grep -w \"\$CLIENT_ADDR_NO_CIDR\" > /dev/null 2>&1" while_check "\$CMD" "\$CHECK" if [ "\$HOST_INTERNET_TRAFFIC_INTERFACE" = "\$NETWORK_NAME" ]; then status fi echo "Connection: \$NETWORK_NAME is up" } function down() { isCurl=\$(docker ps -a --filter name=\${NETWORK_NAME}-curl --format "{{.Names}}") if ! [ -z "\$isCurl" ]; then docker container stop \${NETWORK_NAME}-curl > /dev/null 2>&1 docker container rm \${NETWORK_NAME}-curl > /dev/null 2>&1 fi ip link del \$NETWORK_NAME echo "Connection: \$NETWORK_NAME is down" if ! [ -z \$DOCKER_NETWORK_SUBNET ]; then CMD="ip rule add from \$DOCKER_NETWORK_SUBNET table \$ROUTING_TABLE_ID priority 2000" CHECK="ip rule show | grep -w \"from \$DOCKER_NETWORK_SUBNET lookup \$ROUTING_TABLE_ID\" > /dev/null 2>&1" while_check "\$CMD" "\$CHECK" fi CMD="ip route add blackhole default metric 3 table \$ROUTING_TABLE_ID" CHECK="ip route show table \$ROUTING_TABLE_ID 2>/dev/null | grep -w \"blackhole\" > /dev/null 2>&1" while_check "\$CMD" "\$CHECK" } function restart() { down up } function status() { if ! [ "\$(checkValidIPAddress \$EXT_DOMAIN)" = "true" ]; then edip=\$(timeout 5 dig \$EXT_DOMAIN +short | grep '^[0-9]') if [ \$? -ne 0 ]; then # Cannot resolve hostname, this happens in boot process during networkd-dispatcher startup echo "Cannot resolve hostname, returning..." return 4 fi fi timeout 10 docker ps > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo "Docker is not yet up, returning..." return 5 fi # Checking the status will start a curl container. # So if this is called when the connection is set # to a down state, then the container will not be # cleaned up. CMD="ip route add blackhole default metric 3 table \$ROUTING_TABLE_ID" CHECK="ip route show table \$ROUTING_TABLE_ID 2>/dev/null | grep -w \"blackhole\" > /dev/null 2>&1" while_check "\$CMD" "\$CHECK" isCurl=\$(docker ps -a --filter name=\${NETWORK_NAME}-curl --format "{{.Names}}") if [ -z "\$isCurl" ]; then docker run -d --name=\${NETWORK_NAME}-curl --restart unless-stopped --net=\$DOCKER_NETWORK_NAME curlimages/curl:7.84.0 tail -f /dev/null > /dev/null 2>&1 sleep 5 fi VPNIP=\$(docker exec \${NETWORK_NAME}-curl curl -fsSL --max-time 5 https://api.ipify.org 2>/dev/null) IP=\$(curl --silent https://api.ipify.org) if [ "\$(checkValidIPAddress \$EXT_DOMAIN)" = "true" ]; then rsip=\$EXT_DOMAIN else rsip=\$(getIPFromHostname \$EXT_DOMAIN) fi if [ -z "\$rsip" ]; then echo "ERROR: Could not determine IP from \$EXT_DOMAIN" return 1 elif [[ "\$VPNIP" = "\$rsip" ]]; then echo "Connected to \$rsip: Blackhole active" if [ "\$HOST_INTERNET_TRAFFIC_INTERFACE" = "\$NETWORK_NAME" ]; then ip rule show | grep -w "not from all fwmark .* lookup \$ROUTING_TABLE_ID" > /dev/null 2>&1 if [ \$? -ne 0 ]; then sudo ip rule delete priority 8000 > /dev/null 2>&1 while [ \$? -eq 0 ] do sudo ip rule delete priority 8000 > /dev/null 2>&1 done sudo ip rule add not fwmark \$ROUTING_TABLE_ID table \$ROUTING_TABLE_ID priority 8000 fi fi return 0 elif [[ "\$VPNIP" = "\$IP" ]]; then echo "Not connected to Endpoint: Blackhole NOT active!" return 2 else echo "Unknown error, VPNIP=\$VPNIP, RSIP=\$rsip, LocalIP=\$IP" return 3 fi } function checkValidIPAddress() { ip=\$(echo \$1 | cut -d "/" -f1) stat=1 if [[ \$ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\$ ]]; then OLDIFS=\$IFS IFS='./' ip=(\$ip) IFS=\$OLDIFS [[ \${ip[0]} -le 255 && \${ip[1]} -le 255 && \${ip[2]} -le 255 && \${ip[3]} -le 255 ]] stat=\$? fi if [ \$stat -eq 0 ]; then echo "true" else echo "false" fi } function getIPFromHostname() { echo \$(dig \$1 +short | grep '^[.0-9]*\$') } function getConfigVar() { echo \$(grep ^\$1= \$CONFIG_FILE | sed 's/^[^=]*=//' | sed 's/ *\$//g') } main "\$@" EOFWG sudo chmod 755 $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh } function createWGDockerNetworks() { for conf in $HSHQ_WIREGUARD_DIR/internet/*.conf do if ! test -f "$conf"; then continue; fi docker_net="$(getConfigVarFromFile \#DOCKER_NETWORK_NAME $conf root)" docker_subnet="$(getConfigVarFromFile \#DOCKER_NETWORK_SUBNET $conf root)" MTU="$(getConfigVarFromFile \#MTU $conf root)" docker network inspect $docker_net > /dev/null 2>&1 if [ $? -ne 0 ]; then docker network create $docker_net --subnet $docker_subnet -o com.docker.network.driver.mtu=$MTU -o com.docker.network.bridge.name=$docker_net fi done } function enableAllWGVPNInterfaces() { sudo ls /etc/wireguard | while read fname do bname=$(basename $fname .conf) enableWGInterfaceQuick $bname done } function enableWGInterfaceQuick() { config_filename=$1 sudo systemctl enable wg-quick@${config_filename}.service sudo systemctl daemon-reload sudo systemctl start wg-quick@${config_filename} } function removeWGInterfaceQuick() { config_filename=$1 sudo systemctl stop wg-quick@${config_filename}.service sudo systemctl disable wg-quick@${config_filename}.service sudo systemctl daemon-reload sudo rm -f /etc/wireguard/${config_filename}.conf } function getHSHostIPVarName() { if_name="$1" echo "PORTAINER_HOMESERVER_HOST_${if_name^^}_IP" } function getHSHostSubnetVarName() { if_name="$1" echo "PORTAINER_HOMESERVER_HOST_${if_name^^}_SUBNET" } function checkIPConflict() { chk_ip="$1" chk_subnet="$2" chk_private="$3" if ! [ "$(checkValidIPAddress $chk_ip)" = "true" ]; then echo "Invalid IP" return fi if [[ "$chk_ip" =~ ^127\. ]]; then # This case should never occur, but just for completeness. echo "This IP address is in the 127.0.0.0/8 range. This range is specifically reserved for the loopback interface. You must either change your router's LAN subnet or daisy-chain another router in between, and ideally set it in the 192.168.0.0/16 range." return fi if [ "$chk_private" = "true" ]; then if [ "$(checkNetworkIntersect $chk_subnet 10.0.0.0/8)" = "true" ]; then netsize=$(echo $chk_subnet | cut -d"/" -f2) if [ $netsize -eq 8 ]; then echo "You are using the ENTIRE 10.0.0.0/8 network for your home network - a terribly poor design choice. You will not be able to host a VPN or network with anyone. Please fix your network settings are restart the installation." return fi fi if [ "$(checkNetworkIntersect $chk_subnet $DOCKER_NETWORK_RESERVED_RANGE)" = "true" ]; then echo "Your current home network subnet intersects within the $DOCKER_NETWORK_RESERVED_RANGE range. This range is specifically reserved for Docker networking within this infrastructure. You must either change your router's LAN subnet or daisy-chain another router in between, and ideally set it in the 192.168.0.0/16 range." return fi check_intersect="$(isNetworkIntersectOurNetworks $chk_subnet false false)" if ! [ -z "$check_intersect" ]; then echo "$check_intersect" return fi fi } function isInterfaceExists() { set +e find /sys/class/net -type l -printf '%f\n' | grep "$1" > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "true" else echo "false" fi } function addHSInterface() { iface_name="$1" iface_isPrimary="$2" interfaceListArr=($(echo $HOMESERVER_HOST_NETWORK_INTERFACES | tr "," "\n")) for curInterface in "${interfaceListArr[@]}" do if [ "$iface_name" = "$curInterface" ]; then strMsg="addHSInterface - Interface already added." echo "$strMsg" logHSHQEvent error "$strMsg" return 1 fi done interfaceListArr=($(sqlite3 $HSHQ_DB "select InterfaceName from connections where InterfaceName <> '';")) for curInterface in "${interfaceListArr[@]}" do if [ "$iface_name" = "$curInterface" ]; then strMsg="addHSInterface - Interface name already exists in database." echo "$strMsg" logHSHQEvent error "$strMsg" return 2 fi done isFound=false availInterfacesArr=($(echo "$(getAllNetworkInterfacesCSV)" | tr "," "\n")) for curInterface in "${availInterfacesArr[@]}" do if [ "$iface_name" = "$curInterface" ]; then isFound=true break fi done if [ "$isFound" = "false" ]; then strMsg="addHSInterface - The selected interface is not available." echo "$strMsg" logHSHQEvent error "$strMsg" return 3 fi if [ "$(isInterfaceExists $iface_name)" = "false" ]; then strMsg="addHSInterface - The network interface was not found." echo "$strMsg" logHSHQEvent error "$strMsg" return 3 fi ip_addr=$(getIPAddressOfInterface $iface_name) if [ -z "$ip_addr" ] || [[ "$ip_addr" =~ ^169\.254\. ]]; then if [ "$iface_isPrimary" = "true" ]; then strMsg="addHSInterface - The IP address for this primary interface could not be determined." echo "$strMsg" logHSHQEvent error "$strMsg" return 3 fi ip_addr=127.0.0.1 is_private=true interface_subnet=127.0.0.1/32 else is_private=$(checkIsIPPrivate "$ip_addr") interface_subnet=$(getSubnetOfInterface "$iface_name" "$ip_addr" "$is_private") if [ -z "$interface_subnet" ]; then interface_subnet="${ip_addr}/24" fi chk_conflict=$(checkIPConflict "$ip_addr" "$interface_subnet" "$is_private") if ! [ -z "$chk_conflict" ]; then strMsg="addHSInterface - $chk_conflict" echo "$strMsg" logHSHQEvent error "$strMsg" return 6 fi fi if [ -z "$HOMESERVER_HOST_NETWORK_INTERFACES" ]; then HOMESERVER_HOST_NETWORK_INTERFACES=$iface_name else HOMESERVER_HOST_NETWORK_INTERFACES="$HOMESERVER_HOST_NETWORK_INTERFACES",$iface_name fi updatePlaintextRootConfigVar HOMESERVER_HOST_NETWORK_INTERFACES $HOMESERVER_HOST_NETWORK_INTERFACES if [ "$iface_isPrimary" = "true" ]; then isPrimaryInsert="primary" HOMESERVER_HOST_PRIMARY_INTERFACE_NAME=$iface_name updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_NAME $HOMESERVER_HOST_PRIMARY_INTERFACE_NAME HOMESERVER_HOST_PRIMARY_INTERFACE_IP=$ip_addr updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_IP $HOMESERVER_HOST_PRIMARY_INTERFACE_IP else isPrimaryInsert="secondary" fi curdt=$(getCurrentDate) sudo sqlite3 $HSHQ_DB "insert into connections(Name,ConnectionType,NetworkType,IPAddress,InterfaceName,LastUpdated,Network_Subnet,IsExposeToNetwork,InputAllowPorts,DockerUserAllowPorts) values('HomeServerHost_${iface_name^^}','$isPrimaryInsert','home_network','$ip_addr','$iface_name','$curdt','$interface_subnet',true,'$INPUT_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT','$DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT');" updateDeviceIPMappingFile } function removeHSInterface() { iface_name="$1" HOMESERVER_HOST_NETWORK_INTERFACES=$(echo "$HOMESERVER_HOST_NETWORK_INTERFACES" | sed "s|${iface_name},||g") HOMESERVER_HOST_NETWORK_INTERFACES=$(echo "$HOMESERVER_HOST_NETWORK_INTERFACES" | sed "s|,${iface_name}||g") updatePlaintextRootConfigVar HOMESERVER_HOST_NETWORK_INTERFACES $HOMESERVER_HOST_NETWORK_INTERFACES checkDeleteStackAndDirectory caddy-home-$iface_name "Caddy" true true > /dev/null 2>&1 sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from connections where InterfaceName='$iface_name';" > /dev/null 2>&1 updateDeviceIPMappingFile } function setHSInterfaceAsPrimary() { # This function changes the primary interface iface_name="$1" interfaceListArr=($(echo $HOMESERVER_HOST_NETWORK_INTERFACES | tr "," "\n")) isFound=false for curInterface in "${interfaceListArr[@]}" do if [ "$iface_name" = "$curInterface" ]; then isFound=true break fi done if [ "$isFound" = "false" ]; then strMsg="setHSInterfaceAsPrimary - The selected network interface was not in the list." echo "$strMsg" logHSHQEvent error "$strMsg" return 1 fi iface_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where InterfaceName = '$iface_name';") if [ -z "$iface_ip" ]; then strMsg="setHSInterfaceAsPrimary - The selected network interface was not found in the database." echo "$strMsg" logHSHQEvent error "$strMsg" return 2 fi if ! [ "$(checkValidIPAddress $iface_ip)" = "true" ]; then strMsg="setHSInterfaceAsPrimary - The IP address for this interface is invalid." echo "$strMsg" logHSHQEvent error "$strMsg" return 3 fi sudo sqlite3 $HSHQ_DB "update connections set ConnectionType='primary' where InterfaceName = '$iface_name';" sudo sqlite3 $HSHQ_DB "update connections set ConnectionType='secondary' where InterfaceName = '$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME';" HOMESERVER_HOST_PRIMARY_INTERFACE_NAME="$iface_name" HOMESERVER_HOST_PRIMARY_INTERFACE_IP="$iface_ip" updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_NAME $HOMESERVER_HOST_PRIMARY_INTERFACE_NAME updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_IP $HOMESERVER_HOST_PRIMARY_INTERFACE_IP } function setHSInterfaceIsExpose() { # This function sets the expose setting for the selected interface iface_name="$1" iface_isExpose="$2" interfaceListArr=($(echo $HOMESERVER_HOST_NETWORK_INTERFACES | tr "," "\n")) isFound=false for curInterface in "${interfaceListArr[@]}" do if [ "$iface_name" = "$curInterface" ]; then isFound=true break fi done if [ "$isFound" = "false" ]; then strMsg="setHSInterfaceIsExpose - The selected network interface was not in the list." echo "$strMsg" logHSHQEvent error "$strMsg" return 1 fi sudo sqlite3 $HSHQ_DB "update connections set IsExposeToNetwork=$iface_isExpose where InterfaceName = '$iface_name';" } function checkInterfaceIPChanged() { iface_name="$1" db_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where InterfaceName = '$iface_name';") current_ip=$(getIPAddressOfInterface $iface_name) # || [ "$db_ip" = "$DEFAULT_UNFOUND_IP_ADDRESS" ] if [ "$(checkValidIPAddress $current_ip)" = "false" ] || [ -z "$db_ip" ] || [ -z "$current_ip" ] || [ "$db_ip" = "$current_ip" ] || [ "$current_ip" = "$DEFAULT_UNFOUND_IP_ADDRESS" ]; then echo "false" return fi is_private=$(checkIsIPPrivate "$current_ip") interface_subnet=$(getSubnetOfInterface "$iface_name" "$current_ip" "$is_private") if [ -z "$interface_subnet" ]; then echo "false" return fi chk_conflict=$(checkIPConflict "$current_ip" "$interface_subnet" "$is_private") if ! [ -z "$chk_conflict" ]; then echo "false" return fi echo "true" } function updateHSInterface() { iface_name="$1" db_ip=$(sqlite3 $HSHQ_DB "select IPAddress from connections where InterfaceName = '$iface_name';") current_ip=$(getIPAddressOfInterface $iface_name) if [ "$(checkValidIPAddress $current_ip)" = "false" ]; then if ! [ "$db_ip" = "$DEFAULT_UNFOUND_IP_ADDRESS" ]; then curdt=$(getCurrentDate) sudo sqlite3 $HSHQ_DB "update connections set IPAddress = '$DEFAULT_UNFOUND_IP_ADDRESS', Network_Subnet = '$DEFAULT_UNFOUND_IP_SUBNET', LastUpdated = '$curdt' where InterfaceName = '$iface_name';" > /dev/null 2>&1 return fi strMsg="updateHSInterface - Invalid IP address" echo "$strMsg" # Could be a lot of log spam, lets not log it #logHSHQEvent error "$strMsg" return 1 fi if [ "$db_ip" = "$current_ip" ]; then strMsg="The IP address for $iface_name is unchanged ($current_ip). No updated needed." echo "$strMsg" return 2 fi is_private=$(checkIsIPPrivate "$current_ip") interface_subnet=$(getSubnetOfInterface "$iface_name" "$current_ip" "$is_private") if [ -z "$interface_subnet" ]; then strMsg="updateHSInterface - There was an error obtaining the subnet of this interface ($iface_name)." echo "$strMsg" logHSHQEvent error "$strMsg" return 3 fi chk_conflict=$(checkIPConflict "$current_ip" "$interface_subnet" "$is_private") if ! [ -z "$chk_conflict" ]; then strMsg="updateHSInterface - $chk_conflict" echo "$strMsg" logHSHQEvent error "$strMsg" return 4 fi if [ "$current_ip" = "$DEFAULT_UNFOUND_IP_ADDRESS" ]; then strMsg="updateHSInterface - The IP address is invalid ($DEFAULT_UNFOUND_IP_ADDRESS)." echo "$strMsg" logHSHQEvent error "$strMsg" return 4 fi # Everything is good, the IP addresses are different, so proceed with update. curdt=$(getCurrentDate) sudo sqlite3 $HSHQ_DB "update connections set IPAddress = '$current_ip', Network_Subnet = '$interface_subnet', LastUpdated = '$curdt' where InterfaceName = '$iface_name';" > /dev/null 2>&1 if [ "$iface_name" = "$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME" ]; then HOMESERVER_HOST_PRIMARY_INTERFACE_IP="$current_ip" updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_IP $HOMESERVER_HOST_PRIMARY_INTERFACE_IP fi strMsg="The IP address for $iface_name was changed from $db_ip to $current_ip." echo "$strMsg" logHSHQEvent info "$strMsg" } function checkUpdateHostInterface() { set +e logHSHQEvent debug "checkUpdateHostInterface BEGIN" iface_operation="$1" iface_name="$2" is_close_hshq="$3" iface_isPrimary="$4" iface_isExpose="$5" #echo "iface_operation: $iface_operation, iface_name: $iface_name, iface_isPrimary: $iface_isPrimary, iface_isExpose: $iface_isExpose" primary_ip_prior=$HOMESERVER_HOST_PRIMARY_INTERFACE_IP case $iface_operation in add) if [ -z "$iface_name" ] || [ -z "$iface_isPrimary" ]; then strMsg="checkUpdateHostInterface - You must specify the interface name and if its the primary interface (true|false)." echo "$strMsg" logHSHQEvent error "$strMsg" return 1 fi addHSInterface $iface_name $iface_isPrimary if [ $? -ne 0 ]; then return 2 fi if [ "$iface_isPrimary" = "true" ]; then return 3 fi newIP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where InterfaceName='$iface_name';") ;; remove) if [ -z "$iface_name" ]; then strMsg="checkUpdateHostInterface - You must specify the interface name." echo "$strMsg" logHSHQEvent error "$strMsg" return 4 fi if [ "$iface_name" = "$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME" ]; then strMsg="checkUpdateHostInterface - You cannot remove the primary interface." echo "$strMsg" logHSHQEvent error "$strMsg" return 5 fi removeHSInterface "$iface_name" if [ $? -ne 0 ]; then return 6 fi ;; setisprimary) if [ -z "$iface_name" ] || [ -z "$iface_isPrimary" ]; then strMsg="checkUpdateHostInterface - You must specify the interface name and if its the primary interface (true|false)." echo "$strMsg" logHSHQEvent error "$strMsg" return 7 fi if ! [ "$iface_isPrimary" = "true" ]; then strMsg="checkUpdateHostInterface - This operation is only for changing the primary interface." echo "$strMsg" logHSHQEvent error "$strMsg" return 8 fi if [ "$HOMESERVER_HOST_PRIMARY_INTERFACE_NAME" = "$iface_name" ]; then strMsg="checkUpdateHostInterface - The selected interface is already the primary interface." echo "$strMsg" logHSHQEvent error "$strMsg" return 9 fi setHSInterfaceAsPrimary "$iface_name" if [ $? -ne 0 ]; then return 10 fi ;; setisexpose) if [ -z "$iface_name" ] || [ -z "$iface_isExpose" ]; then strMsg="checkUpdateHostInterface - You must specify the interface name and if its expose setting. (iface_name: $iface_name, iface_isExpose: $iface_isExpose)" echo "$strMsg" logHSHQEvent error "$strMsg" return 11 fi if ! [ "$iface_isExpose" = "true" ] && ! [ "$iface_isExpose" = "false" ]; then strMsg="checkUpdateHostInterface - The expose setting must be either true or false. (iface_name: $iface_name, iface_isExpose: $iface_isExpose)" echo "$strMsg" logHSHQEvent error "$strMsg" return 12 fi setHSInterfaceIsExpose "$iface_name" "$iface_isExpose" if [ $? -ne 0 ]; then return 13 fi ;; update) logHSHQEvent debug "checkUpdateHostInterface Update 1" if [ -z "$iface_name" ]; then strMsg="checkUpdateHostInterface - You must specify the interface name." echo "$strMsg" logHSHQEvent error "$strMsg" return 14 fi logHSHQEvent debug "checkUpdateHostInterface Update 2" updateHSInterface "$iface_name" if [ $? -ne 0 ]; then return 15 fi logHSHQEvent debug "checkUpdateHostInterface Update 3" updateIP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where InterfaceName='$iface_name';") ;; *) strMsg="checkUpdateHostInterface - Unknown operation." echo "$strMsg" logHSHQEvent error "$strMsg" return 16 ;; esac case $iface_operation in add|update) logHSHQEvent debug "checkUpdateHostInterface Update 3" if ! [ "$newIP" = "127.0.0.1" ]; then logHSHQEvent debug "checkUpdateHostInterface Update 4" # Regenerate Portainer and Script-server certs certIPList="127.0.0.1" ifListDBID=($(sqlite3 $HSHQ_DB "select ID from connections where NetworkType = 'home_network';")) for curID in "${ifListDBID[@]}" do curIF_IP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID = $curID;") if [ "$(checkValidIPAddress $curIF_IP)" = "true" ] && ! [ "$curIF_IP" = "$DEFAULT_UNFOUND_IP_ADDRESS" ] && ! [ "$curIF_IP" = "127.0.0.1" ]; then certIPList="$certIPList","$curIF_IP" fi done logHSHQEvent debug "checkUpdateHostInterface Update 5" generateCert portainer portainer "$certIPList" "$(date -u -d "86401 seconds ago" '+%Y-%m-%d %H:%M:%S') GMT" generateCert script-server "script-server,host.docker.internal,localhost" "$certIPList" "$(date -u -d "86401 seconds ago" '+%Y-%m-%d %H:%M:%S') GMT" fi ;; esac case $iface_operation in add|remove|setisprimary|update) logHSHQEvent debug "checkUpdateHostInterface Update 6" updateSvcsIPChanges logHSHQEvent debug "checkUpdateHostInterface Update 7" ;; esac case $iface_operation in setisprimary|update) if [ "$(checkValidIPAddress $primary_ip_prior)" = "true" ] && [ "$(checkValidIPAddress $HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" = "true" ] && ! [ "$primary_ip_prior" = "$HOMESERVER_HOST_PRIMARY_INTERFACE_IP" ]; then logHSHQEvent debug "checkUpdateHostInterface Update 8" sqlite3 $HSHQ_STACKS_DIR/heimdall/config/www/app.sqlite "update items set url='https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$PORTAINER_LOCAL_HTTPS_PORT' where url like '%$primary_ip_prior:$PORTAINER_LOCAL_HTTPS_PORT%';" > /dev/null 2>&1 sqlite3 $HSHQ_STACKS_DIR/heimdall/config/www/app.sqlite "update items set url='https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$SCRIPTSERVER_LOCALHOST_PORT' where url like '%$primary_ip_prior:$SCRIPTSERVER_LOCALHOST_PORT%';" > /dev/null 2>&1 docker container restart heimdall > /dev/null 2>&1 addDomainAndWildcardAdguardHS $HOMESERVER_DOMAIN $HOMESERVER_HOST_PRIMARY_INTERFACE_IP > /dev/null 2>&1 logHSHQEvent debug "checkUpdateHostInterface Update 9" fi ;; esac # Update IP tables checkUpdateAllIPTables checkUpdateHostInterface case $iface_operation in add) # Add Caddy instance if [ "$iface_isExpose" = "true" ]; then installHostInterfaceCaddy "$iface_name" "$newIP" if [ "$newIP" = "127.0.0.1" ]; then startStopStack caddy-home-$iface_name stop > /dev/null 2>&1 sudo sqlite3 $HSHQ_DB "update connections set IPAddress = '$DEFAULT_UNFOUND_IP_ADDRESS', Network_Subnet = '$DEFAULT_UNFOUND_IP_SUBNET' where ID=$curID;" > /dev/null 2>&1 updatePortainerEnv fi fi ;; update) logHSHQEvent debug "checkUpdateHostInterface Update 10" # Restart caddy-home stacks if [ "$(checkValidIPAddress $updateIP)" = "true" ] && ! [ "$updateIP" = "$DEFAULT_UNFOUND_IP_ADDRESS" ]; then logHSHQEvent debug "checkUpdateHostInterface Update 11" startStopStack caddy-home-$iface_name stop > /dev/null 2>&1 docker container stop caddy-home-$iface_name > /dev/null 2>&1 docker container rm caddy-home-$iface_name > /dev/null 2>&1 sleep 1 startStopStack caddy-home-$iface_name start > /dev/null 2>&1 logHSHQEvent debug "checkUpdateHostInterface Update 12" fi ;; *) ;; esac case $iface_operation in add|update) logHSHQEvent debug "checkUpdateHostInterface Update 13" if ! [ "$is_close_hshq" = "false" ]; then logHSHQEvent debug "checkUpdateHostInterface Update 14, is_close_hshq: $is_close_hshq" performExitFunctions false logHSHQEvent debug "checkUpdateHostInterface Update 15" set +e fi sleep 2 logHSHQEvent debug "checkUpdateHostInterface Update 16" sudo systemctl restart runScriptServer ;; esac logHSHQEvent debug "checkUpdateHostInterface END" } function addFirewallSubnet() { ruleName="$1" subnet="${2}/${3}" if [ $(checkValidStringUpperLowerNumbers "$ruleName" "-") = "false" ]; then strMsg="The rule name is invalid. It must consist of a-z (lowercase), 0-9, and/or -, no spaces." echo "ERROR: $strMsg" return 1 fi chkExist=$(sqlite3 $HSHQ_DB "select Name from customfwsubnet where Name='$ruleName' and Subnet='$subnet';") if ! [ -z "$chkExist" ]; then strMsg="This rule name and subnet combo already exists..." echo "ERROR: $strMsg" return 2 fi sudo sqlite3 $HSHQ_DB "insert into customfwsubnet(Name,Subnet) values('$ruleName','$subnet');" } function removeFirewallSubnet() { db_id=$1 sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from customfwsubnet where ID=$db_id;" checkUpdateAllIPTables removeFirewallSubnet } function updateSvcsIPChanges() { if [ "$IS_INSTALLED" = "false" ]; then return fi updatePortainerEnv set +e docker ps | grep jitsi-web > /dev/null 2>&1 if [ $? -eq 0 ]; then startStopStack jitsi stop > /dev/null 2>&1 startStopStack jitsi start > /dev/null 2>&1 fi updateCoturnAllowedIPs } function checkHostInterfacesIsChanged() { interfaceListArr=($(echo $HOMESERVER_HOST_NETWORK_INTERFACES | tr "," "\n")) for curInterface in "${interfaceListArr[@]}" do checkIsChanged=$(checkInterfaceIPChanged "$curInterface") if [ "$checkIsChanged" = "true" ]; then echo "true" return fi done echo "false" } function checkHostAllInterfaceIPChanges() { isBypassHSHQStatus=$1 isCloseHSHQ="$2" callerName="$3" logHSHQEvent debug "checkHostAllInterfaceIPChanges BEGIN" set +e timeout 5 docker ps > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "Docker is not yet up and running, returning..." return 1 fi if [ "$(checkHostInterfacesIsChanged)" = "false" ]; then # Nothing has changed, no need to continue return fi logHSHQEvent debug "checkHostAllInterfaceIPChanges Check 1" if [ -z "$$callerName" ]; then callerName=checkHostAllInterfaceIPChanges fi tgLock="true" if ! [ "$isBypassHSHQStatus" = "true" ]; then tgLock="$(tryGetLock hshqopen $callerName)" fi logHSHQEvent debug "checkHostAllInterfaceIPChanges Check 2" if ! [ "$tgLock" = "true" ]; then checkRes="$(getLockOpenMsg hshqopen)" echo "The HSHQ script is running in another instance ($checkRes), returning..." logHSHQEvent error "checkHostAllInterfaceIPChanges - The HSHQ script is already open or running in a different instance ($checkRes)" return fi logHSHQEvent debug "checkHostAllInterfaceIPChanges Check 3" interfaceListArr=($(echo $HOMESERVER_HOST_NETWORK_INTERFACES | tr "," "\n")) isUpdateIPT=false for curInterface in "${interfaceListArr[@]}" do checkUpdateHostInterface update "$curInterface" "$isCloseHSHQ" na na if [ $? -eq 0 ]; then isUpdateIPT=true fi done logHSHQEvent debug "checkHostAllInterfaceIPChanges Check 4" if ! [ "$isBypassHSHQStatus" = "true" ]; then releaseLock hshqopen "$callerName" false fi logHSHQEvent debug "checkHostAllInterfaceIPChanges Check 5" } function checkUpdateSingleHostInterface() { curInterface="$1" checkUpdateHostInterface update $curInterface true na na if [ $? -eq 0 ]; then checkUpdateAllIPTables checkUpdateSingleHostInterface fi } function logHSHQEvent() { msgType="$1" msgContent="$2" isLogit=false # We'll have to clean up this logic later for all message types, # i.e. debug, info, warn, error...,but for now this is good enough case "$LOG_LEVEL" in info) if ! [ "$msgType" = "debug" ]; then isLogit=true fi ;; debug) isLogit=true ;; esac if [ "$isLogit" = "true" ]; then echo "$(date '+%Y-%m-%d %H:%M:%S.%3N') [$msgType] $msgContent" >> $HSHQ_LOG_FILE fi } function updateExposedPortsLists() { selectedChain="$1" selectedList="$2" selectedPorts="$3" if ! [ -z "$selectedPorts" ]; then checkValidPortsList "$selectedPorts" if [ $? -ne 0 ]; then strMsg="There was an error with the ports list: $selectedPorts" logHSHQEvent error "updateExposedPortsLists (checkValidPortsList) - $strMsg" echo "ERROR: $strMsg" return 1 fi fi if [ "$selectedChain" = "INPUT" ] && ! [ -z "$selectedPorts" ]; then checkValidINPUTChainPortsList "$selectedPorts" if [ $? -ne 0 ]; then strMsg="There was an error with the INPUT chain ports list: $selectedPorts" logHSHQEvent error "updateExposedPortsLists INPUT chain (checkValidPortsList) - $strMsg" echo "ERROR: $strMsg" return 2 fi fi checkCustom=$(echo "$selectedList" | cut -d" " -f2 | xargs) if [ "${selectedList: -8}" = "_DEFAULT" ]; then echo "Default list updated - $selectedList" updatePlaintextUserConfigVar "$selectedList" "$selectedPorts" elif [ "$checkCustom" = "(Custom)" ]; then intID=$(echo "$selectedList" | cut -d" " -f1 | xargs) if [ "$selectedChain" = "INPUT" ]; then sudo sqlite3 $HSHQ_DB "update customfwsubnet set InputAllowPorts='$selectedPorts' where ID=$intID;" elif [ "$selectedChain" = "DOCKER-USER" ]; then sudo sqlite3 $HSHQ_DB "update customfwsubnet set DockerUserAllowPorts='$selectedPorts' where ID=$intID;" fi echo "Applying results to firewall..." checkUpdateAllIPTables updatePorts1 echo "Edits to firewall completed successfully!" else intName=$(echo "$selectedList" | cut -d" " -f1 | xargs) if [ "$selectedChain" = "INPUT" ]; then sudo sqlite3 $HSHQ_DB "update connections set InputAllowPorts='$selectedPorts' where InterfaceName='$intName';" elif [ "$selectedChain" = "DOCKER-USER" ]; then sudo sqlite3 $HSHQ_DB "update connections set DockerUserAllowPorts='$selectedPorts' where InterfaceName='$intName';" # Check fi echo "Applying results to firewall..." checkUpdateAllIPTables updatePorts2 echo "Edits to firewall completed successfully!" fi } function isProgramInstalled() { bin_name=$(echo $1 | cut -d"|" -f1) lib_name=$(echo $1 | cut -d"|" -f2) if [[ -z "$(sudo which ${bin_name})" ]]; then echo "false" else echo "true" fi } function installDocker() { set +e outputDockerSettings util="docker|docker" if [[ "$(isProgramInstalled $util)" = "true" ]]; then sudo systemctl restart rsyslog > /dev/null 2>&1 upgradeDocker return 0 fi performAptInstall apt-transport-https > /dev/null 2>&1 performAptInstall ca-certificates > /dev/null 2>&1 performAptInstall curl > /dev/null 2>&1 performAptInstall gnupg > /dev/null 2>&1 performAptInstall gnupg2 > /dev/null 2>&1 performAptInstall software-properties-common > /dev/null 2>&1 performAptInstall lsb-release > /dev/null 2>&1 case "$DISTRO_ID" in "ubuntu") if [[ "$DISTRO_VERSION" =~ ^22\.04. ]]; then installDockerUbuntu2204 elif [[ "$DISTRO_VERSION" =~ ^24\.04. ]] || [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then installDockerUbuntu2404 elif [[ "$DISTRO_VERSION" =~ ^26\.04. ]]; then installDockerUbuntu2604 else echo "Linux version not found when installing Docker, exiting..." exit 5 fi ;; "debian") if [[ "$DISTRO_VERSION" =~ ^12. ]]; then installDockerDebian12 else echo "Linux version not found when installing Docker, exiting..." exit 5 fi ;; "linuxmint") if [[ "$DISTRO_VERSION" =~ ^22. ]]; then installDockerUbuntu2404 elif [[ "$DISTRO_VERSION" =~ ^6. ]]; then installDockerDebian12 else echo "Linux version not found when installing Docker, exiting..." exit 5 fi ;; *) ;; esac # See https://www.portainer.io/blog/portainer-and-docker-26 sudo apt-mark hold docker-ce sudo apt-mark hold docker-ce-cli sudo systemctl restart rsyslog sudo systemctl restart docker } function upgradeDocker() { echo "Upgrading docker, this could take 5-10 minutes, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo apt-mark unhold docker-ce sudo apt-mark unhold docker-ce-cli case "$DISTRO_ID" in "ubuntu") if [[ "$DISTRO_VERSION" =~ ^22\.04. ]]; then upgradeDockerUbuntu2204 elif [[ "$DISTRO_VERSION" =~ ^24\.04. ]] || [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then upgradeDockerUbuntu2404 elif [[ "$DISTRO_VERSION" =~ ^26\.04. ]]; then upgradeDockerUbuntu2604 else echo "Linux version not found when upgrading Docker, exiting..." exit 5 fi ;; "debian") if [[ "$DISTRO_VERSION" =~ ^12. ]]; then upgradeDockerDebian12 else echo "Linux version not found when upgrading Docker, exiting..." exit 5 fi ;; "linuxmint") if [[ "$DISTRO_VERSION" =~ ^22. ]]; then upgradeDockerUbuntu2404 elif [[ "$DISTRO_VERSION" =~ ^6. ]]; then upgradeDockerDebian12 else echo "Linux version not found when upgrading Docker, exiting..." exit 5 fi ;; *) echo "Linux version not found when upgrading Docker, exiting..." exit 5 ;; esac sudo apt-mark hold docker-ce sudo apt-mark hold docker-ce-cli echo "Docker succesfully upgraded!" } function removeDocker() { sudo systemctl stop docker sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y --purge docker-engine docker docker.io docker-ce docker-compose sudo rm -rf /var/lib/docker /etc/docker sudo rm -rf /etc/apparmor.d/docker sudo rm -rf /var/run/docker.sock sudo rm -rf /usr/local/bin/docker-compose sudo rm -rf /etc/docker sudo rm -rf ~/.docker sudo rm -rf /usr/share/keyrings/docker-archive-keyring.gpg sudo rm -rf /etc/apt/sources.list.d/docker.list sudo groupdel docker } function outputDockerSettings() { sudo mkdir -p /etc/docker if [ -z "$DOCKER_NETWORK_RESERVED_RANGE" ]; then DOCKER_NETWORK_RESERVED_RANGE=172.16.0.0/14 fi if [ -z "$DOCKER_NETWORK_SIZE" ]; then DOCKER_NETWORK_SIZE=24 fi if [ -z "$DOCKER_METRICS_PORT" ]; then DOCKER_METRICS_PORT=8323 fi outputDockerDaemonJson sudo mkdir -p /etc/rsyslog.d sudo tee /etc/rsyslog.d/docker-logs.conf >/dev/null </dev/null < /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2204 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2204 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin } function upgradeDockerUbuntu2204() { docker -v | grep -q $DOCKER_VERSION_CHECK_UBUNTU_2204 if [ $? -eq 0 ]; then sudo systemctl restart docker > /dev/null 2>&1 else sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2204 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2204 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 fi } function installDockerUbuntu2404() { # Install Docker (https://docs.docker.com/engine/install/ubuntu/) sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2404 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2404 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin } function upgradeDockerUbuntu2404() { docker -v | grep -q $DOCKER_VERSION_CHECK_UBUNTU_2404 if [ $? -eq 0 ]; then sudo systemctl restart docker > /dev/null 2>&1 else sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2404 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2404 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 fi } function installDockerUbuntu2604() { # Install Docker (https://docs.docker.com/engine/install/ubuntu/) sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Use noble for now, fix it later when repos are available # Also ensure to update DOCKER_VERSION_UBUNTU_2604 echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2604 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2604 containerd.io docker-buildx-plugin docker-compose-plugin } function upgradeDockerUbuntu2604() { docker -v | grep -q $DOCKER_VERSION_CHECK_UBUNTU_2604 if [ $? -eq 0 ]; then sudo systemctl restart docker > /dev/null 2>&1 else sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2604 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2604 containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null 2>&1 fi } function installDockerDebian12() { # Install Docker (https://docs.docker.com/engine/install/debian/) sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 echo "Installing docker, please wait..." sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_DEBIAN_12 docker-ce-cli=$DOCKER_VERSION_DEBIAN_12 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin } function upgradeDockerDebian12() { docker -v | grep -q $DOCKER_VERSION_CHECK_DEBIAN_12 if [ $? -eq 0 ]; then sudo systemctl restart docker > /dev/null 2>&1 else sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_DEBIAN_12 docker-ce-cli=$DOCKER_VERSION_DEBIAN_12 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1 fi } function initCertificateAuthority() { if [ "$CERTS_IS_CA_INIT" = "true" ]; then return 0 fi if [[ "$(isProgramInstalled openssl)" = "false" ]]; then echo "Installing openssl, please wait..." performAptInstall openssl > /dev/null 2>&1 fi if [[ "$(isProgramInstalled faketime)" = "false" ]]; then echo "Installing faketime, please wait..." performAptInstall faketime > /dev/null 2>&1 fi if [[ "$(isProgramInstalled bc)" = "false" ]]; then echo "Installing bc, please wait..." performAptInstall bc > /dev/null 2>&1 fi if [[ "$(isProgramInstalled htpasswd)" = "false" ]]; then echo "Installing htpasswd, please wait..." performAptInstall apache2-utils > /dev/null 2>&1 fi if [ -z "$CERTS_ROOT_CA_NAME" ]; then CERTS_ROOT_CA_NAME=${HOMESERVER_DOMAIN}-ca updatePlaintextRootConfigVar CERTS_ROOT_CA_NAME $CERTS_ROOT_CA_NAME fi while [ -z "$CERTS_INTERNAL_COUNTRY" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then CERTS_INTERNAL_COUNTRY="US" else CERTS_INTERNAL_COUNTRY=$(promptUserInputMenu "US" "Enter Certificate Country" "Enter the country abbreviation you would like to appear on your certificates: ") if [ -z "$CERTS_INTERNAL_COUNTRY" ]; then showMessageBox "Empty Value" "The country cannot be empty." elif [ $(checkValidStringUpper "$CERTS_INTERNAL_COUNTRY") = "false" ]; then showMessageBox "Invalid Character(s)" "The country contains invalid character(s). It must consist of exactly 2 uppercase letters." CERTS_INTERNAL_COUNTRY="" elif [ $(getStringLength "$CERTS_INTERNAL_COUNTRY") -ne 2 ]; then showMessageBox "Invalid Length" "The country code must be exactly 2 characters." CERTS_INTERNAL_COUNTRY="" fi fi updatePlaintextRootConfigVar CERTS_INTERNAL_COUNTRY "$CERTS_INTERNAL_COUNTRY" done while [ -z "$CERTS_INTERNAL_STATE" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then CERTS_INTERNAL_STATE="Missouri" else CERTS_INTERNAL_STATE=$(promptUserInputMenu "Missouri" "Enter Certificate State" "Enter the state name you would like to appear on your certificates: ") if [ -z "$CERTS_INTERNAL_STATE" ]; then showMessageBox "Empty Value" "The state cannot be empty." elif [ $(checkValidStringUpperLowerNumbers "$CERTS_INTERNAL_STATE" "[:space:].,-") = "false" ]; then showMessageBox "Invalid Character(s)" "The state contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, hyphens, commas or periods." CERTS_INTERNAL_STATE="" fi fi updatePlaintextRootConfigVar CERTS_INTERNAL_STATE "$CERTS_INTERNAL_STATE" done while [ -z "$CERTS_INTERNAL_LOCALITY" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then CERTS_INTERNAL_LOCALITY="St. Louis" else CERTS_INTERNAL_LOCALITY=$(promptUserInputMenu "St. Louis" "Enter Certificate Locality" "Enter the locality you would like to appear on your certificates: ") if [ -z "$CERTS_INTERNAL_LOCALITY" ]; then showMessageBox "Empty Value" "The locality cannot be empty." elif [ $(checkValidStringUpperLowerNumbers "$CERTS_INTERNAL_LOCALITY" "[:space:].,-") = "false" ]; then showMessageBox "Invalid Character(s)" "The state contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, hyphens, commas or periods." CERTS_INTERNAL_LOCALITY="" fi fi updatePlaintextRootConfigVar CERTS_INTERNAL_LOCALITY "$CERTS_INTERNAL_LOCALITY" done while [ -z "$CERTS_INTERNAL_ROOT_CN" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then CERTS_INTERNAL_ROOT_CN="$HOMESERVER_NAME Root CA" else CERTS_INTERNAL_ROOT_CN=$(promptUserInputMenu "$HOMESERVER_NAME Root CA" "Enter Certificate Root CN" "Enter the root CN you would like to appear on your certificates: ") if [ -z "$CERTS_INTERNAL_ROOT_CN" ]; then showMessageBox "Empty Value" "The root CN cannot be empty." elif [ $(checkValidStringUpperLowerNumbers "$CERTS_INTERNAL_ROOT_CN" "[:space:].,-") = "false" ]; then showMessageBox "Invalid Character(s)" "The root CN contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, hyphens, commas or periods." CERTS_INTERNAL_ROOT_CN="" fi fi updatePlaintextRootConfigVar CERTS_INTERNAL_ROOT_CN "$CERTS_INTERNAL_ROOT_CN" done while [ -z "$CERTS_INTERNAL_INTERMEDIATE_CN" ] do if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then CERTS_INTERNAL_INTERMEDIATE_CN="$HOMESERVER_NAME ECC Intermediate" else CERTS_INTERNAL_INTERMEDIATE_CN=$(promptUserInputMenu "$HOMESERVER_NAME ECC Intermediate" "Enter Certificate Intermediate CN" "Enter the intermediate CN you would like to appear on your certificates: ") if [ -z "$CERTS_INTERNAL_INTERMEDIATE_CN" ]; then showMessageBox "Empty Value" "The intermediate CN cannot be empty." elif [ $(checkValidStringUpperLowerNumbers "$CERTS_INTERNAL_INTERMEDIATE_CN" "[:space:].,-") = "false" ]; then showMessageBox "Invalid Character(s)" "The intermediate CN contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, hyphens, commas or periods." CERTS_INTERNAL_INTERMEDIATE_CN="" fi fi updatePlaintextRootConfigVar CERTS_INTERNAL_INTERMEDIATE_CN "$CERTS_INTERNAL_INTERMEDIATE_CN" done if [ -z "$CERTS_INTERNAL_ORG_NAME" ]; then CERTS_INTERNAL_ORG_NAME="$HOMESERVER_NAME" updatePlaintextRootConfigVar CERTS_INTERNAL_ORG_NAME "$CERTS_INTERNAL_ORG_NAME" fi if [ -f $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt ]; then showYesNoMessageBox "Root CA Exists" "Root CA certificate($HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt) exists.\nDo you wish to bypass this step and continue installing?" mbres=$? if [ $mbres -eq 0 ]; then return else exit 1 fi fi # Certificate Authority Key openssl genrsa -out $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.key 4096 cat < $HOME/ca.conf [ req ] distinguished_name = req_distinguished_name prompt = no x509_extensions = v3_ca [ req_distinguished_name ] C = $CERTS_INTERNAL_COUNTRY ST = $CERTS_INTERNAL_STATE L = $CERTS_INTERNAL_LOCALITY O = $CERTS_INTERNAL_ORG_NAME OU = $CERTS_INTERNAL_OU_NAME CN = $CERTS_INTERNAL_ROOT_CN [ v3_ca ] basicConstraints = critical,CA:TRUE subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer keyUsage = critical, cRLSign, keyCertSign EOFCA totDays=$(echo "($(if (( $(echo "$(date --date="$CERTS_INTERNAL_CA_DAYS days" +%s.%N) < 3074607481" | bc -l) )); then echo 3074607481; else echo $(date --date="$CERTS_INTERNAL_CA_DAYS days" +%s.%N); fi)-1559365200)/86400" | bc) # Certificate Authority Certificate # Regarding faketime, see https://support.apple.com/en-us/HT210176 faketime '2019-06-01 16:38:01 GMT' openssl req -new -x509 -sha256 -nodes -days $totDays -key $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.key -out $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -config $HOME/ca.conf # Create a DER format for root cert openssl x509 -in $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -out $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der -outform DER rm -f $HOME/ca.conf chmod 0400 $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.key chmod 0444 $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt chmod 0444 $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.der sudo cp $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates CERTS_IS_CA_INIT=true updatePlaintextRootConfigVar CERTS_IS_CA_INIT $CERTS_IS_CA_INIT } function initDHParams() { echo "Generating DH parameters, $CERTS_INTERNAL_DHPARAMS_KEYLENGTH bit long safe prime..." openssl dhparam -out $HSHQ_SSL_DIR/dhparam.pem $CERTS_INTERNAL_DHPARAMS_KEYLENGTH > /dev/null 2>&1 chmod 0444 $HSHQ_SSL_DIR/dhparam.pem } function generateCert() { CERT_NAME=$1 DNS="$2" IPS="$3" FT_START_DATE="$4" FT_END_DATE="$5" COMMON_NAME=$(echo "$DNS" | sed 's|,|\n|g' | sed -n '1p') if ! [ -f $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.key ]; then echo "Certificate Authority key does not exist. Please generate a CA key and certificate first, exiting..." return 1 fi num_days=$CERTS_INTERNAL_CA_DAYS cur_date_seconds=$(date +%s) if ! [ -z "$FT_START_DATE" ]; then start_date_seconds=$(date -d "$FT_START_DATE" +%s) fi if ! [ -z "$FT_END_DATE" ]; then end_date_seconds=$(date -d "$FT_END_DATE" +%s) if [ $(($end_date_seconds - $cur_date_seconds)) -lt 86400 ]; then echo "ERROR: End date must be at least a day after current date." return 1 fi num_days=$(($(($end_date_seconds - $(date +%s))) / 86400)) fi if ! [ -z "$FT_START_DATE" ] && ! [ -z "$FT_END_DATE" ]; then if [ $(($end_date_seconds - $start_date_seconds)) -lt 86400 ]; then echo "ERROR: End date must be at least a day after start date." return 1 fi num_days=$(($(($end_date_seconds - $start_date_seconds)) / 86400)) fi rm -f $HSHQ_SSL_DIR/$CERT_NAME.key rm -f $HSHQ_SSL_DIR/$CERT_NAME.crt # Generate certificate key openssl genrsa -out $HSHQ_SSL_DIR/$CERT_NAME.key 4096 cat < $HOME/$CERT_NAME-csr.cnf [ req ] distinguished_name = req_distinguished_name prompt = no req_extensions = req_ext [ req_distinguished_name ] C = $CERTS_INTERNAL_COUNTRY ST = $CERTS_INTERNAL_STATE L = $CERTS_INTERNAL_LOCALITY O = $CERTS_INTERNAL_ORG_NAME OU = $CERTS_INTERNAL_OU_NAME CN = $COMMON_NAME [ req_ext ] subjectAltName = @alt_names [ alt_names ] EOFCN cat < $HOME/$CERT_NAME-ext.cnf basicConstraints = CA:FALSE nsComment = "OpenSSL Certificate Generated By $HOMESERVER_NAME" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth,clientAuth subjectAltName = @alt_names [ alt_names ] EOFCN dnslist=$(sed 's|,|\n|g' <<< $DNS) ipnum=1 for dn in $dnslist do echo "DNS."$ipnum" = "$dn >> $HOME/$CERT_NAME-csr.cnf echo "DNS."$ipnum" = "$dn >> $HOME/$CERT_NAME-ext.cnf ((ipnum++)) done iplist=$(sed 's|,|\n|g' <<< $IPS) ipnum=1 for ip in $iplist do echo "IP."$ipnum" = "$ip >> $HOME/$CERT_NAME-csr.cnf echo "IP."$ipnum" = "$ip >> $HOME/$CERT_NAME-ext.cnf ((ipnum++)) done # Generate certificate request openssl req -config $HOME/$CERT_NAME-csr.cnf -new -sha256 -key $HSHQ_SSL_DIR/$CERT_NAME.key -out $HOME/$CERT_NAME.csr # Sign certificate if [ -z "$FT_START_DATE" ]; then openssl x509 -req -sha256 -days $num_days \ -in $HOME/$CERT_NAME.csr -CA $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -CAkey $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.key \ -out $HSHQ_SSL_DIR/$CERT_NAME.crt -extfile $HOME/$CERT_NAME-ext.cnf -CAcreateserial else faketime "$FT_START_DATE" openssl x509 -req -sha256 -days $num_days \ -in $HOME/$CERT_NAME.csr -CA $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt -CAkey $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.key \ -out $HSHQ_SSL_DIR/$CERT_NAME.crt -extfile $HOME/$CERT_NAME-ext.cnf -CAcreateserial fi rm -f $HOME/$CERT_NAME-csr.cnf rm -f $HOME/$CERT_NAME-ext.cnf rm -f $HOME/$CERT_NAME.csr chmod 0444 $HSHQ_SSL_DIR/$CERT_NAME.key chmod 0444 $HSHQ_SSL_DIR/$CERT_NAME.crt chown $USERID:$GROUPID $HSHQ_SSL_DIR/$CERT_NAME.key chown $USERID:$GROUPID $HSHQ_SSL_DIR/$CERT_NAME.crt } function generateCertDialog() { set +e cert_name="" while [ -z "$cert_name" ] do cert_name=$(promptUserInputMenu "" "Enter Certificate Filename" "Enter the filename for this certificate:") mbres=$? if [ $mbres -ne 0 ]; then return 1 fi if [ -z "$cert_name" ]; then showMessageBox "Filename Empty" "The Filename cannot be empty" fi done cert_dom_string="" while [ -z "$cert_dom_string" ] do cert_dom_string=$(promptUserInputMenu "" "Enter Certificate CN" "Enter the domain names for this certificate. Domain names should be grouped together as a comma separated list:") mbres=$? if [ $mbres -ne 0 ]; then return 1 fi if [ -z "$cert_dom_string" ]; then showMessageBox "Domain Name Empty" "The Domain Name cannot be empty" fi done cert_ip_string=$(promptUserInputMenu "" "Enter IP Addresses" "Enter the IP addresses for this certificate. IP addresses should be grouped together as a comma separated list:") ft_string="" while [ -z "$ft_string" ] do ft_string=$(promptUserInputMenu "$(date '+%Y-%m-%d %H:%M:%S %Z')" "Enter Start Date" "Enter the Start Date for the Certificate. It must be formatted as follows - 2023-01-01 00:00:00 GMT:") mbres=$? if [ $mbres -ne 0 ]; then return 1 fi if [ -z "$ft_string" ]; then showMessageBox "Date Empty" "The Date cannot be empty" fi done generateCert "$cert_name" "$cert_dom_string" "$cert_ip_string" "$ft_string" } function getUpdateAssets() { isGitExist=false if [ -d $HSHQ_ASSETS_DIR/.git ]; then isGitExist=true fi curGitTries=0 maxGitTries=10 while [ $curGitTries -lt $maxGitTries ] do if [ "$isGitExist" = "true" ]; then git -C $HSHQ_ASSETS_DIR pull > /dev/null else rm -fr $HSHQ_ASSETS_DIR git clone $HSHQ_ASSETS_GIT_REPO $HSHQ_ASSETS_DIR fi if [ $? -eq 0 ]; then curGitTries=0 break fi echo "($((curGitTries+1)) of $maxGitTries) There was an error pulling the contents from the HSHQ assets Github repository, trying again in 10 seconds..." sleep 10 ((curGitTries++)) done if [ $curGitTries -ge $maxGitTries ]; then return 1 fi } function isImageInRepo() { if [ -z "$1" ]; then echo "false" fi if ! [ -z $(docker image ls --format "table {{.Repository}}:{{.Tag}}" | grep "$1" | head -1) ]; then echo "true" else echo "false" fi } function pullImage() { image_to_pull=$1 secs_mult=60 set +e if [ -z "$image_to_pull" ]; then echo "ERROR: The image is empty." return 3 fi # Check if custom image, and if it exists if [ "$(checkIsCustomImage $image_to_pull)" = "true" ]; then if [ "$(isImageInRepo $image_to_pull)" = "true" ]; then echo "Custom image exists, returning..." return else echo "ERROR: This custom image ($image_to_pull) doesn't exist." return 3 fi fi if ! [ -z $(docker image ls --format "table {{.Repository}}:{{.Tag}}" | grep "$image_to_pull" | head -1) ]; then echo "Image ($image_to_pull) already in local registry..." return fi # Check if registry is indicated, if not, then add mirror.gcr.io to front if [ $(grep -o "/" <<< "$image_to_pull" | wc -l) -lt 2 ] && ! [ ${image_to_pull:0:13} = "mirror.gcr.io" ]; then img_and_version="mirror.gcr.io/$image_to_pull" else img_and_version="$image_to_pull" fi logHSHQEvent info "Pulling Image: $img_and_version" echo "Pulling Image: $img_and_version" is_success=1 num_tries=1 while [ $is_success -ne 0 ] && [ $num_tries -lt $MAX_DOCKER_PULL_TRIES ] do if [ $num_tries -lt $(($MAX_DOCKER_PULL_TRIES-1)) ]; then #timeout $(($num_tries*$secs_mult)) docker pull $img_and_version docker pull $img_and_version else docker pull $img_and_version fi if [ $? -eq 0 ]; then docker image inspect $img_and_version > /dev/null 2>&1 is_success=$? else is_success=1 fi ((num_tries++)) if [ $is_success -ne 0 ]; then echo "There was an error pulling the image, retrying in 15 seconds..." sleep 15 fi done if [ $is_success -ne 0 ]; then echo "Error pulling docker image: $img_and_version" return 5 fi } function extractDockerImages() { imgDir=$1 if ! [ -d "$imgDir" ]; then return fi for curImage in $imgDir/*.tar; do if [ -f "$curImage" ]; then docker load -i $curImage fi done } function createDockerNetworks() { docker network create -o com.docker.network.bridge.name=$NET_EXTERNAL_BRIDGE_NAME --driver=bridge --subnet $NET_EXTERNAL_SUBNET dock-ext > /dev/null docker network create -o com.docker.network.bridge.name=$NET_WEBPROXY_BRIDGE_NAME --driver=bridge --subnet $NET_WEBPROXY_SUBNET --internal dock-proxy > /dev/null docker network create -o com.docker.network.bridge.name=$NET_PRIVATEIP_BRIDGE_NAME --driver=bridge --subnet $NET_PRIVATEIP_SUBNET dock-privateip > /dev/null checkUpdateDockerPrivateIP docker network create -o com.docker.network.bridge.name=$NET_INTERNALMAIL_BRIDGE_NAME --driver=bridge --subnet $NET_INTERNALMAIL_SUBNET --internal dock-internalmail > /dev/null docker network create -o com.docker.network.bridge.name=$NET_DBS_BRIDGE_NAME --driver=bridge --subnet $NET_DBS_SUBNET --internal dock-dbs > /dev/null docker network create -o com.docker.network.bridge.name=$NET_LDAP_BRIDGE_NAME --driver=bridge --subnet $NET_LDAP_SUBNET --internal dock-ldap > /dev/null docker network create -o com.docker.network.bridge.name=$NET_MAILU_EXT_BRIDGE_NAME --driver=bridge --subnet $NET_MAILU_EXT_SUBNET dock-mailu-ext > /dev/null docker network create -o com.docker.network.bridge.name=$NET_MAILU_INT_BRIDGE_NAME --driver=bridge --subnet $NET_MAILU_INT_SUBNET --internal dock-mailu-int > /dev/null docker network create -o com.docker.network.bridge.name=$NET_AIPRIVATE_BRIDGE_NAME --driver=bridge --subnet $NET_AIPRIVATE_SUBNET --internal dock-aipriv > /dev/null docker network create -o com.docker.network.bridge.name=$NET_AILIMITED_BRIDGE_NAME --driver=bridge --subnet $NET_AILIMITED_SUBNET --internal dock-ailimit > /dev/null createWGDockerNetworks } function removeDockerNetworks() { docker network rm dock-ext > /dev/null 2>&1 docker network rm dock-proxy > /dev/null 2>&1 docker network rm dock-privateip > /dev/null 2>&1 docker network rm dock-internalmail > /dev/null 2>&1 docker network rm dock-dbs > /dev/null 2>&1 docker network rm dock-ldap > /dev/null 2>&1 docker network rm dock-mailu-ext > /dev/null 2>&1 docker network rm dock-mailu-int > /dev/null 2>&1 docker network rm dock-aipriv > /dev/null 2>&1 docker network rm dock-ailimit > /dev/null 2>&1 for conf in $HSHQ_WIREGUARD_DIR/internet/*.conf do if ! test -f "$conf"; then continue; fi net_name=$(sudo grep ^\#DOCKER_NETWORK_NAME= $conf | sed 's/^[^=]*=//') docker network rm $net_name > /dev/null 2>&1 done } function notifyStackInstallFailure() { sf_stack_name=$1 sendEmail -s "Stack Installation Failure ($sf_stack_name)" -b "$sf_stack_name did not install correctly. Please uninstall this stack and reinstall. If the error persists, then create a topic on the forum (https://forum.homeserverhq.com)." -f "$(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" } function checkStackHSHQManaged() { check_hshq_managed="$1" echo "$check_hshq_managed" | grep "^$STACK_VERSION_PREFIX" > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "true" else echo "false" fi } function getStackNameFromComposeLine() { echo "$1" | cut -d" " -f2 } function getVersionFromComposeLine() { echo "$1" | cut -d" " -f3 | cut -d"v" -f2 } function getScriptStackVersionNumber() { echo "$1" | cut -d"v" -f2 } function removeImageCSVList() { if [ "$LOG_LEVEL" = "debug" ]; then return fi img_csv_list=$1 set +e img_list=($(echo $img_csv_list | tr "," "\n")) for rm_img in "${img_list[@]}" do if ! [ -z "$rm_img" ]; then docker image rm $rm_img > /dev/null 2>&1 if [ $(grep -o "/" <<< "$rm_img" | wc -l) -lt 2 ] && ! [ ${rm_img:0:13} = "mirror.gcr.io" ]; then docker image rm "mirror.gcr.io/$rm_img" > /dev/null 2>&1 fi fi done } function prepPerformUpdate() { # This function is a drop-in replacement for all repetetive # code at the beginning of each of the performUpdate functions. # The underlying performUpdate function modifies the variable # perform_update_report with the results of the update process. # It is up to the caller to do something with it. perform_update_report="" if [ -z "$perform_stack_name" ]; then is_upgrade_error=true perform_update_report="ERROR($perform_stack_name): The stack name cannot be empty." return 1 fi perform_stack_id=$(getStackID $perform_stack_name) if [ -z "$perform_stack_id" ]; then is_upgrade_error=true perform_update_report="ERROR($perform_stack_name): There was a problem with the Portainer API. If this error persists, try restarting Portainer." return 2 fi perform_compose=$HSHQ_STACKS_DIR/portainer/compose/$perform_stack_id/docker-compose.yml perform_stack_firstline=$(sudo sed -n 1p $perform_compose) perform_stack_ver=$(getVersionFromComposeLine "$perform_stack_firstline") unset image_update_map image_update_map="" oldVer=v"$perform_stack_ver" } function getComposeImageList() { gclComposeFile="$1" gclRet="" set +e OLDIFS=$IFS IFS=$(echo -en "\n\b") gclArr=($(sudo grep "image:" $gclComposeFile)) for curImageLine in "${gclArr[@]}" do curImage=$(echo $curImageLine | xargs | cut -d " " -f2) echo "$gclRet" | grep -q "$curImage" if [ $? -ne 0 ]; then if [ -z "$gclRet" ]; then gclRet="$curImage" else gclRet="${gclRet},$curImage" fi fi done IFS=$OLDIFS echo $(sortCSVList "$gclRet") } function upgradeStack() { # This function modifies the variable stack_upgrade_report # with the results of the upgrade process. It is up to the # caller to do something with it. # This function also depends on the array, image_update_map, # to be set by the caller prior to entry, since passing # arrays as arguments in bash is a headache. comp_stack_name=$1 comp_stack_id=$2 old_ver=$3 new_ver=$4 cur_img_list="$5" upgrade_compose_file=$6 stackModFunction=$7 isReinstallStack=$8 stackReinstallModFunction=$9 rem_image_list="" is_upgrade_error=false stack_upgrade_report="" if [ "$(checkImageList $cur_img_list $upgrade_compose_file)" = "false" ]; then is_upgrade_error=true stack_upgrade_report="ERROR ($comp_stack_name): Container image mismatch, stack version: $old_ver\nExpected image list: $(sortCSVList $cur_img_list)\n Actual image list: $(getComposeImageList $upgrade_compose_file)" return fi # Even though the image list (cur_img_list) matches the corresponding # items in the update map (image_update_map), they are both used as # a deliberate means of double-checking the intended images. This next # loop block confirms that they match. curImageListArr=($(echo $cur_img_list | tr "," "\n")) curItemNum=0 for cur_repl in "${image_update_map[@]}" do cur_img=$(echo $cur_repl | cut -d"," -f1) if ! [ "$cur_img" = "${curImageListArr[$curItemNum]}" ]; then is_upgrade_error=true stack_upgrade_report="ERROR ($comp_stack_name): Image list does not match update map ($cur_img_list) - This is a developer error" return fi ((curItemNum++)) done for cur_repl in "${image_update_map[@]}" do new_img=$(echo $cur_repl | cut -d"," -f2) buildOrPullImage $new_img if [ $? -ne 0 ]; then is_upgrade_error=true break fi done set +e if [ "$is_upgrade_error" = "true" ]; then stack_upgrade_report="ERROR ($comp_stack_name): Error downloading image: $new_img, current stack version: $old_ver" return fi rm_image_list="" for cur_repl in "${image_update_map[@]}" do old_img=$(echo $cur_repl | cut -d"," -f1) new_img=$(echo $cur_repl | cut -d"," -f2) if ! [ "$old_img" = "$new_img" ]; then rm_image_list="${rm_image_list},$old_img" sudo sed -i "s|image: $old_img|image: $new_img|g" $upgrade_compose_file fi done sudo sed -i "1s|.*|$STACK_VERSION_PREFIX $comp_stack_name $new_ver|" $upgrade_compose_file set +e $stackModFunction if [ $? -ne 0 ]; then is_upgrade_error=true stack_upgrade_report="ERROR ($comp_stack_name): There was an issue with the modify function." return fi if [ "$isStartStackFromStopped" = "true" ] && [ "$isReinstallStack" = "false" ]; then unset isStartStackFromStopped isStartStackFromStopped="" startStopStack $comp_stack_name start if [ $? -ne 0 ]; then is_upgrade_error=true stack_upgrade_report="ERROR ($comp_stack_name): There was a problem restarting the stopped stack." return fi else if [ "$isReinstallStack" = "true" ]; then sudo cp $upgrade_compose_file $HOME/${comp_stack_name}-compose.yml sudo cp $HSHQ_STACKS_DIR/portainer/compose/$comp_stack_id/stack.env $HOME/${comp_stack_name}.env sudo chown $USERNAME:$USERNAME $HOME/${comp_stack_name}-compose.yml sudo chown $USERNAME:$USERNAME $HOME/${comp_stack_name}.env set +e $stackReinstallModFunction if [ $? -ne 0 ]; then is_upgrade_error=true stack_upgrade_report="ERROR ($comp_stack_name): There was an issue with the reinstall modify function." return fi updateStackByID ${comp_stack_name} ${comp_stack_id} $HOME/${comp_stack_name}-compose.yml $HOME/${comp_stack_name}.env if [ $? -ne 0 ]; then is_upgrade_error=true stack_upgrade_report="ERROR ($comp_stack_name): There was a problem updating the stack." return fi else restartStackIfRunning "$comp_stack_name" 5 if [ $? -ne 0 ]; then is_upgrade_error=true stack_upgrade_report="ERROR ($comp_stack_name): There was a problem restarting the stack." return fi fi fi set +e removeImageCSVList "$rm_image_list" stack_upgrade_report="${stack_upgrade_report}${comp_stack_name}: Upgraded from $old_ver to $new_ver" } function doNothing() { return } function replaceRedisBlock() { stackName="$1" containerName="$2" redisImg="$3" isIncludeVolume="$4" set +e lineStart=" $containerName:" lineEnd="$(grep REDIS_PASSWORD=.* $HOME/$stackName-compose.yml | head -1)" rpass="$(echo $lineEnd | cut -d"=" -f2)" replaceText="" replaceText=$replaceText">>$containerName:\n" replaceText=$replaceText">>>>image: $redisImg\n" replaceText=$replaceText">>>>container_name: $containerName\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>env_file: stack.env\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--requirepass $rpass\n" if ! [ "$isIncludeVolume" = "false" ]; then replaceText=$replaceText">>>>>>--appendonly yes\n" else replaceText=$replaceText">>>>>>--appendonly no\n" fi replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- int-$stackName-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n" if ! [ "$isIncludeVolume" = "false" ]; then replaceText=$replaceText">>>>>>- v-$stackName-redis:/data\n" fi replaceTextBlockInFile "$lineStart" "$lineEnd" "$replaceText" $HOME/$stackName-compose.yml false ">" cat -s $HOME/${stackName}-compose.yml > $HOME/${stackName}.tmp mv $HOME/${stackName}.tmp $HOME/${stackName}-compose.yml } function outputNextcloudInotifyScan() { cat < $HSHQ_SCRIPTS_DIR/user/monitorShared.sh #!/bin/bash set +e BASE_SHARED_PATH="$HSHQ_STACKS_DIR/shared" DEBOUNCE=5 STAMP="/tmp/nextcloud_last_change" LOCK="/tmp/nextcloud_scan.lock" MONITOR_PATH_1="\$BASE_SHARED_PATH/PersonalConsume" MONITOR_PATH_2="\$BASE_SHARED_PATH/PersonalProcessed" MONITOR_PATH_3="\$BASE_SHARED_PATH/SharedConsume" MONITOR_PATH_4="\$BASE_SHARED_PATH/SharedProcessed" MONITOR_PATH_5="\$BASE_SHARED_PATH/PersonalTranscribeInput" function main() { rm -f "\$STAMP" rm -fr "\$LOCK" 2>/dev/null date +%s > "\$STAMP" startProducer } function startProducer() { inotifywait -m -r -e create,delete,move --timefmt '%s' --format '%T' "\$MONITOR_PATH_1" "\$MONITOR_PATH_2" "\$MONITOR_PATH_3" "\$MONITOR_PATH_4" "\$MONITOR_PATH_5" | while read -r event_time do echo "\$event_time" > "\$STAMP" runFullScan & done } function runFullScan() { if ! mkdir "\$LOCK" 2>/dev/null; then return fi docker ps | grep -q nextcloud-app > /dev/null 2>&1 if [ \$? -ne 0 ]; then return fi while true; do last_at_start=\$(cat "\$STAMP") sleep 1 docker exec nextcloud-app php occ files:scan --all > /dev/null 2>&1 sleep "\$DEBOUNCE" last_at_end=\$(cat "\$STAMP") if [ "\$last_at_start" == "\$last_at_end" ]; then break fi done rmdir "\$LOCK" } trap "rm -f \$STAMP; rm -fr \$LOCK 2>/dev/null; kill 0; exit" INT TERM main EOFNS chmod 755 $HSHQ_SCRIPTS_DIR/user/monitorShared.sh sudo tee /etc/systemd/system/inotifyShared.service >/dev/null < /dev/null 2>&1 rtVal=$? if [ $rtVal -eq 0 ]; then echo "ERROR: Username ($addPUUID) already exists..." return fi if [ $rtVal -ne 32 ]; then echo "ERROR: Unknown error ($rtVal), returning..." return fi docker exec mailu-admin flask mailu config-export user | grep -q "$addPUEmailAddress" > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "ERROR: Email address ($addPUEmailAddress) already exists..." return fi pwHash=$(openssl passwd -6 $addPUPassword) echo "Adding primary user email..." addUserMailu user-import "$addPUUID" "$HOMESERVER_DOMAIN" "$pwHash" > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ERROR: There was a problem adding this email address ($addPUEmailAddress)..." return fi createStandardMailuMailboxes "$addPUEmailAddress" set +e echo "Adding primary user to LDAP..." lastUID=$(docker exec ldapserver bash -c "ldapsearch -x -D \"$LDAP_ADMIN_BIND_DN\" -w $LDAP_ADMIN_BIND_PASSWORD -H ldaps://localhost -b \"cn=lastUID,$LDAP_BASE_DN\" -LLL serialNumber | grep serialNumber | cut -d\" \" -f2 | xargs") ((lastUID++)) cat < $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/addUser.ldif dn: uid=$addPUUID,ou=people,${LDAP_BASE_DN} givenName:: $(echo -n "$addPUFirstName " | base64) sn:: $(echo -n "$addPULastName" | base64) uid: $addPUUID mail: $addPUEmailAddress objectClass: person objectClass: inetOrgPerson objectClass: posixAccount uidNumber: $lastUID gidNumber: 2001 loginShell: /sbin/nologin homeDirectory: /home/$addPUUID cn:: $(echo -n "$addPUFirstName $addPULastName" | base64) userPassword: {CRYPT}$pwHash EOFAU docker exec ldapserver bash -c "ldapadd -x -D \"$LDAP_ADMIN_BIND_DN\" -w $LDAP_ADMIN_BIND_PASSWORD -H ldaps://localhost -f /tmp/initconfig/addUser.ldif" > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/addUser.ldif cat < $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/incID.ldif dn: cn=lastUID,$LDAP_BASE_DN changetype: modify replace: serialNumber serialNumber: $lastUID EOFAU docker exec ldapserver bash -c "ldapmodify -x -D \"$LDAP_ADMIN_BIND_DN\" -w $LDAP_ADMIN_BIND_PASSWORD -H ldaps://localhost -f /tmp/initconfig/incID.ldif" > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/incID.ldif cat < $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/addG.ldif dn: cn=everybody,ou=groups,$LDAP_BASE_DN changetype: modify add: uniqueMember uniqueMember: uid=$addPUUID,ou=people,$LDAP_BASE_DN EOFAU docker exec ldapserver bash -c "ldapmodify -x -D \"$LDAP_ADMIN_BIND_DN\" -w $LDAP_ADMIN_BIND_PASSWORD -H ldaps://localhost -f /tmp/initconfig/addG.ldif" > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/addG.ldif cat < $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/addG.ldif dn: cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN changetype: modify add: uniqueMember uniqueMember: uid=$addPUUID,ou=people,$LDAP_BASE_DN EOFAU docker exec ldapserver bash -c "ldapmodify -x -D \"$LDAP_ADMIN_BIND_DN\" -w $LDAP_ADMIN_BIND_PASSWORD -H ldaps://localhost -f /tmp/initconfig/addG.ldif" > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/addG.ldif sleep 5 fi newuser_mailu_app_password=$(curl -s -X POST https://$SUB_MAILU.$HOMESERVER_DOMAIN/api/v1/token -H "Authorization: Bearer $MAILU_API_TOKEN" -H "Content-Type: application/json" -d "{\"email\": \"$addPUEmailAddress\", \"comment\": \"MCP\"}" | jq -r .token) addUserEmailClassifierAI "$addPUEmailAddress" "$newuser_mailu_app_password" "Consume" "Processed" sudo sqlite3 $HSHQ_STACKS_DIR/authelia/config/db.sqlite3 "insert into user_preferences(username,second_factor_method) values('$addPUUID','totp');" auth_uuid=$(uuidgen) sudo sqlite3 $HSHQ_STACKS_DIR/authelia/config/db.sqlite3 "insert into user_opaque_identifier(service,sector_id,username,identifier) values('openid','','$addPUUID','$auth_uuid');" echo "Adding primary user shared directories..." addUserShareDirectories ${addPUUID} addPrimaryUserNextcloud "${addPUUID}" "$addPUEmailAddress" "$addPUPassword" "$addPUFirstName $addPULastName" "$newuser_mailu_app_password" addPrimaryUserPaperless "${addPUUID}" "$addPUEmailAddress" "$addPUPassword" "$addPUFirstName" "$addPULastName" "$newuser_mailu_app_password" docker ps | grep -q paperless-app > /dev/null 2>&1 if [ $? -eq 0 ]; then jsonbody="username=${addPUUID}&password=$addPUPassword" newuser_paperless_apitoken=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/token/" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -d "$jsonbody" | jq -r '.token') fi fullName="${addPUFirstName}${addPULastName}" cleanName="${fullName//[![:alnum:]]/}" newuser_immich_api_key=$(pwgen -c -n 41 1) addPrimaryUserImmich "${addPUUID}" "$addPUEmailAddress" "$addPUFirstName $addPULastName" "$newuser_immich_api_key" docker ps | grep -q linkwarden-app > /dev/null 2>&1 if [ $? -eq 0 ]; then newuser_linkwarden_api_key=$(docker exec linkwarden-app node /data/data/provision-user.mjs $addPUEmailAddress "${addPUFirstName} ${addPULastName}" "MCP" $addPUUID) fi newuser_mdcollab_api_key=$(addPrimaryUsermdCollab "$addPUUID" "${addPUFirstName} ${addPULastName}" $addPUEmailAddress) newuser_mealie_api_key=$(addPrimaryUserMealie "$addPUUID" "${addPUFirstName} ${addPULastName}" $addPUEmailAddress false) newuser_presenton_api_key=$(addPrimaryUserPresenton "$addPUUID" "$addPUPassword") newuser_twenty_api_key=$(addPrimaryUserTwenty "$addPUUID" "$addPUEmailAddress" "$addPUPassword" "$addPUFirstName" "$addPULastName" "$newuser_mailu_app_password") if ! [ "$addPUIsLDAPAdmin" = "true" ]; then newuser_ragflow_api_key=$(addPrimaryUserRAGFlow "$addPUEmailAddress" "$addPUFirstName $addPULastName" "$addPUPassword") fi addPrimaryUserAutoKB "$addPUUID" "$cleanName" "$addPUEmailAddress" "$newuser_mailu_app_password" 1 "$newuser_ragflow_api_key" docker ps | grep -q openwebui-app > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "Adding primary user MCPs to OpenWebUI..." OPENWEBUI_PU_UUID=$(uuidgen) OPENWEBUI_PU_API_KEY=sk-$(uuidgen | sed "s/-//g") cat < $HSHQ_STACKS_DIR/openwebui/dbexport/addPrimaryUserOWUI.sh #!/bin/bash OPENWEBUI_DATABASE_NAME=$OPENWEBUI_DATABASE_NAME OPENWEBUI_DATABASE_USER=$OPENWEBUI_DATABASE_USER PGPASSWORD=$OPENWEBUI_DATABASE_USER_PASSWORD curdt=$(date '+%s') curdtfmt=$(date '+%Y-%m-%d %H:%M:%S.%3N') RANDOM_PASSWORD_HASH="$(htpasswd -bnBC 10 "" $(pwgen -c -n 32 1) | tr -d ':\n' | sed 's/\$2y/\$2b/' | sed 's/\$/\\\$/g')" function main() { sqlcmd="insert into \"user\"(id,name,email,role,oauth,profile_image_url,created_at,updated_at,last_active_at) values ('$OPENWEBUI_PU_UUID', '$addPUFirstName $addPULastName', '$addPUEmailAddress', 'user', '{\"oidc\": {\"sub\": \"$auth_uuid\"}}','/user.png', \$curdt, \$curdt, \$curdt);" echo "\$sqlcmd" | psql -U \$OPENWEBUI_DATABASE_USER \$OPENWEBUI_DATABASE_NAME psql -U \$OPENWEBUI_DATABASE_USER \$OPENWEBUI_DATABASE_NAME -c "insert into auth(id,email,password,active) values('$OPENWEBUI_PU_UUID','$addPUEmailAddress','\$RANDOM_PASSWORD_HASH',true);" echo "insert into api_key(id,user_id,key,created_at,updated_at) values('key_$OPENWEBUI_PU_UUID','$OPENWEBUI_PU_UUID','$OPENWEBUI_PU_API_KEY','\$curdt','\$curdt');" | psql -U $OPENWEBUI_DATABASE_USER $OPENWEBUI_DATABASE_NAME echo "insert into group_member(id,group_id,user_id,created_at,updated_at) values('$(uuidgen)','$OPENWEBUI_PRIMARYUSERS_UUID','$OPENWEBUI_PU_UUID','\$curdt','\$curdt');" | psql -U $OPENWEBUI_DATABASE_USER $OPENWEBUI_DATABASE_NAME } main EOFIM chmod 755 $HSHQ_STACKS_DIR/openwebui/dbexport/addPrimaryUserOWUI.sh docker exec openwebui-db bash /dbexport/addPrimaryUserOWUI.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/openwebui/dbexport/addPrimaryUserOWUI.sh jsonbody=$(jq -n \ --arg imap_username "$addPUEmailAddress" \ --arg imap_password "$newuser_mailu_app_password" \ --arg sender_name "$addPUFirstName $addPULastName" \ '{imap_host: "mailu-front", imap_username: $imap_username, imap_password: $imap_password, smtp_host: "mailu-front", sender_name: $sender_name}') curl -s -X POST "https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/api/v1/tools/id/imap_email_tool/valves/user/update" -H "Authorization: Bearer $OPENWEBUI_PU_API_KEY" -H "Content-Type: application/json" -d "$jsonbody" > /dev/null 2>&1 jsonbody=$(jq -n \ --arg immich_api_key "$newuser_immich_api_key" \ --arg nextcloud_api_key "$(echo -n ${addPUUID}:${newuser_nextcloud_app_password} | base64 -w 0)" \ --arg paperless_api_key "$newuser_paperless_apitoken" \ --arg opennotebook_api_key "$OPENNOTEBOOK_ADMIN_PASSWORD" \ --arg linkwarden_api_key "$newuser_linkwarden_api_key" \ --arg hedgedoc_api_key "$newuser_mdcollab_api_key" \ --arg mealie_api_key "$newuser_mealie_api_key" \ --arg presenton_api_key "$newuser_presenton_api_key" \ --arg twenty_api_key "$newuser_twenty_api_key" \ --arg ragflow_api_key "$newuser_ragflow_api_key" \ '{immich_api_key: $immich_api_key, nextcloud_api_key: $nextcloud_api_key, paperless_api_key: $paperless_api_key, opennotebook_api_key: $opennotebook_api_key, linkwarden_api_key: $linkwarden_api_key, hedgedoc_api_key: $hedgedoc_api_key, mealie_api_key: $mealie_api_key, presenton_api_key: $presenton_api_key, twenty_api_key: $twenty_api_key, ragflow_api_key: $ragflow_api_key}') curl -s -X POST "https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/api/v1/tools/id/mcpkeyvault_tool/valves/user/update" -H "Authorization: Bearer $OPENWEBUI_PU_API_KEY" -H "Content-Type: application/json" -d "$jsonbody" > /dev/null 2>&1 docker exec openwebui-db bash -lc "PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" -v ON_ERROR_STOP=1 -c \"INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) VALUES (gen_random_uuid()::text, 'knowledge', '$AUTOKB_PKB_REMOTE_TARGET_ID', 'user', '$OPENWEBUI_PU_UUID', 'read', EXTRACT(EPOCH FROM NOW())::bigint);\"" fi docker ps | grep -q speakr-app > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart speakr-app > /dev/null 2>&1 fi if ! [ "$addPUIsLDAPAdmin" = "true" ]; then echo "Sending Vaultwarden template to ${addPUUID}@${HOMESERVER_DOMAIN}..." emailUserVaultwardenCredentials "$addPUUID" "$addPUEmailAddress" fi echo "Primary user succesfully added!" } function addUserEmailClassifierAI() { set +e addUserCAI_email="$1" addUserCAI_pw="$2" addUserCAI_consume="$3" addUserCAI_processed="$4" if ! sudo test -f $HSHQ_STACKS_DIR/emailclassifierai/data/accounts.db; then return fi isExists=$(sudo sqlite3 $HSHQ_STACKS_DIR/emailclassifierai/data/accounts.db "select uuid from accounts where user='$addUserCAI_email';") if ! [ -z "$isExists" ]; then echo "User has already been added to EmailClassifierAI, returning..." return fi sudo sqlite3 $HSHQ_STACKS_DIR/emailclassifierai/data/accounts.db "insert into accounts(server, user, password, consume_folder, processed_folder, is_active) values('mailu-front','$addUserCAI_email','$addUserCAI_pw','$addUserCAI_consume','$addUserCAI_processed',1);" docker ps | grep -q emailclassifierai-monitor > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart emailclassifierai-worker > /dev/null 2>&1 docker container restart emailclassifierai-monitor > /dev/null 2>&1 fi } function addPrimaryUserImmich() { set +e addUserImm_uid="$1" addUserImm_email="$2" addUserImm_proper="$3" addUserImm_api_key="$4" docker ps | grep -q immich-app > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi echo "Adding primary user to Immich..." is_user_exist=$(curl -s -X GET "https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/api/users" -H "x-api-key: $IMMICH_API_KEY" | jq -r --arg email "$addUserImm_email" '.[] | select(.email == $email) | .id') if ! [ -z "$is_user_exist" ]; then echo "User has already been added to Immich, returning..." return fi jsonbody="{ \"email\": \"$addUserImm_email\", \"name\": \"$addUserImm_proper\", \"password\": \"$(pwgen -c -n 32 1)\", \"isAdmin\": false, \"shouldChangePassword\": false }" add_user_resp=$(curl -s -X POST "https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/api/admin/users" -H "Content-Type: application/json" -H "x-api-key: $IMMICH_API_KEY" -d "$jsonbody") new_user_id=$(echo "$add_user_resp" | jq -r '.id') jsonbody="{ \"name\": \"${addUserImm_proper}'s Library\", \"ownerId\": \"$new_user_id\", \"type\": \"EXTERNAL\", \"importPaths\": [\"/shared/Images/$addUserImm_uid/Images\"] }" curl -s -X POST "https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/api/libraries" -H "Content-Type: application/json" -H "x-api-key: $IMMICH_API_KEY" -d "$jsonbody" > /dev/null 2>&1 addImmichAPIKeyDB "$addUserImm_email" "$addUserImm_api_key" } function addPrimaryUserNextcloud() { set +e addUserNext_uid="$1" addUserNext_email="$2" addUserNext_password="$3" addUserNext_proper="$4" addUserNext_emailpw="$5" docker ps | grep -q nextcloud-app > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi echo "Adding primary user to Nextcloud..." curl -s -u $addUserNext_uid:$addUserNext_password -kX PROPFIND https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/remote.php/dav/files/$addUserNext_uid | grep -q exception if [ $? -eq 0 ]; then echo "User does not exist in Nextcloud, returning..." return fi docker exec -u www-data nextcloud-app php occ mail:account:create "$addUserNext_uid" "$addUserNext_proper" "$addUserNext_email" "mailu-front" 993 ssl "$addUserNext_email" "$addUserNext_emailpw" "mailu-front" 465 ssl "$addUserNext_email" "$addUserNext_emailpw" > /dev/null 2>&1 newuser_nextcloud_app_password=$(docker exec -u www-data nextcloud-app sh -c "export NC_PASS=$addUserNext_password && php occ user:auth-tokens:add --password-from-env $addUserNext_uid | tail -n 1") } function addPrimaryUserPaperless() { set +e addUserPaper_uid="$1" addUserPaper_email="$2" addUserPaper_password="$3" addUserPaper_firstname="$4" addUserPaper_lastname="$5" addUserPaper_emailpw="$6" docker ps | grep -q paperless-app > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi echo "Adding primary user to Paperless..." primary_user_gid=$(docker exec paperless-app curl -s -X GET "http://localhost:8000/api/groups/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" | jq -r --arg LDAP_PRIMARY_USER_GROUP_NAME "$LDAP_PRIMARY_USER_GROUP_NAME" '.results[] | select(.name == $LDAP_PRIMARY_USER_GROUP_NAME) | .id') if [ -z "$primary_user_gid" ] || ! [[ $primary_user_gid =~ ^[+-]?[0-9]+$ ]]; then return fi jsonbody="{\"username\": \"$addUserPaper_uid\",\"email\": \"$addUserPaper_email\",\"password\": \"$addUserPaper_password\",\"first_name\": \"$addUserPaper_firstname\",\"last_name\": \"$addUserPaper_lastname\",\"is_staff\": false,\"is_active\": true,\"is_superuser\": false,\"groups\": [$primary_user_gid]}" add_user_id=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/users/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') if [ -z "$add_user_id" ] || ! [[ $add_user_id =~ ^[+-]?[0-9]+$ ]]; then return fi jsonbody="{ \"name\": \"${addUserPaper_uid} Email\", \"imap_server\": \"$SMTP_HOSTNAME\", \"imap_port\": 143, \"imap_security\": 3, \"username\": \"$addUserPaper_email\", \"password\": \"$addUserPaper_emailpw\", \"account_type\": 1, \"owner\": $add_user_id, \"user_can_change\": true }" mail_account_id=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/mail_accounts/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') if [ -n "$mail_account_id" ]; then jsonbody="{ \"name\": \"${addUserPaper_uid} Email Personal\", \"account\": $mail_account_id, \"enabled\": true, \"folder\": \"Processed.Personal\", \"maximum_age\": 0, \"action\": 5, \"action_parameter\": \"paperless\", \"assign_title_from\": 2, \"assign_correspondent_from\": 1, \"assign_tags\": [ $PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID ], \"assign_owner_from_rule\": true, \"order\": 2, \"attachment_type\": 1, \"consumption_scope\": 1, \"pdf_layout\": 0, \"owner\": $add_user_id, \"user_can_change\": true, \"stop_processing\": false }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/mail_rules/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"${addUserPaper_uid} Email Work\", \"account\": $mail_account_id, \"enabled\": true, \"folder\": \"Processed.Work\", \"maximum_age\": 0, \"action\": 5, \"action_parameter\": \"paperless\", \"assign_title_from\": 2, \"assign_correspondent_from\": 1, \"assign_tags\": [ $PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID ], \"assign_owner_from_rule\": true, \"order\": 2, \"attachment_type\": 1, \"consumption_scope\": 1, \"pdf_layout\": 0, \"owner\": $add_user_id, \"user_can_change\": true, \"stop_processing\": false }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/mail_rules/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 fi jsonbody="{ \"name\": \"${addUserPaper_uid}_personalconsume\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"sources\": [ 1, 2, 3, 4 ], \"type\": 1, \"filter_path\": \"*/PersonalConsume/${addUserPaper_uid}/PersonalConsume/*\", \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true }, { \"sources\": [], \"type\": 2, \"filter_path\": null, \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true, \"filter_has_tags\": [ $PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID ] }, { \"sources\": [], \"type\": 3, \"filter_path\": null, \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true, \"filter_has_tags\": [ $PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID ] } ], \"actions\": [ { \"type\": 1, \"assign_owner\": $add_user_id }, { \"type\": 1, \"assign_storage_path\": 1 } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"${addUserPaper_uid}_transcribeconsume\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"sources\": [ 1, 2, 3, 4 ], \"type\": 1, \"filter_path\": \"*/PersonalTranscribeOutput/${addUserPaper_uid}/*\", \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true } ], \"actions\": [ { \"type\": 1, \"assign_owner\": $add_user_id, \"assign_document_type\": $PAPERLESS_TRANSCRIPTION_DOCTYPE_ID }, { \"type\": 1, \"assign_storage_path\": 1 } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 } function addPrimaryUserAutoKB() { set +e user_name="$1" formal_name="$2" imap_username="$3" imap_password="$4" paperless_storage_id="$5" ragflow_api_key="$6" docker ps | grep -q autokb-web > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi paperless_user_id=$(getPaperlessIDFromUsername "$user_name") echo "Creating IMAP personal source subscription..." akbRes="$(docker exec autokb-web curl -sS -X POST \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ --data "{\"name\":\"${formal_name}-IMAP-Personal-Source\",\"cron\":\"*/15 * * * * \",\"config\":{\"host\":\"$SMTP_HOSTNAME\",\"port\":993,\"use_ssl\":true,\"user\":\"$imap_username\",\"password\":\"$imap_password\",\"folder\":\"Processed.Personal\",\"monitor_subfolders\":true,\"chunking_enabled\":false}}" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/subscriptions/imapFolderWatchPlugin")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "IMAP source failed: $akbBody" >&2; return 1; } IMAP_PERSONAL_SUB_ID="$(printf '%s' "$akbBody" | jq -r '.id')" echo "Creating IMAP shared source subscription..." akbRes="$(docker exec autokb-web curl -sS -X POST \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ --data "{\"name\":\"${formal_name}-IMAP-Shared-Source\",\"cron\":\"*/15 * * * * \",\"config\":{\"host\":\"$SMTP_HOSTNAME\",\"port\":993,\"use_ssl\":true,\"user\":\"$imap_username\",\"password\":\"$imap_password\",\"folder\":\"Processed.Work\",\"monitor_subfolders\":true,\"chunking_enabled\":false}}" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/subscriptions/imapFolderWatchPlugin")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "IMAP source failed: $akbBody" >&2; return 1; } IMAP_SHARED_SUB_ID="$(printf '%s' "$akbBody" | jq -r '.id')" echo "Creating Paperless source subscription..." akbRes="$(docker exec autokb-web curl -sS -X POST \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ --data "{\"name\":\"${formal_name}-Paperless-Source\",\"cron\":\"*/15 * * * * \",\"config\":{\"document_filter\":\"storage_path__id=$paperless_storage_id&owner__id=$paperless_user_id&tags__id__in=$PAPERLESS_KNOWLEDGEBASE_TAG_ID\",\"paperless_url\":\"http://paperless-app:8000\",\"paperless_token\":\"$PAPERLESS_API_TOKEN\",\"docling_url\":\"http://docling-app:5001\",\"docling_api_key\":\"$DOCLING_API_KEY\",\"chunking_enabled\":false,\"processing_mode\":\"Paperless Content\"}}" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/subscriptions/ePaperlessDoclingPlugin")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Paperless source failed: $akbBody" >&2; return 1; } PAPERLESS_SUB_ID="$(printf '%s' "$akbBody" | jq -r '.id')" docker ps | grep paperless-app > /dev/null 2>&1 if [ $? -eq 0 ]; then AKB_WORKFLOW=$(docker exec paperless-app curl -s -X GET http://localhost:8000/api/workflows/$PAPERLESS_AKB_WORKFLOW_ID/ -H "Authorization: Token $PAPERLESS_API_TOKEN") if ! [ -z "$AKB_WORKFLOW" ]; then NEW_ACTION="{ \"type\": 4, \"order\": 99, \"webhook\": { \"url\": \"http://autokb-web/api/subscriptions/$PAPERLESS_SUB_ID/trigger\", \"headers\": { \"Authorization\": \"Bearer $AUTOKB_WEBHOOK_API_KEY\" }, \"include_document\": false } }" UPDATED_WF=$(echo "$AKB_WORKFLOW" | jq --argjson action "$NEW_ACTION" '.actions += [$action]') echo "$UPDATED_WF" | docker exec -i paperless-app curl -s -X PUT http://localhost:8000/api/workflows/$PAPERLESS_AKB_WORKFLOW_ID/ -H "Authorization: Token $PAPERLESS_API_TOKEN" -H "Content-Type: application/json" -d @- > /dev/null 2>&1 fi fi docker ps | grep -q openwebui-app > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "Resolving openWebUISink service id..." akbRes="$(docker exec autokb-web curl -sS -X GET \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/sinks")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Sinks lookup failed: $akbBody" >&2; return 1; } SINK_ID="$(printf '%s' "$akbBody" | jq -r '.[] | select(.name == "openWebUISink") | .service_id' | head -n1)" if [ -z "$SINK_ID" ]; then echo "openWebUISink not found among provisioned sinks" >&2 return 1 fi echo "Creating OpenWebUI target KB ${formal_name}-PKB..." akbRes="$(docker exec autokb-web curl -sS -X POST \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ --data "{\"name\":\"${formal_name}-PKB\",\"api_url\":\"http://openwebui-app:8080\",\"api_key\":\"$OPENWEBUI_ADMIN_API_KEY\",\"target_extra_params\":{},\"include_path_in_filename\":true,\"access_level\":\"PRIVATE\",\"subscription_ids\":[\"$IMAP_PERSONAL_SUB_ID\",\"$PAPERLESS_SUB_ID\"]}" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/sinks/$SINK_ID/targets")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Target creation failed: $akbBody" >&2; return 1; } AUTOKB_PKB_REMOTE_TARGET_ID="$(printf '%s' "$akbBody" | jq -r '.remote_target_id')" echo "Fetching current links for shared target $AUTOKB_SHARED_OWUI_TARGET_ID..." akbRes="$(docker exec autokb-web curl -sS -X GET \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/targets/$AUTOKB_SHARED_OWUI_TARGET_ID")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Target lookup failed: $akbBody" >&2; return 1; } existing="$(printf '%s' "$akbBody" | jq -c '[.subscriptions[].subscription_id | select(. != null)]')" ids="$(printf '%s' "$existing" | jq -c --argjson ns "\"$IMAP_SHARED_SUB_ID\"" '. + [$ns] | unique')" echo "Attaching source $IMAP_SHARED_SUB_ID to shared target $AUTOKB_SHARED_OWUI_TARGET_ID..." akbRes="$(docker exec autokb-web curl -sS -X PUT \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ --data "{\"subscription_ids\":$ids}" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/targets/$AUTOKB_SHARED_OWUI_TARGET_ID")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Target update failed: $akbBody" >&2; return 1; } fi docker ps | grep -q ragflow-app > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "Resolving ragflowSink service id..." akbRes="$(docker exec autokb-web curl -sS -X GET \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/sinks")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Sinks lookup failed: $akbBody" >&2; return 1; } SINK_ID="$(printf '%s' "$akbBody" | jq -r '.[] | select(.name == "ragflowSink") | .service_id' | head -n1)" if [ -z "$SINK_ID" ]; then echo "ragflowSink not found among provisioned sinks" >&2 return 1 fi echo "Creating RAGFlow target KB ${formal_name}-PKB..." akbRes="$(docker exec autokb-web curl -sS -X POST \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ --data "{\"name\":\"${formal_name}-PKB\",\"api_url\":\"http://ragflow-app:9380\",\"api_key\":\"$ragflow_api_key\",\"target_extra_params\":{},\"include_path_in_filename\":true,\"access_level\":\"PRIVATE\",\"subscription_ids\":[\"$IMAP_PERSONAL_SUB_ID\",\"$PAPERLESS_SUB_ID\"]}" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/sinks/$SINK_ID/targets")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Target creation failed: $akbBody" >&2; return 1; } echo "Fetching current links for shared target $AUTOKB_SHARED_RAGFLOW_TARGET_ID..." akbRes="$(docker exec autokb-web curl -sS -X GET \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/targets/$AUTOKB_SHARED_RAGFLOW_TARGET_ID")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Target lookup failed: $akbBody" >&2; return 1; } existing="$(printf '%s' "$akbBody" | jq -c '[.subscriptions[].subscription_id | select(. != null)]')" ids="$(printf '%s' "$existing" | jq -c --argjson ns "\"$IMAP_SHARED_SUB_ID\"" '. + [$ns] | unique')" echo "Attaching source $IMAP_SHARED_SUB_ID to shared target $AUTOKB_SHARED_RAGFLOW_TARGET_ID..." akbRes="$(docker exec autokb-web curl -sS -X PUT \ -H "Authorization: Bearer $AUTOKB_API_KEY" \ -H "Content-Type: application/json" \ --data "{\"subscription_ids\":$ids}" \ -w $'\n%{http_code}' \ "http://autokb-web:80/api/targets/$AUTOKB_SHARED_RAGFLOW_TARGET_ID")" akbCode="${akbRes##*$'\n'}" akbBody="${akbRes%$'\n'*}" [ "$akbCode" -ge 200 ] && [ "$akbCode" -lt 300 ] || { echo "Target update failed: $akbBody" >&2; return 1; } fi echo "Done." } function addPrimaryUserHedgeDoc() { local username="$1" local display_name="$2" local email="$3" docker ps | grep -q hedgedoc-db > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi hd_key_id="$(openssl rand 8 | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')" hd_secret="$(openssl rand 64 | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')" hd_full_token="hd2.${hd_key_id}.${hd_secret}" dtNow="$(date -u '+%Y-%m-%d %H:%M:%S.000')" docker exec -i \ -e "PGPASSWORD=${HEDGEDOC_DATABASE_USER_PASSWORD}" \ "hedgedoc-db" \ psql -q -v ON_ERROR_STOP=1 \ -U "${HEDGEDOC_DATABASE_USER}" \ -d "${HEDGEDOC_DATABASE_NAME}" \ -v v_username="$username" \ -v v_uid="$username" \ -v v_display_name="$display_name" \ -v v_email="$email" \ -v v_label="MCP" \ -v v_key_id="$hd_key_id" \ -v v_secret_hash="$(printf '%s' "$hd_secret" | openssl dgst -sha512 | awk '{print $2}')" \ -v v_now="$dtNow" \ -v v_valid="$(date -u -d "+88 year" '+%Y-%m-%d %H:%M:%S.000')" \ -v v_provider_id="hshq" >/dev/null <<'SQL' BEGIN; INSERT INTO "user" (username, display_name, photo_url, email, author_style, guest_uuid, created_at) VALUES (:'v_username', :'v_display_name', NULL, NULLIF(:'v_email', ''), 1, NULL, :'v_now') ON CONFLICT (username) DO UPDATE SET username = EXCLUDED.username; INSERT INTO identity (user_id, provider_type, provider_identifier, provider_user_id, password_hash, created_at, updated_at) SELECT id, 'ldap', :'v_provider_id', :'v_uid', NULL, :'v_now', :'v_now' FROM "user" WHERE username = :'v_username' ON CONFLICT (user_id, provider_type, provider_identifier) DO UPDATE SET provider_user_id = EXCLUDED.provider_user_id, updated_at = EXCLUDED.updated_at; DELETE FROM api_token WHERE user_id = (SELECT id FROM "user" WHERE username = :'v_username') AND label = :'v_label'; INSERT INTO api_token (id, user_id, label, secret_hash, valid_until, created_at, last_used_at) VALUES (:'v_key_id', (SELECT id FROM "user" WHERE username = :'v_username'), :'v_label', :'v_secret_hash', :'v_valid', :'v_now', NULL); COMMIT; SQL printf '%s\n' "$hd_full_token" } function addPrimaryUsermdCollab() { local username="$1" local display_name="$2" local email="$3" docker ps | grep -q mdcollab-db > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi hd_key_id="$(openssl rand 8 | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')" hd_secret="$(openssl rand 64 | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')" hd_full_token="hd2.${hd_key_id}.${hd_secret}" dtNow="$(date -u '+%Y-%m-%d %H:%M:%S.000')" docker exec -i \ -e "PGPASSWORD=${MDCOLLAB_DATABASE_USER_PASSWORD}" \ "mdcollab-db" \ psql -q -v ON_ERROR_STOP=1 \ -U "${MDCOLLAB_DATABASE_USER}" \ -d "${MDCOLLAB_DATABASE_NAME}" \ -v v_username="$username" \ -v v_uid="$username" \ -v v_display_name="$display_name" \ -v v_email="$email" \ -v v_label="MCP" \ -v v_key_id="$hd_key_id" \ -v v_secret_hash="$(printf '%s' "$hd_secret" | openssl dgst -sha512 | awk '{print $2}')" \ -v v_now="$dtNow" \ -v v_valid="$(date -u -d "+88 year" '+%Y-%m-%d %H:%M:%S.000')" \ -v v_provider_id="hshq" >/dev/null <<'SQL' BEGIN; INSERT INTO "user" (username, display_name, photo_url, email, author_style, guest_uuid, created_at) VALUES (:'v_username', :'v_display_name', NULL, NULLIF(:'v_email', ''), 1, NULL, :'v_now') ON CONFLICT (username) DO UPDATE SET username = EXCLUDED.username; INSERT INTO identity (user_id, provider_type, provider_identifier, provider_user_id, password_hash, created_at, updated_at) SELECT id, 'ldap', :'v_provider_id', :'v_uid', NULL, :'v_now', :'v_now' FROM "user" WHERE username = :'v_username' ON CONFLICT (user_id, provider_type, provider_identifier) DO UPDATE SET provider_user_id = EXCLUDED.provider_user_id, updated_at = EXCLUDED.updated_at; DELETE FROM api_token WHERE user_id = (SELECT id FROM "user" WHERE username = :'v_username') AND label = :'v_label'; INSERT INTO api_token (id, user_id, label, secret_hash, valid_until, created_at, last_used_at) VALUES (:'v_key_id', (SELECT id FROM "user" WHERE username = :'v_username'), :'v_label', :'v_secret_hash', :'v_valid', :'v_now', NULL); COMMIT; SQL printf '%s\n' "$hd_full_token" } function addPrimaryUserMealie() { mle_username="$1" mle_full_name="$2" mle_email="$3" mle_is_admin="$4" docker ps | grep -q mealie-app > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi if [ -z "$mle_is_admin" ]; then mle_is_admin=false fi docker exec -i -e USERNAME="$mle_username" -e EMAIL="$mle_email" -e ADMIN="$mle_is_admin" -e FULLNAME="$mle_full_name" mealie-app python - <<'PYEOF' import os import sys from datetime import timedelta from sqlalchemy import select from mealie.core.security import create_access_token from mealie.db.db_setup import session_context from mealie.db.models.users.users import AuthMethod, LongLiveToken from mealie.repos.all_repositories import get_repositories username = os.environ["USERNAME"] email = os.environ["EMAIL"] admin = os.environ.get("ADMIN", "false").lower() in ("1", "true", "yes", "y") full_name = os.environ["FULLNAME"] with session_context() as session: repos = get_repositories(session, group_id=None, household_id=None) user = repos.users.get_by_username(username) if user is None: user = repos.users.create( { "username": username, "password": "LDAP", "full_name": full_name, "email": email, "admin": admin, "auth_method": AuthMethod.LDAP, } ) existing = session.scalars( select(LongLiveToken).where(LongLiveToken.user_id == user.id, LongLiveToken.name == "MCP") ).first() if existing is not None: print(existing.token) sys.exit(0) token = create_access_token( {"long_token": True, "id": str(user.id), "name": "MCP", "integration_id": "generic"}, timedelta(days=32120), ) session.add(LongLiveToken(name="MCP", token=token, user_id=user.id)) session.commit() print(token) PYEOF } function addPrimaryUserPresenton() { pres_username="$1" pres_password="$2" docker ps | grep -q presenton-app > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi docker exec -i -w /app/servers/fastapi -e USERNAME="$pres_username" -e PASSWORD="$pres_password" presenton-app python - <<'PYEOF' import asyncio import os os.environ.setdefault( "USER_CONFIG_PATH", os.path.join(os.getenv("APP_DATA_DIRECTORY", "/app_data"), "userConfig.json"), ) from sqlalchemy import select from api.v1.auth.users import PASSWORD_HELPER from models.sql.api_key import ApiKey from models.sql.user import User from services.api_keys import issue_api_key, reveal_api_key from services.database import async_session_maker async def main() -> None: username = os.environ["USERNAME"].strip() password = os.environ["PASSWORD"] admin = True async with async_session_maker() as session: user = await session.scalar(select(User).where(User.username == username)) if user is None: user = User( username=username, hashed_password=PASSWORD_HELPER.hash(password), is_active=True, is_verified=True, is_superuser=admin, auth_version=1, ) session.add(user) await session.commit() await session.refresh(user) existing = ( await session.scalars( select(ApiKey) .where( ApiKey.user_id == user.id, ApiKey.revoked_at.is_(None), ) .order_by(ApiKey.created_at.desc()) ) ).all() for api_key in existing: token = reveal_api_key(api_key) if token is not None: print(token) return _, token = await issue_api_key( session, user_id=user.id, created_by_id=user.id, label="CLI provisioned key", expiry_days=365, ) print(token) asyncio.run(main()) PYEOF } function addPrimaryUserTwenty() { set +e local user_uid="$1" local user_email="$2" local user_password="$3" local user_first_name="$4" local user_last_name="$5" local user_emailpw="$6" local admin_email="$TWENTY_ADMIN_EMAIL_ADDRESS" local admin_password="$TWENTY_ADMIN_PASSWORD" local origin="https://$SUB_TWENTY.$HOMESERVER_DOMAIN" local user_position="Team Member" local imap_host="$SMTP_HOSTNAME" local imap_port="993" local imap_user="$user_email" local imap_pass="$user_emailpw" local imap_sec="SSL_TLS" local smtp_host="$SMTP_HOSTNAME" local smtp_port="587" local smtp_user="$user_email" local smtp_pass="$user_emailpw" local smtp_sec="STARTTLS" local caldav_host="" local caldav_port="443" local caldav_user="" local caldav_pass="" local caldav_sec="" local is_nextcloud_installed=false docker ps | grep -q twenty-app > /dev/null 2>&1 if [ $? -ne 0 ]; then return fi docker ps | grep -q nextcloud-app > /dev/null 2>&1 if [ $? -eq 0 ]; then is_nextcloud_installed=true caldav_host="https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/remote.php/dav" caldav_port="443" caldav_user="$user_uid" caldav_pass="$user_password" caldav_sec="SSL_TLS" fi local expires_at expires_at="$(date -u -d "+88 years" +%Y-%m-%dT%H:%M:%S.000Z)" log(){ echo "$*" >&2; } jwt_claim() { local b64="${1#*.}"; b64="${b64%.*}" local pad=$(( (4 - ${#b64} % 4) % 4 )) i for ((i=0;i/dev/null | jq -r "$2" } app_curl_exec(){ docker exec twenty-app curl -sS --max-time 30 "$@"; } gql(){ local query="$1" vars="$2" bearer="$3" local hdr=(-H 'Content-Type: application/json' -H "Origin: $origin") if [[ -n "$bearer" ]]; then hdr+=(-H "Authorization: Bearer $bearer"); fi app_curl_exec -X POST "${hdr[@]}" http://localhost:3000/metadata \ --data "$(jq -cn --arg q "$query" --argjson v "$vars" '{query:$q,variables:$v}')" } local lt lt="$(gql \ 'mutation($email:String!,$password:String!,$origin:String!){ getLoginTokenFromCredentials(email:$email,password:$password,origin:$origin){ loginToken{ token } } }' \ "{\"email\":\"$admin_email\",\"password\":\"$admin_password\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getLoginTokenFromCredentials.loginToken.token')" if [[ -z "$lt" || "$lt" == "null" ]]; then log "ERROR: admin password login failed for '$admin_email'." >&2 return 1 fi local admin_token workspace_id admin_token="$(gql \ 'mutation($loginToken:String!,$origin:String!){ getAuthTokensFromLoginToken(loginToken:$loginToken,origin:$origin){ tokens{ accessOrWorkspaceAgnosticToken{ token } } } }' \ "{\"loginToken\":\"$lt\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token')" if [[ -z "$admin_token" || "$admin_token" == "null" ]]; then log "ERROR: could not obtain an admin ACCESS token." >&2 return 1 fi workspace_id="$(jwt_claim "$admin_token" '.workspaceId')" local member_role_id member_role_id="$(docker exec -i -e WSID="$workspace_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT \"defaultRoleId\" FROM core.workspace WHERE id = '\''$WSID'\''" ' 2>/dev/null | tr -d '[:space:]')" if [[ -z "$member_role_id" ]]; then log "ERROR: could not resolve default (Member) role for workspace '$workspace_id'." >&2 return 1 fi local lower_email invite_token lower_email="$(printf '%s' "$user_email" | tr '[:upper:]' '[:lower:]')" invite_token="$(openssl rand -hex 32)" if ! docker exec -i -e WSID="$workspace_id" -e EMAIL="$lower_email" \ -e TOKEN="$invite_token" -e EXP="$(date -u -d "+30 days" +%Y-%m-%dT%H:%M:%SZ)" \ -e ROLEID="$member_role_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "INSERT INTO core.\"appToken\" (id, \"workspaceId\", type, value, \"expiresAt\", context) VALUES (gen_random_uuid(), '\''$WSID'\'', '\''INVITATION_TOKEN'\'', '\''$TOKEN'\'', '\''$EXP'\'', jsonb_build_object('\''email'\'', '\''$EMAIL'\'', '\''roleId'\'', '\''$ROLEID'\''::uuid))" ' >/dev/null 2>&1; then log "ERROR: could not create invitation token." >&2 return 1 fi local user_login_token user_token user_login_token="$(gql \ 'mutation($email:String!,$password:String!,$workspaceId:UUID,$workspacePersonalInviteToken:String!,$locale:String!){ signUpInWorkspace(email:$email,password:$password,workspaceId:$workspaceId,workspacePersonalInviteToken:$workspacePersonalInviteToken,locale:$locale){ loginToken{ token } } }' \ "{\"email\":\"$lower_email\",\"password\":\"$user_password\",\"workspaceId\":\"$workspace_id\",\"workspacePersonalInviteToken\":\"$invite_token\",\"locale\":\"en\"}" "" \ | jq -r '.data.signUpInWorkspace.loginToken.token')" if [[ -z "$user_login_token" || "$user_login_token" == "null" ]]; then log "ERROR: signUpInWorkspace failed for '$lower_email'." >&2 return 1 fi user_token="$(gql \ 'mutation($loginToken:String!,$origin:String!){ getAuthTokensFromLoginToken(loginToken:$loginToken,origin:$origin){ tokens{ accessOrWorkspaceAgnosticToken{ token } } } }' \ "{\"loginToken\":\"$user_login_token\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token')" if [[ -z "$user_token" || "$user_token" == "null" ]]; then log "ERROR: could not obtain a user ACCESS token." >&2 return 1 fi local user_wm_id user_wm_id="$(jwt_claim "$user_token" '.workspaceMemberId')" gql \ 'mutation($input: UpdateWorkspaceMemberSettingsInput!){ updateWorkspaceMemberSettings(input:$input) }' \ "{\"input\":{\"workspaceMemberId\":\"$user_wm_id\",\"update\":{\"name\":{\"firstName\":\"$user_first_name\",\"lastName\":\"$user_last_name\"},\"jobTitle\":\"$user_position\"}}}" \ "$user_token" >/dev/null 2>&1 local conn_params conn_params="$(jq -cn \ --arg n "$lower_email" \ --arg ih "$imap_host" --argjson ip "$imap_port" \ --arg iu "$imap_user" --arg ipa "$imap_pass" --arg is "$imap_sec" \ --arg sh "$smtp_host" --argjson sp "$smtp_port" \ --arg su "$smtp_user" --arg spa "$smtp_pass" --arg ss "$smtp_sec" \ --arg ch "$caldav_host" --argjson cp "$caldav_port" \ --arg cu "$caldav_user" --arg cpa "$caldav_pass" --arg cs "$caldav_sec" \ '{name:$n, IMAP:{host:$ih,port:$ip,username:$iu,password:$ipa,connectionSecurity:$is}, SMTP:{host:$sh,port:$sp,username:$su,password:$spa,connectionSecurity:$ss}} + (if ($ch != "") then {CALDAV:{host:$ch,port:$cp,username:$cu,password:$cpa,connectionSecurity:$cs}} else {} end)')" local save_res save_res="$(gql \ 'mutation($handle:String!,$connectionParameters:EmailAccountConnectionParameters!){ saveImapSmtpCaldavAccount(handle:$handle,connectionParameters:$connectionParameters){ success connectedAccountId } }' \ "$(jq -cn --arg h "$lower_email" --argjson cp "$conn_params" '{handle:$h,connectionParameters:$cp}')" \ "$user_token")" if [[ "$(printf '%s' "$save_res" | jq -r '.data.saveImapSmtpCaldavAccount.success // false' 2>/dev/null)" != "true" ]]; then log "ERROR: saveImapSmtpCaldavAccount failed: $save_res" >&2 return 1 fi local acct_id mc_id acct_id="$(printf '%s' "$save_res" | jq -r '.data.saveImapSmtpCaldavAccount.connectedAccountId // empty')" if [[ -z "$acct_id" ]]; then log "ERROR: could not resolve connectedAccountId for user mail sync." >&2 return 1 fi mc_id="$(docker exec -i -e AID="$acct_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT id FROM core.\"messageChannel\" WHERE \"connectedAccountId\" = '\''$AID'\''" ' 2>/dev/null | tr -d '[:space:]')" if [[ -n "$mc_id" ]]; then docker exec -i -e MCID="$mc_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "UPDATE core.\"messageChannel\" SET \"messageFolderImportPolicy\" = '\''SELECTED_FOLDERS'\'' WHERE id = '\''$MCID'\''; UPDATE core.\"messageFolder\" SET \"isSynced\" = false WHERE \"messageChannelId\" = '\''$MCID'\''; UPDATE core.\"messageFolder\" f SET \"isSynced\" = true FROM core.\"messageFolder\" p WHERE f.\"messageChannelId\" = '\''$MCID'\'' AND f.name = '\''Work'\'' AND p.\"externalId\" = f.\"parentFolderId\" AND p.name = '\''Processed'\'' AND p.\"messageChannelId\" = '\''$MCID'\'';" ' >/dev/null 2>&1 local synced_work unsynced_count synced_work="$(docker exec -i -e MCID="$mc_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT count(*) FROM core.\"messageFolder\" f JOIN core.\"messageFolder\" p ON p.\"externalId\" = f.\"parentFolderId\" WHERE f.\"messageChannelId\" = '\''$MCID'\'' AND f.name = '\''Work'\'' AND p.name = '\''Processed'\'' AND f.\"isSynced\" = true" ' 2>/dev/null | tr -d '[:space:]')" unsynced_count="$(docker exec -i -e MCID="$mc_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT count(*) FROM core.\"messageFolder\" WHERE \"messageChannelId\" = '\''$MCID'\'' AND NOT \"isSynced\"" ' 2>/dev/null | tr -d '[:space:]')" log "$(date -u +%T) email folder policy = SELECTED_FOLDERS (Processed -> Work synced=$synced_work, others unsynced=$unsynced_count)" if [[ "$synced_work" != "1" ]]; then log "WARN: Processed -> Work folder was not set as the only synced folder (synced_work=$synced_work)." >&2 fi fi docker exec -i -e AID="$acct_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "UPDATE core.\"calendarChannel\" SET visibility = '\''SHARE_EVERYTHING'\'' WHERE \"connectedAccountId\" = '\''$AID'\''" ' >/dev/null 2>&1 gql \ 'mutation($connectedAccountId: UUID!){ startChannelSync(connectedAccountId: $connectedAccountId){ success } }' \ "{\"connectedAccountId\":\"$acct_id\"}" "$user_token" >/dev/null log "$(date -u +%T) channel sync started -> account setup autocompleted" local key_name="MCP-$lower_email" local key_id key_id="$(app_curl_exec -sS -X GET -H "Authorization: Bearer $admin_token" \ -H 'Content-Type: application/json' \ --max-time 30 "http://localhost:3000/rest/apiKeys" 2>/dev/null \ | jq -r --arg n "$key_name" '.[]? | select(.name==$n and .revokedAt==null) | .id' 2>/dev/null \ | head -n1)" if [[ -z "$key_id" ]]; then key_id="$(app_curl_exec -sS -X POST -H "Authorization: Bearer $admin_token" \ -H 'Content-Type: application/json' \ --data "$(jq -cn --arg n "$key_name" --arg e "$expires_at" --arg r "$member_role_id" \ '{name:$n,expiresAt:$e,roleId:$r}')" \ "http://localhost:3000/rest/apiKeys" 2>/dev/null \ | jq -r '.id' 2>/dev/null)" if [[ -z "$key_id" || "$key_id" == "null" ]]; then log "ERROR: could not create API key for '$lower_email'." >&2 return 1 fi fi local token token="$(gql \ 'mutation($apiKeyId:UUID!,$expiresAt:String!){ generateApiKeyToken(apiKeyId:$apiKeyId,expiresAt:$expiresAt){ token } }' \ "{\"apiKeyId\":\"$key_id\",\"expiresAt\":\"$expires_at\"}" "$admin_token" \ | jq -r '.data.generateApiKeyToken.token')" if [[ -z "$token" || "$token" == "null" ]]; then log "ERROR: could not generate API key token for '$lower_email'." >&2 return 1 fi echo "$token" } function addPrimaryUserRAGFlow() { set +e if ! docker ps | grep -q ragflow-app > /dev/null 2>&1; then return fi rf_email="$1" rf_name="$2" rf_password="$3" rf_password_b64=$(printf '%s' "$rf_password" | base64 | tr -d '\n') rf_admin_password_b64=$(printf '%s' "$RAGFLOW_ADMIN_PASSWORD" | base64 | tr -d '\n') rf_email_q=$(jq -rn --arg v "$rf_email" '$v|@uri') rf_admin_token=$(docker exec -i ragflow-app curl -sS -D - -o /dev/null -X POST "http://127.0.0.1:9381/api/v1/admin/login" -H "Content-Type: application/json" -d "$(jq -n --arg email "$RAGFLOW_ADMIN_EMAIL_ADDRESS" --arg password "$rf_admin_password_b64" '{email:$email,password:$password}')" | tr -d '\r' | awk 'tolower($1)=="authorization:"{print $2; exit}') if [ -z "$rf_admin_token" ]; then echo "ERROR: RAGFlow admin login failed for $RAGFLOW_ADMIN_EMAIL_ADDRESS" >&2 return 1 fi rf_user_json=$(docker exec -i ragflow-app curl -sS "http://127.0.0.1:9381/api/v1/admin/users/$rf_email_q" -H "Authorization: $rf_admin_token") rf_user_code=$(printf '%s' "$rf_user_json" | jq -r '.code // 1') if [ "$rf_user_code" != "0" ]; then rf_create_json=$(docker exec -i ragflow-app curl -sS -X POST "http://127.0.0.1:9381/api/v1/admin/users" -H "Authorization: $rf_admin_token" -H "Content-Type: application/json" -d "$(jq -n --arg u "$rf_email" --arg p "$rf_password_b64" '{username:$u,password:$p,role:"user"}')") rf_create_code=$(printf '%s' "$rf_create_json" | jq -r '.code // 1') if [ "$rf_create_code" != "0" ]; then echo "ERROR: RAGFlow user create failed: $(printf '%s' "$rf_create_json" | jq -r '.message // empty')" >&2 return 1 fi fi rf_user_token=$(docker exec -i ragflow-app curl -sS -D - -o /dev/null -X POST "http://127.0.0.1:9380/api/v1/auth/login" -H "Content-Type: application/json" -d "$(jq -n --arg email "$rf_email" --arg password "$rf_password_b64" '{email:$email,password:$password}')" | tr -d '\r' | awk 'tolower($1)=="authorization:"{print $2; exit}') if [ -z "$rf_user_token" ]; then docker exec -i ragflow-app curl -sS -X PUT "http://127.0.0.1:9381/api/v1/admin/users/$rf_email_q/password" -H "Authorization: $rf_admin_token" -H "Content-Type: application/json" -d "$(jq -n --arg p "$rf_password_b64" '{new_password:$p}')" >/dev/null rf_user_token=$(docker exec -i ragflow-app curl -sS -D - -o /dev/null -X POST "http://127.0.0.1:9380/api/v1/auth/login" -H "Content-Type: application/json" -d "$(jq -n --arg email "$rf_email" --arg password "$rf_password_b64" '{email:$email,password:$password}')" | tr -d '\r' | awk 'tolower($1)=="authorization:"{print $2; exit}') fi if [ -z "$rf_user_token" ]; then echo "ERROR: RAGFlow user login failed for $rf_email" >&2 return 1 fi rf_key_json=$(docker exec -i ragflow-app curl -sS -X POST "http://127.0.0.1:9380/api/v1/system/keys" -H "Authorization: $rf_user_token" -H "Content-Type: application/json" -d '{}') rf_key=$(printf '%s' "$rf_key_json" | jq -r '.data.token // empty') if [ -z "$rf_key" ]; then echo "ERROR: RAGFlow user key mint failed" >&2 return 1 fi newuser_ragflow_api_key="$rf_key" rf_profile_json=$(jq -n --arg n "$rf_name" --arg tz "$TZ" 'if $tz=="" then {nickname:$n} else {nickname:$n,timezone:$tz} end') docker exec -i ragflow-app curl -sS -X PATCH "http://127.0.0.1:9380/api/v1/users/me" -H "Authorization: $rf_key" -H "Content-Type: application/json" -d "$rf_profile_json" >/dev/null if [ -f "$HSHQ_STACKS_DIR/ragflow/config/ragflow_bootstrap.json" ]; then mapfile -t rf_providers < <(jq -c '.providers[]' "$HSHQ_STACKS_DIR/ragflow/config/ragflow_bootstrap.json") for rf_provider in "${rf_providers[@]}"; do rf_pname=$(printf '%s' "$rf_provider" | jq -r '.name') rf_pname_q=$(jq -rn --arg v "$rf_pname" '$v|@uri') docker exec -i ragflow-app curl -sS -X PUT "http://127.0.0.1:9380/api/v1/providers" -H "Authorization: $rf_key" -H "Content-Type: application/json" -d "$(jq -n --arg p "$rf_pname" '{provider_name:$p}')" >/dev/null rf_existing=$(docker exec -i ragflow-app curl -sS "http://127.0.0.1:9380/api/v1/providers/$rf_pname_q/instances" -H "Authorization: $rf_key") mapfile -t rf_instances < <(printf '%s' "$rf_provider" | jq -c '.instances[]?') for rf_inst in "${rf_instances[@]}"; do rf_iname=$(printf '%s' "$rf_inst" | jq -r '.instance_name') rf_iname_q=$(jq -rn --arg v "$rf_iname" '$v|@uri') rf_payload=$(printf '%s' "$rf_inst" | jq -c '{instance_name:.instance_name,api_key:(.api_key // ""),base_url:(.base_url // ""),region:(.region // ""),model_info:(.models // [])}') if printf '%s' "$rf_existing" | jq -e --arg n "$rf_iname" '.data[]? | select(.instance_name == $n)' >/dev/null 2>&1; then rf_update=$(printf '%s' "$rf_payload" | jq -c '. + {verify:false}') docker exec -i ragflow-app curl -sS -X PUT "http://127.0.0.1:9380/api/v1/providers/$rf_pname_q/instances/$rf_iname_q" -H "Authorization: $rf_key" -H "Content-Type: application/json" -d "$rf_update" >/dev/null else docker exec -i ragflow-app curl -sS -X POST "http://127.0.0.1:9380/api/v1/providers/$rf_pname_q/instances" -H "Authorization: $rf_key" -H "Content-Type: application/json" -d "$rf_payload" >/dev/null fi done done mapfile -t rf_defaults < <(jq -c '.defaults[]' "$HSHQ_STACKS_DIR/ragflow/config/ragflow_bootstrap.json") for rf_default in "${rf_defaults[@]}"; do docker exec -i ragflow-app curl -sS -X PATCH "http://127.0.0.1:9380/api/v1/models/default" -H "Authorization: $rf_key" -H "Content-Type: application/json" -d "$rf_default" >/dev/null done fi rf_team="$RAGFLOW_TEAM_TENANT_ID" if [ -z "$rf_team" ] && [ -n "$RAGFLOW_TEAM_API_KEY" ]; then rf_team_json=$(docker exec -i ragflow-app curl -sS "http://127.0.0.1:9380/api/v1/users/me" -H "Authorization: $RAGFLOW_TEAM_API_KEY") rf_team=$(printf '%s' "$rf_team_json" | jq -r '.data.id // .data.tenant_id // empty') fi if [ -n "$rf_team" ] && [ -n "$RAGFLOW_TEAM_API_KEY" ]; then rf_invite=$(docker exec -i ragflow-app curl -sS -X POST "http://127.0.0.1:9380/api/v1/tenants/$rf_team/users" -H "Authorization: $RAGFLOW_TEAM_API_KEY" -H "Content-Type: application/json" -d "$(jq -n --arg e "$rf_email" '{email:$e}')") rf_invite_code=$(printf '%s' "$rf_invite" | jq -r '.code // empty') if [ -n "$rf_invite_code" ] && [ "$rf_invite_code" != "0" ]; then echo "WARN: RAGFlow team invite: $(printf '%s' "$rf_invite" | jq -r '.message // empty')" >&2 fi rf_accept=$(docker exec -i ragflow-app curl -sS -X PATCH "http://127.0.0.1:9380/api/v1/tenants/$rf_team" -H "Authorization: $rf_key") rf_accept_code=$(printf '%s' "$rf_accept" | jq -r '.code // empty') if [ -n "$rf_accept_code" ] && [ "$rf_accept_code" != "0" ]; then echo "WARN: RAGFlow team accept: $(printf '%s' "$rf_accept" | jq -r '.message // empty')" >&2 fi else echo "WARN: RAGFlow team not configured; user key created without team access" >&2 fi printf '%s\n' "$newuser_ragflow_api_key" } function addPrimaryGroupMCPServerOpenWebUI() { mcp_svc_name="$1" mcp_svc_proper="$2" mcp_enabled="$3" mcp_owui_uuid="$5" mcp_header_type="$6" mcp_header_val="$7" mcp_function_list="$8" mcp_new_connection=$(jq -n \ --arg url "http://litellm-proxy:4000/mcp/$mcp_svc_name" \ --arg auth_key "$LITELLM_AUTH_KEY" \ --arg mcp_header_type "$mcp_header_type" \ --arg mcp_header_val "$mcp_header_val" \ --arg mcp_enabled "$mcp_enabled" \ --arg s_id "${mcp_svc_name}" \ --arg s_name "$mcp_svc_proper" \ --arg s_desc "$mcp_svc_proper" \ --arg mcp_function_list "$mcp_function_list" \ --arg mcp_owui_uuid "$mcp_owui_uuid" \ '{ url: $url, path: "openapi.json", type: "mcp", auth_type: "bearer", headers: { ($mcp_header_type): $mcp_header_val }, key: $auth_key, config: { enable: $mcp_enabled, function_name_filter_list: $mcp_function_list, access_grants: [ { principal_type: "group", principal_id: $mcp_owui_uuid, permission: "read" } ] }, spec_type: "url", spec: "", info: { id: $s_id, name: $s_name, description: $s_desc } }') mcp_current_config=$(curl -s -X GET https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/api/v1/configs/tool_servers -H "Authorization: Bearer $OPENWEBUI_ADMIN_API_KEY" -H "Content-Type: application/json") updated_config=$(echo "$mcp_current_config" | jq --argjson new_conn "$mcp_new_connection" '.TOOL_SERVER_CONNECTIONS += [$new_conn] | {TOOL_SERVER_CONNECTIONS: .TOOL_SERVER_CONNECTIONS}') response=$(curl -s -X POST https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/api/v1/configs/tool_servers -H "Authorization: Bearer $OPENWEBUI_ADMIN_API_KEY" -H "Content-Type: application/json" -d "$updated_config") } function upgradeDatabaseInStack() { stackName="$1" dbType="$2" dbName="$3" dbUser="$4" dbPassword="$5" dbRootPassword="$6" curHostDBDirectory="$7" curExportDirectory="$8" curContainerDirectory="${9}" curImage="${10}" newImage="${11}" set +e cur_stack_id=$(getStackID $stackName) if [ -z "$stackName" ] || [ -z "$cur_stack_id" ]; then return fi stackStatus=$(getStackStatusByName $stackName) if [ $stackStatus -eq 1 ]; then startStopStackByID $cur_stack_id stop fi if [ $? -ne 0 ]; then echo "ERROR: Stack did not shut down properly, returning..." return 1 fi sudo rm -fr /tmp/$stackName mkdir /tmp/$stackName echo "upgradeDatabaseInStack - Starting db container with current image..." case "$dbType" in postgres) docker run --name dbctemp -d --user="${USERID}:${GROUPID}" -e TZ=$TZ -e POSTGRES_DB=$dbName -e POSTGRES_USER=$dbUser -e POSTGRES_PASSWORD=$dbPassword -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro -v ${curHostDBDirectory}:${curContainerDirectory} -v ${HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro -v ${curExportDirectory}:/dbexport $curImage > /dev/null 2>&1 waitForContainerLogString dbctemp 1 300 "ready to accept connections" ;; mysql) docker run --name dbctemp -d -e TZ=$TZ -e MYSQL_DATABASE=$dbName -e MYSQL_USER=$dbUser -e MYSQL_PASSWORD=$dbPassword -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro -v ${curHostDBDirectory}:${curContainerDirectory} -v ${HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro -v ${curExportDirectory}:/dbexport $curImage mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 > /dev/null 2>&1 waitForContainerLogString dbctemp 1 300 "ready for connections" ;; mariadb) docker run --name dbctemp -d -e TZ=$TZ -e MARIADB_DATABASE=$dbName -e MARIADB_USER=$dbUser -e MARIADB_PASSWORD=$dbPassword -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro -v ${curHostDBDirectory}:${curContainerDirectory} -v ${HSHQ_SCRIPTS_DIR}/user/exportMariaDB.sh:/exportDB.sh:ro -v ${curExportDirectory}:/dbexport $curImage mariadbd --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 > /dev/null 2>&1 waitForContainerLogString dbctemp 1 300 "ready for connections" ;; *) echo "ERROR: Unknown database type, returning..." return 2 ;; esac if [ $? -ne 0 ]; then echo "ERROR: Database container did not start properly, returning..." return 3 fi echo "upgradeDatabaseInStack - Exporting db..." sleep 1 docker exec dbctemp bash -c "/exportDB.sh" > /dev/null 2>&1 docker container stop dbctemp > /dev/null 2>&1 docker container rm dbctemp > /dev/null 2>&1 sudo cp $curExportDirectory/${dbName}.sql /tmp/$stackName/${dbName}.sql sudo chmod 0555 /tmp/$stackName/${dbName}.sql sudo mv $curHostDBDirectory ${curHostDBDirectory}_bak mkdir $curHostDBDirectory rtVal=0 echo "upgradeDatabaseInStack - Starting db container with new image..." case "$dbType" in postgres) docker run --name dbctemp -d --user="${USERID}:${GROUPID}" -e TZ=$TZ -e POSTGRES_DB=$dbName -e POSTGRES_USER=$dbUser -e POSTGRES_PASSWORD=$dbPassword -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro -v ${curHostDBDirectory}:${curContainerDirectory} -v /tmp/$stackName:/dbimport $newImage > /dev/null 2>&1 waitForContainerLogString dbctemp 1 300 "ready to accept connections" sleep 3 docker exec dbctemp /bin/bash -c "PGPASSWORD=$dbPassword psql --username $dbUser $dbName -f /dbimport/${dbName}.sql" > /dev/null 2>&1 ;; mysql) docker run --name dbctemp -d -e TZ=$TZ -e MYSQL_DATABASE=$dbName -e MYSQL_ROOT_PASSWORD=$dbRootPassword -e MYSQL_USER=$dbUser -e MYSQL_PASSWORD=$dbPassword -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro -v ${curHostDBDirectory}:${curContainerDirectory} -v /tmp/$stackName:/dbimport $newImage mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 > /dev/null 2>&1 waitForContainerLogString dbctemp 1 300 "ready for connections" sleep 3 docker exec dbctemp /bin/bash -c "mysql -u $dbUser -p$dbPassword < /dbimport/${dbName}.sql" > /dev/null 2>&1 ;; mariadb) docker run --name dbctemp -d -e TZ=$TZ -e MARIADB_DATABASE=$dbName -e MARIADB_ROOT_PASSWORD=$dbRootPassword -e MARIADB_USER=$dbUser -e MARIADB_PASSWORD=$dbPassword -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro -v ${curHostDBDirectory}:${curContainerDirectory} -v /tmp/$stackName:/dbimport $newImage mariadbd --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 > /dev/null 2>&1 waitForContainerLogString dbctemp 1 300 "ready for connections" sleep 3 docker exec dbctemp /bin/bash -c "mysql -u $dbUser -p$dbPassword < /dbimport/${dbName}.sql" > /dev/null 2>&1 ;; *) echo "ERROR: Unknown database type, returning..." return 4 ;; esac if [ $? -eq 0 ]; then sudo rm -fr ${curHostDBDirectory}_bak else echo "ERROR: Database did not restore correctly. You can restore from the backup directory(${curHostDBDirectory}_bak) or retry..." rtVal=5 fi sleep 5 docker container stop dbctemp > /dev/null 2>&1 docker container rm dbctemp > /dev/null 2>&1 sudo rm -fr /tmp/$stackName echo "upgradeDatabaseInStack - Import complete!" return $rtVal } function addReadOnlyUserToDatabase() { rou_name="$1" rou_dbtype="$2" rou_container="$3" rou_databasename="$4" rou_rootuser="$5" rou_rootpassword="$6" rou_directory="$7" rou_readonlyuser="$8" rou_readonlypassword="$9" set +e docker ps | grep -q $rou_container > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "Database container ($rou_container) not found, returning..." return fi case "$rou_dbtype" in "mysql") cat < $rou_directory/addreadonly.sql CREATE USER IF NOT EXISTS '$rou_readonlyuser'@'%' IDENTIFIED BY '$rou_readonlypassword'; GRANT SELECT, SHOW VIEW ON $rou_databasename.* TO '$rou_readonlyuser'@'%'; FLUSH PRIVILEGES; EOFPC ro_res=$(docker exec $rou_container bash -c "mariadb -N -u root -p$rou_rootpassword < /dbexport/addreadonly.sql" 2>&1) ro_ret=$? rm -f $rou_directory/addreadonly.sql ;; "postgres") cat < $rou_directory/addreadonly.sql DO \$do\$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'readaccess') THEN CREATE ROLE readaccess LOGIN PASSWORD '$rou_readonlypassword'; ELSE RAISE NOTICE 'Role "readaccess" already exists. Skipping.'; END IF; END \$do\$; GRANT CONNECT ON DATABASE $rou_databasename TO readaccess; GRANT USAGE ON SCHEMA public TO readaccess; GRANT SELECT ON ALL TABLES IN SCHEMA public TO readaccess; CREATE USER "$rou_readonlyuser" WITH PASSWORD '$rou_readonlypassword'; GRANT readaccess TO "$rou_readonlyuser"; EOFPC ro_res=$(docker exec -u postgres $rou_container psql $rou_databasename $rou_rootuser -f /dbexport/addreadonly.sql 2>&1) ro_ret=$? rm -f $rou_directory/addreadonly.sql ;; "mongodb") cat < $rou_directory/addreadonly.js use admin; const existingUser = db.getUser("$rou_readonlyuser"); if (!existingUser) { db.createUser({ user: "$rou_readonlyuser", pwd: "$rou_readonlypassword", roles: [ { role: "readAnyDatabase", db: "admin" } ] }); } EOFPC ro_res=$(docker exec $rou_container sh -c "mongosh --host localhost -u $rou_rootuser -p $rou_rootpassword --authenticationDatabase admin < /dump/addreadonly.js" 2>&1) ro_ret=$? rm -f $rou_directory/addreadonly.js ;; *) echo "ERROR: Case not found, returning..." return 1 ;; esac if [ $ro_ret -eq 0 ]; then echo "The readonly user ($rou_readonlyuser) was added successfully!" else echo "ERROR: There was a problem adding this readonly user ($rou_readonlyuser): $ro_res" return 1 fi } # Services Functions function loadPinnedDockerImages() { # Common images IMG_ELASTICSEARCH=mirror.gcr.io/elasticsearch:9.1.1 IMG_MEILISEARCH=mirror.gcr.io/getmeili/meilisearch:v1.49.0 IMG_MYSQL=mirror.gcr.io/mariadb:10.7.3 IMG_NGINX=mirror.gcr.io/nginx:1.28.0-alpine IMG_POSTGRES=mirror.gcr.io/postgres:15.0-bullseye IMG_REDIS=mirror.gcr.io/valkey/valkey:alpine3.23 IMG_ALPINE=mirror.gcr.io/alpine:3.22.1 IMG_MINIO=ghcr.io/homeserverhq/minio:v1.0 IMG_MONGODB=mirror.gcr.io/mongo:8.0.13 IMG_FRAPPE_BENCH=mirror.gcr.io/frappe/bench:v5.25.9 # Stack specific images IMG_ACTIVEPIECES_APP=ghcr.io/activepieces/activepieces:0.90.4 IMG_ADGUARD=mirror.gcr.io/adguard/adguardhome:v0.107.78 IMG_ADMINER=mirror.gcr.io/adminer:5.4.1 IMG_AISTACK_MINDSDB_APP=mirror.gcr.io/mindsdb/mindsdb:v25.7.4.0 IMG_AISTACK_MINDSDB_MOD_APP=hshq/mindsdb:v1 IMG_AISTACK_OPENTELEMETRY=mirror.gcr.io/otel/opentelemetry-collector-contrib:0.131.1 IMG_AISTACK_LANGFUSE=mirror.gcr.io/langfuse/langfuse:2.87.0 IMG_AISTACK_OLLAMA_SERVER=mirror.gcr.io/ollama/ollama:0.11.4 IMG_AISTACK_OPENWEBUI=ghcr.io/open-webui/open-webui:0.6.22 IMG_AUDIOBOOKSHELF=ghcr.io/advplyr/audiobookshelf:2.31.0 IMG_AUTHELIA=mirror.gcr.io/authelia/authelia:4.39.20 IMG_AUTOMATISCH_APP=mirror.gcr.io/automatischio/automatisch:0.15.0 IMG_BARASSISTANT_APP=mirror.gcr.io/barassistant/server:5.15.3 IMG_BARASSISTANT_SALTRIM=mirror.gcr.io/barassistant/salt-rim:4.15.0 IMG_BARASSISTANT_MCP=ghcr.io/homeserverhq/barassistant-mcp:v2 IMG_BUDIBASE_APP=mirror.gcr.io/budibase/apps:3.23.36 IMG_BUDIBASE_WORKER=mirror.gcr.io/budibase/worker:3.23.36 IMG_BUDIBASE_PROXY=mirror.gcr.io/budibase/proxy:3.23.36 IMG_BUDIBASE_COUCHDB=mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1 IMG_CADDY=ghcr.io/homeserverhq/caddy-crowdsec:2.11.7 IMG_CALCOM=mirror.gcr.io/calcom/cal.com:v5.9.8 IMG_CALIBRE_SERVER=mirror.gcr.io/linuxserver/calibre:v8.15.0-ls373 IMG_CALIBRE_WEB=mirror.gcr.io/linuxserver/calibre-web:0.6.25-ls359 IMG_CHANGEDETECTION_APP=ghcr.io/dgtlmoon/changedetection.io:0.51.4 IMG_CHANGEDETECTION_PLAYWRIGHT_CHROME=mirror.gcr.io/dgtlmoon/sockpuppetbrowser:latest IMG_CLOUDBEAVER_APP=mirror.gcr.io/dbeaver/cloudbeaver:25.3.0 IMG_CODESERVER=mirror.gcr.io/codercom/code-server:4.106.3 IMG_COLLABORA=mirror.gcr.io/collabora/code:26.04.2.1.1 IMG_COTURN=mirror.gcr.io/coturn/coturn:4.7.0 IMG_DBGATE_APP=mirror.gcr.io/dbgate/dbgate:6.7.3-alpine IMG_DISCOURSE=mirror.gcr.io/bitnami/discourse:3.4.7 IMG_DNSMASQ=mirror.gcr.io/jpillora/dnsmasq:1.1 IMG_DOLIBARR_APP=mirror.gcr.io/dolibarr/dolibarr:23.0.4 IMG_DOLIBARR_MCP=ghcr.io/homeserverhq/dolibarr-mcp:v2 IMG_DOZZLE=mirror.gcr.io/amir20/dozzle:v6.1.1 IMG_DRAWIO_PLANTUML=mirror.gcr.io/jgraph/plantuml-server IMG_DRAWIO_EXPORT=mirror.gcr.io/jgraph/export-server IMG_DRAWIO_WEB=mirror.gcr.io/jgraph/drawio:29.2.2 IMG_DRAWIO_NEXTAI=ghcr.io/dayuanjiang/next-ai-draw-io:sha-5007c7b IMG_DUPLICATI=mirror.gcr.io/linuxserver/duplicati:v2.3.0.4_stable_2026-07-09-ls300 IMG_EASYAPPOINTMENTS_APP=mirror.gcr.io/alextselegidis/easyappointments:1.5.2 IMG_ESPOCRM_APP=mirror.gcr.io/espocrm/espocrm:9.2.5-apache IMG_EXCALIDRAW_SERVER=ghcr.io/homeserverhq/excalidraw-room:v0.1 IMG_EXCALIDRAW_STORAGE=ghcr.io/homeserverhq/excalidraw-storage-backend:v0.1 IMG_EXCALIDRAW_WEB=ghcr.io/homeserverhq/excalidraw:v0.1 IMG_EXCALIDRAW_AI=ghcr.io/homeserverhq/excalidraw-canvas:v0.1 IMG_EXCALIDRAW_MCP=ghcr.io/homeserverhq/excalidraw-mcp:v0.1 IMG_FILEBROWSER=mirror.gcr.io/filebrowser/filebrowser:v2.50.0 IMG_FILEDROP=filedrop/filedrop:1 IMG_FIREFLY_APP=mirror.gcr.io/fireflyiii/core:version-6.4.9 IMG_FIREFLY_IMPORTER=mirror.gcr.io/fireflyiii/data-importer:version-1.9.1 IMG_FIREFLY_FINTSIMPORTER=mirror.gcr.io/benkl/firefly-iii-fints-importer IMG_FRESHRSS=mirror.gcr.io/freshrss/freshrss:1.27.1 IMG_GHOST=mirror.gcr.io/ghost:6.9.3-alpine IMG_GITEA_APP=mirror.gcr.io/gitea/gitea:28.2.0 IMG_GITEA_MCP=ghcr.io/homeserverhq/gitea-mcp:v1.0.0 IMG_GITLAB_APP=mirror.gcr.io/gitlab/gitlab-ce:18.2.1-ce.0 IMG_GRAFANA=mirror.gcr.io/grafana/grafana-oss:13.0.2 IMG_LOKI=mirror.gcr.io/grafana/loki:3.7.7 IMG_ALLOY=mirror.gcr.io/grafana/alloy:v1.19.2 IMG_GRAMPSWEB=ghcr.io/gramps-project/grampsweb:25.7.3 IMG_GUACAMOLE_GUACD=mirror.gcr.io/guacamole/guacd:1.6.0 IMG_GUACAMOLE_WEB=mirror.gcr.io/guacamole/guacamole:1.6.0 IMG_HEIMDALL=linuxserver/heimdall:2.4.13 IMG_HOMARR_APP=ghcr.io/homarr-labs/homarr:v1.45.1 IMG_HOMEASSISTANT_APP=mirror.gcr.io/homeassistant/home-assistant:2026.7.2 IMG_HOMEASSISTANT_CONFIGURATOR=mirror.gcr.io/causticlab/hass-configurator-docker:0.6.0 IMG_HOMEASSISTANT_NODERED=mirror.gcr.io/nodered/node-red:5.0.1-24 IMG_HOMEASSISTANT_TASMOADMIN=ghcr.io/tasmoadmin/tasmoadmin:v5.0.0 IMG_HOMEASSISTANT_MCP=ghcr.io/homeassistant-ai/ha-mcp:7.12.1 IMG_HUGINN_APP=ghcr.io/huginn/huginn:e95438fe02e9fc5be85101029b2e4441d0822328 IMG_IMMICH_DB=ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 IMG_IMMICH_APP=ghcr.io/immich-app/immich-server:v3.0.2 IMG_IMMICH_ML=ghcr.io/immich-app/immich-machine-learning:v3.0.2 IMG_IMMICH_MCP=ghcr.io/homeserverhq/immich-mcp:v2 IMG_INFLUXDB=mirror.gcr.io/influxdb:2.7.12-alpine IMG_INVIDIOUS_WEB=quay.io/invidious/invidious:master IMG_INVIDIOUS_COMPANION=quay.io/invidious/invidious-companion:latest IMG_INVIDIOUS_SESSIONGEN=quay.io/invidious/youtube-trusted-session-generator IMG_INVOICENINJA_APP=mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37 IMG_INVOICESHELF_APP=mirror.gcr.io/invoiceshelf/invoiceshelf:2.4.2 IMG_INVOICESHELF_MCP=ghcr.io/homeserverhq/invoiceshelf-mcp:v2 IMG_ITTOOLS=ghcr.io/corentinth/it-tools:latest IMG_JELLYFIN=mirror.gcr.io/jellyfin/jellyfin:10.11.4 IMG_JITSI_WEB=jitsi/web:stable-10655 IMG_JITSI_PROSODY=jitsi/prosody:stable-10655 IMG_JITSI_JICOFO=jitsi/jicofo:stable-10655 IMG_JITSI_JVB=jitsi/jvb:stable-10655 IMG_JUPYTER=continuumio/anaconda3:2024.10-1 IMG_KANBOARD_APP=mirror.gcr.io/kanboard/kanboard:v1.2.48 IMG_KASM=mirror.gcr.io/linuxserver/kasm:1.18.0-ls108 IMG_KEILA_APP=mirror.gcr.io/pentacent/keila:0.30.2 IMG_KEILA_MCP=ghcr.io/homeserverhq/keila-mcp:v2 IMG_KILLBILL_DB=mirror.gcr.io/killbill/mariadb:0.24 IMG_KILLBILL_APP=mirror.gcr.io/killbill/killbill:0.24.15 IMG_KILLBILL_WEB=mirror.gcr.io/killbill/kaui:3.0.23 IMG_LINKWARDEN_APP=ghcr.io/linkwarden/linkwarden:v2.15.1 IMG_LINKWARDEN_MCP=ghcr.io/homeserverhq/linkwarden-mcp:v2 IMG_MAIL_RELAY_POSTFIX=ghcr.io/homeserverhq/mail-relay-postfix:v1 IMG_MAIL_RELAY_RSPAMD=ghcr.io/homeserverhq/mail-relay-rspamd:v1 IMG_MAIL_RELAY_CLAMAV=clamav/clamav:1.2.1 IMG_MAIL_RELAY_UNBOUND=mvance/unbound:1.19.0 IMG_MAILU_ADMIN=ghcr.io/mailu/admin:2024.06.65 IMG_MAILU_ANTISPAM=ghcr.io/mailu/rspamd:2024.06.65 IMG_MAILU_ANTIVIRUS=mirror.gcr.io/clamav/clamav:1.5.3 IMG_MAILU_FETCHMAIL=ghcr.io/mailu/fetchmail:2024.06.65 IMG_MAILU_FRONT=ghcr.io/mailu/nginx:2024.06.65 IMG_MAILU_IMAP=ghcr.io/mailu/dovecot:2024.06.65 IMG_MAILU_OLETOOLS=ghcr.io/mailu/oletools:2024.06.65 IMG_MAILU_SMTP=ghcr.io/mailu/postfix:2024.06.65 IMG_MAILU_TIKA=mirror.gcr.io/apache/tika:3.3.1.0-full IMG_MAILU_UNBOUND=ghcr.io/mailu/unbound:2024.06.65 IMG_MAILU_WEBDAV=ghcr.io/mailu/radicale:2024.06.65 IMG_MAILU_WEBMAIL=ghcr.io/mailu/webmail:2024.06.65 IMG_MASTODON_APP=mirror.gcr.io/tootsuite/mastodon:v4.5.2 IMG_MASTODON_STREAMING=mirror.gcr.io/tootsuite/mastodon-streaming:v4.5.2 IMG_MATRIX_ELEMENT=mirror.gcr.io/vectorim/element-web:v1.12.6 IMG_MATRIX_SYNAPSE=mirror.gcr.io/matrixdotorg/synapse:v1.143.0 IMG_MATOMO_APP=mirror.gcr.io/matomo:5.6.1-fpm-alpine IMG_MEALIE_APP=ghcr.io/mealie-recipes/mealie:v3.20.1 IMG_MEALIE_MCP=ghcr.io/homeserverhq/mealie-mcp:v2 IMG_MESHCENTRAL=ghcr.io/ylianst/meshcentral:1.1.53 IMG_METABASE=mirror.gcr.io/metabase/metabase:v0.63.2 IMG_MINTHCM_WEB=mirror.gcr.io/minthcm/minthcm:latest IMG_MINTHCM_ELASTICSEARCH=docker.elastic.co/elasticsearch/elasticsearch:7.9.3 IMG_N8N_APP=mirror.gcr.io/n8nio/n8n:1.122.5 IMG_NAVIDROME=mirror.gcr.io/deluan/navidrome:0.58.5 IMG_NETDATA=mirror.gcr.io/netdata/netdata:v2.8.2 IMG_NEXTCLOUD_APP=mirror.gcr.io/nextcloud:34.0.1-fpm-alpine IMG_NEXTCLOUD_HARP=ghcr.io/nextcloud/nextcloud-appapi-harp:v0.4.2 IMG_NEXTCLOUD_IMAGINARY=mirror.gcr.io/nextcloud/aio-imaginary:20260702_083546 IMG_NEXTCLOUD_TALKHPB=ghcr.io/nextcloud-releases/aio-talk:20260702_083546 IMG_NEXTCLOUD_TALKRECORD=ghcr.io/nextcloud-releases/aio-talk-recording:20260702_083546 IMG_NEXTCLOUD_MCP=ghcr.io/homeserverhq/nextcloud-mcp:v2 IMG_NTFY=mirror.gcr.io/binwiederhier/ntfy:v2.15.0 IMG_NODE_EXPORTER=mirror.gcr.io/prom/node-exporter:v1.12.0 IMG_ODOO_APP=mirror.gcr.io/odoo:19.0-20251121 IMG_OFELIA=mirror.gcr.io/mcuadros/ofelia:0.3.18 IMG_OMBI_APP=linuxserver/ombi:4.47.1 IMG_OPENLDAP_MANAGER=mirror.gcr.io/wheelybird/luminary:v2.2.1 IMG_OPENLDAP_PHP=osixia/phpldapadmin:stable IMG_OPENLDAP_SERVER=osixia/openldap:1.5.0 IMG_OPENPROJECT_APP=mirror.gcr.io/openproject/openproject:17.6.0-slim IMG_OPENPROJECT_MCP=ghcr.io/homeserverhq/openproject-mcp:v2 IMG_PAPERLESS_APP=ghcr.io/homeserverhq/paperless-ngx:v3.0.5 IMG_PAPERLESS_GOTENBERG=mirror.gcr.io/gotenberg/gotenberg:8.34.0 IMG_PAPERLESS_TIKA=mirror.gcr.io/apache/tika:3.3.1.0-full IMG_PAPERLESS_AI=ghcr.io/homeserverhq/zettelrobbe:v2026.08.04 IMG_PAPERLESS_GPT=ghcr.io/icereed/paperless-gpt:v0.27.0 IMG_PAPERLESS_MCP=ghcr.io/homeserverhq/paperless-mcp:v2 IMG_PASTEFY=mirror.gcr.io/interaapps/pastefy:7.1.5 IMG_PEERTUBE_APP=mirror.gcr.io/chocobozzz/peertube:v7.3.0-bookworm IMG_PENPOT_BACKEND=mirror.gcr.io/penpotapp/backend:2.11.1 IMG_PENPOT_FRONTEND=mirror.gcr.io/penpotapp/frontend:2.11.1 IMG_PENPOT_EXPORTER=mirror.gcr.io/penpotapp/exporter:2.11.1 IMG_PHOTOPRISM_APP=mirror.gcr.io/photoprism/photoprism:251130 IMG_PIPED_FRONTEND=1337kavin/piped-frontend:latest IMG_PIPED_PROXY=1337kavin/piped-proxy:latest IMG_PIPED_API=1337kavin/piped:latest IMG_PIPED_CRON=barrypiccinni/psql-curl IMG_PIXELFED_APP=ghcr.io/jippi/docker-pixelfed:v0.12.5-docker1-apache-8.4-bookworm IMG_PIXELFED_MOD_APP=hshq/pixelfed:v1 IMG_PORTAINER=mirror.gcr.io/portainer/portainer-ce:2.39.5-alpine IMG_PROMETHEUS=mirror.gcr.io/prom/prometheus:v3.13.1 IMG_QBITTORRENT=mirror.gcr.io/linuxserver/qbittorrent:5.1.4 IMG_RABBITMQ=mirror.gcr.io/rabbitmq:4.1.4 IMG_RALLLY_APP=mirror.gcr.io/lukevella/rallly:4.5.7 IMG_REMOTELY=immybot/remotely:1037 IMG_REVOLT_API=ghcr.io/revoltchat/server:20250807-1 IMG_REVOLT_EVENTS=ghcr.io/revoltchat/bonfire:20250807-1 IMG_REVOLT_WEB=ghcr.io/revoltchat/client:master IMG_REVOLT_AUTUMN=ghcr.io/revoltchat/autumn:20250807-1 IMG_REVOLT_JANUARY=ghcr.io/revoltchat/january:20250807-1 IMG_REVOLT_CROND=ghcr.io/revoltchat/crond:20250807-1 IMG_REVOLT_PUSHD=ghcr.io/revoltchat/pushd:20250807-1 IMG_SABNZBD=mirror.gcr.io/linuxserver/sabnzbd:4.5.5 IMG_SEARXNG_APP=ghcr.io/searxng/searxng:2026.10.27-cb74e7f81 IMG_SEARXNG_MCP=ghcr.io/homeserverhq/mcp-searxng-enhanced:v1.0 IMG_SERVARR_SONARR=mirror.gcr.io/linuxserver/sonarr:4.0.16 IMG_SERVARR_RADARR=mirror.gcr.io/linuxserver/radarr:6.1.0-nightly IMG_SERVARR_LIDARR=mirror.gcr.io/linuxserver/lidarr:3.1.0 IMG_SERVARR_READARR=mirror.gcr.io/linuxserver/readarr:0.4.19-nightly IMG_SERVARR_BAZARR=mirror.gcr.io/linuxserver/bazarr:1.5.3 IMG_SERVARR_MYLAR3=mirror.gcr.io/linuxserver/mylar3:0.8.3 IMG_SERVARR_PROWLARR=mirror.gcr.io/linuxserver/prowlarr:2.3.1-nightly IMG_SHLINK_APP=mirror.gcr.io/shlinkio/shlink:4.6.0 IMG_SHLINK_WEB=mirror.gcr.io/shlinkio/shlink-web-client:4.6.1 IMG_SNIPPETBOX=pawelmalak/snippet-box:latest IMG_SPEEDTEST_TRACKER_APP=mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0 IMG_SQLPAD=mirror.gcr.io/sqlpad/sqlpad:7.5.5 IMG_STANDARDNOTES_SERVER=standardnotes/server:0d82819cba9694bc9fb5a3fa53e2dbeda05d1242 IMG_STANDARDNOTES_LOCALSTACK=mirror.gcr.io/localstack/localstack:4.11.1 IMG_STANDARDNOTES_WEB=mirror.gcr.io/standardnotes/web:3190a278314fe788f9136528971c0a9cd0dffbf4 IMG_STIRLINGPDF=mirror.gcr.io/stirlingtools/stirling-pdf:1.2.0 IMG_SYNCTHING=mirror.gcr.io/syncthing/syncthing:2.1.2 IMG_TOMCAT=tomcat:11 IMG_TWENTY_APP=mirror.gcr.io/twentycrm/twenty:v2.21.0 IMG_TWENTY_MCP=ghcr.io/homeserverhq/twenty-mcp:v2 IMG_UPTIMEKUMA=mirror.gcr.io/louislam/uptime-kuma:2.4.0 IMG_VAULTWARDEN_APP=mirror.gcr.io/vaultwarden/server:1.37.1-alpine IMG_VAULTWARDEN_LDAP=mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1 IMG_WALLABAG=mirror.gcr.io/wallabag/wallabag:2.6.14 IMG_WAZUH_MANAGER=wazuh/wazuh-manager:4.14.7 IMG_WAZUH_INDEXER=wazuh/wazuh-indexer:4.14.7 IMG_WAZUH_DASHBOARD=wazuh/wazuh-dashboard:4.14.7 IMG_WEKAN_APP=ghcr.io/wekan/wekan:v8.17 IMG_WGPORTAL=wgportal/wg-portal:1.0.19 IMG_WIKIJS_APP=mirror.gcr.io/requarks/wiki:2.5.314 IMG_WIKIJS_MCP=ghcr.io/homeserverhq/wikijs-mcp:v2 IMG_WIREGUARD=mirror.gcr.io/linuxserver/wireguard:1.0.20250521-r0-ls93 IMG_WORDPRESS_APP=mirror.gcr.io/wordpress:php8.5-apache IMG_WORDPRESS_CLI=mirror.gcr.io/wordpress:cli-php8.5 IMG_WORDPRESS_MCP=ghcr.io/homeserverhq/wordpress-mcp:v2 IMG_YAMTRACK_APP=ghcr.io/fuzzygrim/yamtrack:0.24.8 IMG_ZAMMAD=ghcr.io/zammad/zammad:6.5.2-49 IMG_ZULIP_APP=mirror.gcr.io/zulip/docker-zulip:11.4-0 IMG_ZULIP_DB=mirror.gcr.io/zulip/zulip-postgresql:14 IMG_BESZEL_APP=ghcr.io/henrygd/beszel/beszel:0.19.0 IMG_BESZEL_AGENT=ghcr.io/henrygd/beszel/beszel-agent:0.19.0 IMG_TAIGA_BACK=mirror.gcr.io/taigaio/taiga-back:6.9.0 IMG_TAIGA_BACK=mirror.gcr.io/taigaio/taiga-back:6.9.0 IMG_TAIGA_FRONT=mirror.gcr.io/taigaio/taiga-front:6.9.0 IMG_TAIGA_EVENTS=mirror.gcr.io/taigaio/taiga-events:6.9.0 IMG_TAIGA_PROTECTED=mirror.gcr.io/taigaio/taiga-protected:6.9.0 IMG_OPENSIGN_SERVER=mirror.gcr.io/opensign/opensignserver:main IMG_OPENSIGN_CLIENT=mirror.gcr.io/opensign/opensign:main IMG_DOCUSEAL_APP=mirror.gcr.io/docuseal/docuseal:2.2.4 IMG_CONTROLR_APP=mirror.gcr.io/translucency/controlr:0.16.0.0 IMG_CONTROLR_ASPIRE=mcr.microsoft.com/dotnet/aspire-dashboard:9.2 IMG_AKAUNTING_APP=mirror.gcr.io/akaunting/akaunting:3.1.19 IMG_AXELOR_APP=mirror.gcr.io/axelor/aos-ce:8.4 IMG_AXELOR_GOOVEE=mirror.gcr.io/axelor/goovee-ce:v1.1.0 IMG_CONVERTX_APP=ghcr.io/c4illin/convertx:v0.16.0 IMG_KOPIA_APP=mirror.gcr.io/kopia/kopia:0.22.3 IMG_LOCALAI_SERVER=mirror.gcr.io/localai/localai:v3.8.0-aio-cpu IMG_LOCALAI_RECALL=quay.io/mudler/localrecall:v0.4.1 IMG_LOCALAI_LRHEALTH=mirror.gcr.io/busybox:1.37.0 IMG_LOCALAI_SSHBOX=hshq/sshbox:v1 IMG_LOCALAI_DIND=mirror.gcr.io/docker:29.1.3-dind IMG_LOCALAI_AGI=quay.io/mudler/localagi:v2.6.0 IMG_COMFYUI_APP=mirror.gcr.io/corundex/comfyui-rocm:comfyui_0.3.43__rocm6.4.1_ubuntu24.04_py3.12_pytorch_2.6.0 IMG_LANGFLOW_APP=mirror.gcr.io/langflowai/langflow:1.8.1 IMG_ANYTHINGLLM_APP=mirror.gcr.io/mintplexlabs/anythingllm:1.9.1 IMG_PERPLEXICA_APP=mirror.gcr.io/itzcrazykns1337/perplexica:slim-v1.12.0 IMG_FIRECRAWL_DB=hshq/firecrawl-db:v1 IMG_FIRECRAWL_PLAYWRIGHT=ghcr.io/firecrawl/playwright-service:latest IMG_FIRECRAWL_API=ghcr.io/firecrawl/firecrawl:latest IMG_LIBRECHAT_APP=ghcr.io/danny-avila/librechat:v0.8.1 IMG_LIBRECHAT_RAGAPI=ghcr.io/danny-avila/librechat-rag-api-dev-lite:v0.7.0 IMG_CRAWL4AI_APP=ghcr.io/homeserverhq/crawl4ai:v0.9.5 IMG_CRAWL4AI_PROXY=ghcr.io/homeserverhq/crawl4ai-proxy:v0.1 IMG_OLLAMA_SERVER=mirror.gcr.io/ollama/ollama:0.20.5 IMG_OPENWEBUI_APP=ghcr.io/homeserverhq/open-webui:v0.13.0 IMG_OPENWEBUI_OPENTERMINAL=ghcr.io/open-webui/open-terminal:0.17.4 IMG_OPENWEBUI_MCPO=ghcr.io/open-webui/mcpo:main IMG_OPENWEBUI_PIPELINES=ghcr.io/open-webui/pipelines:main IMG_OPENWEBUI_MCP=hshq/openwebui-mcp:v1 IMG_KHOJ_SANDBOX=ghcr.io/khoj-ai/terrarium:latest IMG_KHOJ_COMPUTER=ghcr.io/khoj-ai/khoj-computer:1.42.0 IMG_KHOJ_SERVER=ghcr.io/khoj-ai/khoj:1.42.10 IMG_LOBECHAT_APP=mirror.gcr.io/lobehub/lobe-chat-database:1.143.3 IMG_INVOKEAI_APP=ghcr.io/invoke-ai/invokeai:6.12-cpu IMG_RAGFLOW_INFINITY=mirror.gcr.io/infiniflow/infinity:v0.7.5-x64-v3 IMG_RAGFLOW_APP=mirror.gcr.io/infiniflow/ragflow:v1.0.0 IMG_RAGFLOW_SANDBOX=mirror.gcr.io/infiniflow/sandbox-executor-manager:latest IMG_RAGFLOW_SB_NODEJS=mirror.gcr.io/infiniflow/sandbox-base-nodejs:latest IMG_RAGFLOW_SB_PYTHON=mirror.gcr.io/infiniflow/sandbox-base-python:latest IMG_TABBYML_APP=mirror.gcr.io/tabbyml/tabby:0.31.2 IMG_DEEPWIKIOPEN_APP=ghcr.io/asyncfuncai/deepwiki-open:sha-d48f5bc IMG_DOCLING_APP=hshq/docling-serve:v1.38.1.1 IMG_DIFY_SSRF=mirror.gcr.io/ubuntu/squid:latest IMG_DIFY_API=mirror.gcr.io/langgenius/dify-api:1.11.2 IMG_DIFY_PLUGIND=mirror.gcr.io/langgenius/dify-plugin-daemon:0.5.2-local IMG_DIFY_SANDBOX=mirror.gcr.io/langgenius/dify-sandbox:0.2.12 IMG_DIFY_WEB=mirror.gcr.io/langgenius/dify-web:1.11.2 IMG_MINDSDB_APP=mirror.gcr.io/mindsdb/mindsdb:v25.13.1-cloud IMG_WATERCRAWL_APP=mirror.gcr.io/watercrawl/watercrawl:v0.12.1 IMG_WATERCRAWL_PLAYWRIGHT=mirror.gcr.io/watercrawl/playwright:1.2 IMG_WATERCRAWL_FRONTEND=mirror.gcr.io/watercrawl/frontend:v0.12.1 IMG_WATERCRAWL_MCP=mirror.gcr.io/watercrawl/mcp:v1.3.0 IMG_FLOWISE_APP=mirror.gcr.io/flowiseai/flowise:3.1.4 IMG_FLOWISE_WORKER=mirror.gcr.io/flowiseai/flowise-worker:3.1.4 IMG_SURFSENSE_APP=ghcr.io/modsetter/surfsense:v1.0.92 IMG_NOCODB_APP=mirror.gcr.io/nocodb/nocodb:0.265.1 IMG_ENTE_SERVER=hshq/ente-server:v1 IMG_ENTE_WEB=ghcr.io/ente-io/web:460ee1671b08b119b894f0ddd71b4c906fb29647 IMG_MORPHIC_APP=ghcr.io/miurla/morphic:6443b20c1205adf233c98b67beb34a6117e1cd7a IMG_OPENNOTEBOOK_DB=mirror.gcr.io/surrealdb/surrealdb:v2.4-dev IMG_OPENNOTEBOOK_APP=ghcr.io/lfnovo/open-notebook:1.14.0 IMG_OPENNOTEBOOK_MCP=ghcr.io/homeserverhq/opennotebook-mcp:v2 IMG_APPSMITH_APP=mirror.gcr.io/appsmith/appsmith-ce:v1.94 IMG_TRILIUM=mirror.gcr.io/triliumnext/trilium:v0.101.3 IMG_DOCSGPT_FRONTEND=hshq/docsgpt-frontend:v1 IMG_DOCSGPT_BACKEND=hshq/docsgpt-backend:v1 IMG_MEMOS_APP=mirror.gcr.io/neosmemo/memos:0.25.3 IMG_SILLYTAVERN=ghcr.io/sillytavern/sillytavern:1.15.0 IMG_LEMONADE_SERVER=ghcr.io/lemonade-sdk/lemonade-server:v9.2.0 IMG_SPEAKR_APP=ghcr.io/homeserverhq/speakr:v0.10.16 IMG_SPEAKR_WHISPERX=mirror.gcr.io/onerahmet/openai-whisper-asr-webservice:v1.9.1 IMG_INSANELYFASTWHISPER_APP=hshq/insanelyfastwhisper:v3 IMG_IVBOX_APP=ghcr.io/homeserverhq/ivbox:v0.1.0 IMG_MONICA_APP=hshq/monica-app:v1 IMG_MONICA_WEB=hshq/monica-web:v1 IMG_AFFINE_APP=ghcr.io/toeverything/affine:0.25.7 IMG_JOPLIN_APP=mirror.gcr.io/joplin/server:3.5.2 IMG_JOPLIN_TRANSCRIBE_APP=mirror.gcr.io/joplin/transcribe:3.4.8 IMG_SUPERSET_APP=mirror.gcr.io/apache/superset:6.0.0 IMG_SUPERSET_WORKER=mirror.gcr.io/apache/superset:dev IMG_SUPERSET_WEBSOCKET=mirror.gcr.io/apache/superset:6.0.0-websocket IMG_KOKORO_TTS=ghcr.io/remsky/kokoro-fastapi-cpu:v0.9.0 IMG_KOKORO_WEB=ghcr.io/remsky/kokoro-fastapi-ui:v0.1.0 IMG_CHATTERBOX_APP=hshq/chatterbox:v1 IMG_LITELLM_APP=ghcr.io/berriai/litellm:v1.108.2 IMG_LANGFUSE_WORKER=langfuse/langfuse-worker:3.213.0 IMG_LANGFUSE_APP=mirror.gcr.io/langfuse/langfuse:3.213.0 IMG_SKYVERN_API=mirror.gcr.io/skyvern/skyvern:v1.0.13 IMG_SKYVERN_WEB=mirror.gcr.io/skyvern/skyvern-ui:v1.0.13 IMG_WGER=mirror.gcr.io/wger/server:2.7.0 IMG_WORKOUTCOOL=ghcr.io/snouzy/workout-cool:1.3.2 IMG_OPENRAG_DB=mirror.gcr.io/langflowai/openrag-opensearch:0.4.0 IMG_OPENRAG_BACKEND=mirror.gcr.io/langflowai/openrag-backend:0.4.0 IMG_OPENRAG_FRONTEND=mirror.gcr.io/langflowai/openrag-frontend:0.4.0 IMG_OPENRAG_LANGFLOW=mirror.gcr.io/langflowai/openrag-langflow:0.4.0 IMG_VOICEBOX_APP=hshq/voicebox:v1 IMG_OPENCODE_APP=ghcr.io/homeserverhq/opencode-docker:v3.1.0 IMG_OPENCODE_PLAYWRIGHT=mcr.microsoft.com/playwright/mcp:v0.0.70 IMG_OPENSKILLS_APP=hshq/openskills:v1 IMG_EMAILCLASSIFIERAI_APP=ghcr.io/homeserverhq/emailclassifierai:v6 IMG_HERMES_APP=hshq/hermes-agent:v1 IMG_HERMES_TERMINAL=hshq/hermes-terminal:v1 IMG_HERMES_CAMOFOX=ghcr.io/jo-inc/camofox-browser:1.11.2 IMG_HERMES_WEBUI=ghcr.io/nesquena/hermes-webui:0.52.302 IMG_AUTOKB_APP=ghcr.io/homeserverhq/autokb-app:v6 IMG_AUTOKB_MCP=ghcr.io/homeserverhq/autokb-mcp:v6 IMG_SUITECRM_APP=ghcr.io/homeserverhq/suitecrm-core:v8.10.1 IMG_SUITECRM_MCP=ghcr.io/homeserverhq/suitecrm-mcp:v2 IMG_HEDGEDOC_FRONTEND=ghcr.io/homeserverhq/hedgedoc-frontend:v2.0.1-alpha IMG_HEDGEDOC_BACKEND=ghcr.io/homeserverhq/hedgedoc-backend:v2.0.1-alpha IMG_HEDGEDOC_MCP=ghcr.io/homeserverhq/hedgedoc-mcp:v2 IMG_PRESENTON_APP=ghcr.io/presenton/presenton:v0.9.12-beta IMG_PRESENTON_MCP=ghcr.io/homeserverhq/presenton-mcp:v2 IMG_BASICMEMORY_APP=ghcr.io/basicmachines-co/basic-memory:0.22.1 IMG_COGNEE_APP=ghcr.io/homeserverhq/cognee-app:v1.4.2 IMG_COGNEE_FRONTEND=ghcr.io/homeserverhq/cognee-frontend:v1.4.2 IMG_COGNEE_MCP=ghcr.io/homeserverhq/cognee-mcp:v1.4.2 IMG_LIGHTRAG_APP=ghcr.io/hkuds/lightrag:v1.5.6 IMG_OPENSERP_APP=mirror.gcr.io/karust/openserp:0.8 IMG_SURICATA_APP=mirror.gcr.io/jasonish/suricata:8.0.7 IMG_SURICATA_WEB=mirror.gcr.io/jasonish/evebox:0.29.0 IMG_CROWDSEC_APP=ghcr.io/crowdsecurity/crowdsec:v1.8.1 IMG_CROWDSEC_WEB=ghcr.io/theduffman85/crowdsec-web-ui:2026.10.3 IMG_FASTCRW_APP=ghcr.io/fastcrw/crw:0.41.0 IMG_LIGHTPANDA=mirror.gcr.io/lightpanda/browser:1.0.2 IMG_FASTCRW_PROXY=ghcr.io/homeserverhq/webloader-proxy:v0.1 IMG_HEADLESS_BROWSER=ghcr.io/homeserverhq/headless-browser:v0.1 IMG_WHITEBOARDRT_WEB=ghcr.io/homeserverhq/whiteboardrt:v0.2 IMG_WHITEBOARDRT_AGENT=ghcr.io/homeserverhq/whiteboardrt-agent-canvas:v0.2 IMG_WHITEBOARDRT_MCP=ghcr.io/homeserverhq/whiteboardrt-agent-mcp:v0.2 IMG_WHITEBOARDRT_STORAGE=ghcr.io/homeserverhq/whiteboardrt-storage-backend:v0.2 IMG_WHITEBOARDRT_SERVER=ghcr.io/homeserverhq/whiteboardrt-server:v0.2 IMG_MDCOLLAB_BACKEND=ghcr.io/homeserverhq/mdcollab-backend:v2.1 IMG_MDCOLLAB_FRONTEND=ghcr.io/homeserverhq/mdcollab-frontend:v2.1 IMG_MDCOLLAB_MCP=ghcr.io/homeserverhq/mdcollab-mcp:v3.0 IMG_HALOSPEAK_APP=hshq/halospeak:v3.0 #ADD_NEW_IMAGES_HERE } function getScriptStackVersion() { stack_name=$1 case "$stack_name" in portainer) echo "v6" ;; adguard) echo "v9" ;; sysutils) echo "v11" ;; openldap) echo "v2" ;; mailu) echo "v9" ;; wazuh) echo "v8" ;; collabora) echo "v9" ;; nextcloud) echo "v14" ;; jitsi) echo "v9" ;; matrix) echo "v9" ;; wikijs) echo "v6" ;; duplicati) echo "v8" ;; mastodon) echo "v10" ;; dozzle) echo "v7" ;; searxng) echo "v12" ;; jellyfin) echo "v7" ;; filebrowser) echo "v8" ;; photoprism) echo "v4" ;; guacamole) echo "v4" ;; authelia) echo "v8" ;; wordpress) echo "v6" ;; ghost) echo "v9" ;; peertube) echo "v8" ;; homeassistant) echo "v11" ;; gitlab) echo "v8" ;; vaultwarden) echo "v11" ;; discourse) echo "v7" ;; syncthing) echo "v10" ;; codeserver) echo "v8" ;; shlink) echo "v9" ;; firefly) echo "v10" ;; excalidraw) echo "v5" ;; drawio) echo "v9" ;; invidious) echo "v2" ;; ittools) echo "v1" ;; gitea) echo "v10" ;; mealie) echo "v11" ;; kasm) echo "v8" ;; ntfy) echo "v6" ;; remotely) echo "v4" ;; calibre) echo "v8" ;; netdata) echo "v7" ;; linkwarden) echo "v11" ;; stirlingpdf) echo "v7" ;; bar-assistant) echo "v11" ;; freshrss) echo "v6" ;; keila) echo "v6" ;; wallabag) echo "v5" ;; jupyter) echo "v4" ;; paperless) echo "v12" ;; speedtest-tracker-local) echo "v7" ;; speedtest-tracker-vpn) echo "v7" ;; heimdall) echo "v1" ;; changedetection) echo "v7" ;; huginn) echo "v4" ;; coturn) echo "v4" ;; filedrop) echo "v1" ;; piped) echo "v2" ;; grampsweb) echo "v4" ;; penpot) echo "v4" ;; espocrm) echo "v4" ;; immich) echo "v9" ;; homarr) echo "v5" ;; matomo) echo "v3" ;; pastefy) echo "v3" ;; snippetbox) echo "v1" ;; aistack) echo "v1" ;; pixelfed) echo "v2" ;; yamtrack) echo "v3" ;; servarr) echo "v2" ;; sabnzbd) echo "v2" ;; qbittorrent) echo "v2" ;; ombi) echo "v1" ;; meshcentral) echo "v2" ;; navidrome) echo "v2" ;; adminer) echo "v2" ;; budibase) echo "v3" ;; audiobookshelf) echo "v2" ;; standardnotes) echo "v2" ;; metabase) echo "v4" ;; kanboard) echo "v2" ;; wekan) echo "v2" ;; revolt) echo "v3" ;; frappe-hr) echo "v1" ;; minthcm) echo "v1" ;; cloudbeaver) echo "v2" ;; twenty) echo "v6" ;; odoo) echo "v2" ;; calcom) echo "v2" ;; rallly) echo "v4" ;; easyappointments) echo "v1" ;; openproject) echo "v6" ;; zammad) echo "v4" ;; zulip) echo "v2" ;; killbill) echo "v1" ;; invoiceshelf) echo "v1" ;; invoiceninja) echo "v4" ;; dolibarr) echo "v6" ;; n8n) echo "v2" ;; automatisch) echo "v1" ;; activepieces) echo "v3" ;; dbgate) echo "v2" ;; sqlpad) echo "v8" ;; ofelia) echo "v6" ;; caddy-*) echo "v8" ;; clientdns-*) echo "v4" ;; uptimekuma) echo "v7" ;; mail-relay) echo "v2" ;; wgportal) echo "v2" ;; beszel) echo "v3" ;; taiga) echo "v1" ;; opensign) echo "v1" ;; docuseal) echo "v2" ;; controlr) echo "v2" ;; akaunting) echo "v1" ;; axelor) echo "v1" ;; convertx) echo "v2" ;; kopia) echo "v2" ;; localai) echo "v1" ;; comfyui) echo "v1" ;; langflow) echo "v2" ;; anythingllm) echo "v1" ;; perplexica) echo "v1" ;; firecrawl) echo "v1" ;; librechat) echo "v1" ;; crawl4ai) echo "v3" ;; ollama) echo "v2" ;; openwebui) echo "v7" ;; khoj) echo "v1" ;; lobechat) echo "v2" ;; invokeai) echo "v1" ;; ragflow) echo "v6" ;; tabbyml) echo "v1" ;; deepwikiopen) echo "v1" ;; docling) echo "v4" ;; dify) echo "v2" ;; mindsdb) echo "v1" ;; watercrawl) echo "v2" ;; flowise) echo "v2" ;; surfsense) echo "v1" ;; nocodb) echo "v2" ;; ente) echo "v2" ;; morphic) echo "v1" ;; opennotebook) echo "v3" ;; appsmith) echo "v1" ;; trilium) echo "v1" ;; docsgpt) echo "v1" ;; memos) echo "v1" ;; sillytavern) echo "v1" ;; lemonade) echo "v1" ;; speakr) echo "v5" ;; insanelyfastwhisper) echo "v3" ;; ivbox) echo "v2" ;; monica) echo "v1" ;; affine) echo "v1" ;; joplin) echo "v1" ;; superset) echo "v1" ;; kokoro) echo "v2" ;; chatterbox) echo "v1" ;; litellm) echo "v5" ;; langfuse) echo "v3" ;; skyvern) echo "v1" ;; wger) echo "v2" ;; workoutcool) echo "v1" ;; openrag) echo "v1" ;; voicebox) echo "v1" ;; opencode) echo "v2" ;; openskills) echo "v1" ;; emailclassifierai) echo "v3" ;; hermes-agent) echo "v1" ;; autokb) echo "v1" ;; suitecrm) echo "v2" ;; hedgedoc) echo "v1" ;; presenton) echo "v3" ;; basicmemory) echo "v1" ;; cognee) echo "v1" ;; lightrag) echo "v1" ;; openserp) echo "v1" ;; suricata) echo "v1" ;; crowdsec) echo "v1" ;; fastcrw) echo "v1" ;; whiteboardrt) echo "v1" ;; mdcollab) echo "v1" ;; halospeak) echo "v1" ;; #ADD_NEW_SCRIPT_STACK_VERSION_HERE esac } function pullDockerImages() { buildOrPullImage $IMG_PORTAINER buildOrPullImage $IMG_REDIS buildOrPullImage $IMG_POSTGRES buildOrPullImage $IMG_MYSQL buildOrPullImage $IMG_NGINX buildOrPullImage $IMG_ALPINE buildOrPullImage $IMG_ADGUARD buildOrPullImage $IMG_GRAFANA buildOrPullImage $IMG_PROMETHEUS buildOrPullImage $IMG_NODE_EXPORTER buildOrPullImage $IMG_LOKI buildOrPullImage $IMG_ALLOY buildOrPullImage $IMG_OPENLDAP_SERVER buildOrPullImage $IMG_OPENLDAP_PHP buildOrPullImage $IMG_OPENLDAP_MANAGER buildOrPullImage $IMG_MAILU_ADMIN buildOrPullImage $IMG_MAILU_ANTISPAM buildOrPullImage $IMG_MAILU_ANTIVIRUS buildOrPullImage $IMG_MAILU_FETCHMAIL buildOrPullImage $IMG_MAILU_FRONT buildOrPullImage $IMG_MAILU_IMAP buildOrPullImage $IMG_MAILU_OLETOOLS buildOrPullImage $IMG_MAILU_SMTP buildOrPullImage $IMG_MAILU_TIKA buildOrPullImage $IMG_MAILU_UNBOUND buildOrPullImage $IMG_MAILU_WEBDAV buildOrPullImage $IMG_MAILU_WEBMAIL buildOrPullImage $IMG_WAZUH_MANAGER buildOrPullImage $IMG_WAZUH_INDEXER buildOrPullImage $IMG_WAZUH_DASHBOARD buildOrPullImage $IMG_COLLABORA buildOrPullImage $IMG_NETDATA buildOrPullImage $IMG_NEXTCLOUD_APP buildOrPullImage $IMG_NEXTCLOUD_IMAGINARY buildOrPullImage $IMG_NEXTCLOUD_TALKHPB buildOrPullImage $IMG_NEXTCLOUD_TALKRECORD buildOrPullImage $IMG_NEXTCLOUD_MCP buildOrPullImage $IMG_JITSI_WEB buildOrPullImage $IMG_JITSI_PROSODY buildOrPullImage $IMG_JITSI_JICOFO buildOrPullImage $IMG_JITSI_JVB buildOrPullImage $IMG_MATRIX_SYNAPSE buildOrPullImage $IMG_MATRIX_ELEMENT buildOrPullImage $IMG_WIKIJS_APP buildOrPullImage $IMG_WIKIJS_MCP buildOrPullImage $IMG_DUPLICATI buildOrPullImage $IMG_MASTODON_APP buildOrPullImage $IMG_DOZZLE buildOrPullImage $IMG_SEARXNG_APP buildOrPullImage $IMG_SEARXNG_MCP buildOrPullImage $IMG_JELLYFIN buildOrPullImage $IMG_FILEBROWSER buildOrPullImage $IMG_PHOTOPRISM_APP buildOrPullImage $IMG_GUACAMOLE_GUACD buildOrPullImage $IMG_GUACAMOLE_WEB buildOrPullImage $IMG_GITLAB_APP buildOrPullImage $IMG_HOMEASSISTANT_APP buildOrPullImage $IMG_HOMEASSISTANT_CONFIGURATOR buildOrPullImage $IMG_HOMEASSISTANT_NODERED buildOrPullImage $IMG_HOMEASSISTANT_TASMOADMIN buildOrPullImage $IMG_HOMEASSISTANT_MCP buildOrPullImage $IMG_INFLUXDB buildOrPullImage $IMG_ITTOOLS buildOrPullImage $IMG_DISCOURSE buildOrPullImage $IMG_SQLPAD buildOrPullImage $IMG_SYNCTHING buildOrPullImage $IMG_VAULTWARDEN_APP buildOrPullImage $IMG_VAULTWARDEN_LDAP buildOrPullImage $IMG_WORDPRESS_APP buildOrPullImage $IMG_WORDPRESS_CLI buildOrPullImage $IMG_WORDPRESS_MCP buildOrPullImage $IMG_GHOST buildOrPullImage $IMG_PEERTUBE_APP buildOrPullImage $IMG_AUTHELIA buildOrPullImage $IMG_HEIMDALL buildOrPullImage $IMG_CADDY buildOrPullImage $IMG_UPTIMEKUMA buildOrPullImage $IMG_OFELIA buildOrPullImage $IMG_CODESERVER buildOrPullImage $IMG_SHLINK_APP buildOrPullImage $IMG_SHLINK_WEB buildOrPullImage $IMG_FIREFLY_APP buildOrPullImage $IMG_FIREFLY_IMPORTER buildOrPullImage $IMG_FIREFLY_FINTSIMPORTER buildOrPullImage $IMG_EXCALIDRAW_WEB buildOrPullImage $IMG_EXCALIDRAW_SERVER buildOrPullImage $IMG_EXCALIDRAW_STORAGE buildOrPullImage $IMG_EXCALIDRAW_AI buildOrPullImage $IMG_EXCALIDRAW_MCP buildOrPullImage $IMG_INVIDIOUS_WEB buildOrPullImage $IMG_INVIDIOUS_COMPANION buildOrPullImage $IMG_GITEA_APP buildOrPullImage $IMG_MEALIE_APP buildOrPullImage $IMG_MEALIE_MCP buildOrPullImage $IMG_KASM buildOrPullImage $IMG_DNSMASQ buildOrPullImage $IMG_WIREGUARD buildOrPullImage $IMG_CALIBRE_SERVER buildOrPullImage $IMG_CALIBRE_WEB buildOrPullImage $IMG_LINKWARDEN_APP buildOrPullImage $IMG_LINKWARDEN_MCP buildOrPullImage $IMG_STIRLINGPDF buildOrPullImage $IMG_BARASSISTANT_APP buildOrPullImage $IMG_BARASSISTANT_MCP buildOrPullImage $IMG_MEILISEARCH buildOrPullImage $IMG_BARASSISTANT_SALTRIM buildOrPullImage $IMG_FRESHRSS buildOrPullImage $IMG_KEILA_APP buildOrPullImage $IMG_KEILA_MCP buildOrPullImage $IMG_WALLABAG buildOrPullImage $IMG_JUPYTER buildOrPullImage $IMG_PAPERLESS_APP buildOrPullImage $IMG_PAPERLESS_GOTENBERG buildOrPullImage $IMG_PAPERLESS_TIKA buildOrPullImage $IMG_PAPERLESS_AI buildOrPullImage $IMG_PAPERLESS_GPT buildOrPullImage $IMG_PAPERLESS_MCP buildOrPullImage $IMG_SPEEDTEST_TRACKER_APP buildOrPullImage $IMG_CHANGEDETECTION_APP buildOrPullImage $IMG_CHANGEDETECTION_PLAYWRIGHT_CHROME buildOrPullImage $IMG_HUGINN_APP buildOrPullImage $IMG_COTURN buildOrPullImage $IMG_FILEDROP buildOrPullImage $IMG_PIPED_FRONTEND buildOrPullImage $IMG_PIPED_PROXY buildOrPullImage $IMG_PIPED_API buildOrPullImage $IMG_PIPED_CRON buildOrPullImage $IMG_GRAMPSWEB buildOrPullImage $IMG_PENPOT_BACKEND buildOrPullImage $IMG_PENPOT_FRONTEND buildOrPullImage $IMG_PENPOT_EXPORTER buildOrPullImage $IMG_ESPOCRM_APP buildOrPullImage $IMG_IMMICH_DB buildOrPullImage $IMG_IMMICH_APP buildOrPullImage $IMG_IMMICH_ML buildOrPullImage $IMG_IMMICH_MCP buildOrPullImage $IMG_HOMARR_APP buildOrPullImage $IMG_MATOMO_APP buildOrPullImage $IMG_PASTEFY buildOrPullImage $IMG_SNIPPETBOX buildOrPullImage $IMG_AISTACK_MINDSDB_APP buildOrPullImage $IMG_AISTACK_OPENTELEMETRY buildOrPullImage $IMG_AISTACK_LANGFUSE buildOrPullImage $IMG_AISTACK_OLLAMA_SERVER buildOrPullImage $IMG_AISTACK_OPENWEBUI buildOrPullImage $IMG_PIXELFED_APP buildOrPullImage $IMG_YAMTRACK_APP buildOrPullImage $IMG_SERVARR_SONARR buildOrPullImage $IMG_SERVARR_RADARR buildOrPullImage $IMG_SERVARR_LIDARR buildOrPullImage $IMG_SERVARR_READARR buildOrPullImage $IMG_SERVARR_BAZARR buildOrPullImage $IMG_SERVARR_MYLAR3 buildOrPullImage $IMG_SERVARR_PROWLARR buildOrPullImage $IMG_SABNZBD buildOrPullImage $IMG_QBITTORRENT buildOrPullImage $IMG_OMBI_APP buildOrPullImage $IMG_MESHCENTRAL buildOrPullImage $IMG_NAVIDROME buildOrPullImage $IMG_ADMINER buildOrPullImage $IMG_BUDIBASE_APP buildOrPullImage $IMG_BUDIBASE_WORKER buildOrPullImage $IMG_BUDIBASE_PROXY buildOrPullImage $IMG_BUDIBASE_COUCHDB buildOrPullImage $IMG_AUDIOBOOKSHELF buildOrPullImage $IMG_STANDARDNOTES_SERVER buildOrPullImage $IMG_STANDARDNOTES_LOCALSTACK buildOrPullImage $IMG_STANDARDNOTES_WEB buildOrPullImage $IMG_METABASE buildOrPullImage $IMG_KANBOARD_APP buildOrPullImage $IMG_WEKAN_APP buildOrPullImage $IMG_REVOLT_API buildOrPullImage $IMG_REVOLT_EVENTS buildOrPullImage $IMG_REVOLT_WEB buildOrPullImage $IMG_REVOLT_AUTUMN buildOrPullImage $IMG_REVOLT_JANUARY buildOrPullImage $IMG_REVOLT_CROND buildOrPullImage $IMG_REVOLT_PUSHD buildOrPullImage $IMG_MINTHCM_WEB buildOrPullImage $IMG_CLOUDBEAVER_APP buildOrPullImage $IMG_DBGATE_APP buildOrPullImage $IMG_TWENTY_APP buildOrPullImage $IMG_ODOO_APP buildOrPullImage $IMG_CALCOM buildOrPullImage $IMG_RALLLY_APP buildOrPullImage $IMG_EASYAPPOINTMENTS_APP buildOrPullImage $IMG_OPENPROJECT_APP buildOrPullImage $IMG_ZAMMAD buildOrPullImage $IMG_ZULIP_DB buildOrPullImage $IMG_ZULIP_APP buildOrPullImage $IMG_KILLBILL_DB buildOrPullImage $IMG_KILLBILL_APP buildOrPullImage $IMG_KILLBILL_WEB buildOrPullImage $IMG_INVOICESHELF_APP buildOrPullImage $IMG_INVOICESHELF_MCP buildOrPullImage $IMG_INVOICENINJA_APP buildOrPullImage $IMG_DOLIBARR_APP buildOrPullImage $IMG_DOLIBARR_MCP buildOrPullImage $IMG_N8N_APP buildOrPullImage $IMG_AUTOMATISCH_APP buildOrPullImage $IMG_ACTIVEPIECES_APP buildOrPullImage $IMG_BESZEL_APP buildOrPullImage $IMG_BESZEL_AGENT buildOrPullImage $IMG_TAIGA_BACK buildOrPullImage $IMG_TAIGA_BACK buildOrPullImage $IMG_TAIGA_FRONT buildOrPullImage $IMG_TAIGA_EVENTS buildOrPullImage $IMG_TAIGA_PROTECTED buildOrPullImage $IMG_OPENSIGN_SERVER buildOrPullImage $IMG_OPENSIGN_CLIENT buildOrPullImage $IMG_DOCUSEAL_APP buildOrPullImage $IMG_CONTROLR_APP buildOrPullImage $IMG_CONTROLR_ASPIRE buildOrPullImage $IMG_AKAUNTING_APP buildOrPullImage $IMG_AXELOR_APP buildOrPullImage $IMG_AXELOR_GOOVEE buildOrPullImage $IMG_CONVERTX_APP buildOrPullImage $IMG_KOPIA_APP buildOrPullImage $IMG_LOCALAI_SERVER buildOrPullImage $IMG_LOCALAI_RECALL buildOrPullImage $IMG_LOCALAI_LRHEALTH buildOrPullImage $IMG_LOCALAI_SSHBOX buildOrPullImage $IMG_LOCALAI_DIND buildOrPullImage $IMG_LOCALAI_AGI buildOrPullImage $IMG_COMFYUI_APP buildOrPullImage $IMG_LANGFLOW_APP buildOrPullImage $IMG_ANYTHINGLLM_APP buildOrPullImage $IMG_PERPLEXICA_APP buildOrPullImage $IMG_FIRECRAWL_DB buildOrPullImage $IMG_FIRECRAWL_PLAYWRIGHT buildOrPullImage $IMG_FIRECRAWL_API buildOrPullImage $IMG_LIBRECHAT_APP buildOrPullImage $IMG_LIBRECHAT_RAGAPI buildOrPullImage $IMG_CRAWL4AI_APP buildOrPullImage $IMG_CRAWL4AI_PROXY buildOrPullImage $IMG_OLLAMA_SERVER buildOrPullImage $IMG_OPENWEBUI_APP buildOrPullImage $IMG_OPENWEBUI_OPENTERMINAL buildOrPullImage $IMG_OPENWEBUI_MCPO buildOrPullImage $IMG_OPENWEBUI_PIPELINES buildOrPullImage $IMG_OPENWEBUI_MCP buildOrPullImage $IMG_KHOJ_SANDBOX buildOrPullImage $IMG_KHOJ_COMPUTER buildOrPullImage $IMG_KHOJ_SERVER buildOrPullImage $IMG_LOBECHAT_APP buildOrPullImage $IMG_INVOKEAI_APP buildOrPullImage $IMG_RAGFLOW_INFINITY buildOrPullImage $IMG_RAGFLOW_APP buildOrPullImage $IMG_TABBYML_APP buildOrPullImage $IMG_DEEPWIKIOPEN_APP buildOrPullImage $IMG_DOCLING_APP buildOrPullImage $IMG_DIFY_SSRF buildOrPullImage $IMG_DIFY_API buildOrPullImage $IMG_DIFY_PLUGIND buildOrPullImage $IMG_DIFY_SANDBOX buildOrPullImage $IMG_DIFY_WEB buildOrPullImage $IMG_MINDSDB_APP buildOrPullImage $IMG_WATERCRAWL_APP buildOrPullImage $IMG_WATERCRAWL_PLAYWRIGHT buildOrPullImage $IMG_WATERCRAWL_FRONTEND buildOrPullImage $IMG_WATERCRAWL_MCP buildOrPullImage $IMG_FLOWISE_APP buildOrPullImage $IMG_FLOWISE_WORKER buildOrPullImage $IMG_SURFSENSE_APP buildOrPullImage $IMG_NOCODB_APP buildOrPullImage $IMG_ENTE_SERVER buildOrPullImage $IMG_ENTE_WEB buildOrPullImage $IMG_MORPHIC_APP buildOrPullImage $IMG_OPENNOTEBOOK_DB buildOrPullImage $IMG_OPENNOTEBOOK_APP buildOrPullImage $IMG_OPENNOTEBOOK_MCP buildOrPullImage $IMG_APPSMITH_APP buildOrPullImage $IMG_TRILIUM buildOrPullImage $IMG_DOCSGPT_BACKEND buildOrPullImage $IMG_DOCSGPT_FRONTEND buildOrPullImage $IMG_MEMOS_APP buildOrPullImage $IMG_SILLYTAVERN buildOrPullImage $IMG_LEMONADE_SERVER buildOrPullImage $IMG_SPEAKR_APP buildOrPullImage $IMG_SPEAKR_WHISPERX buildOrPullImage $IMG_INSANELYFASTWHISPER_APP buildOrPullImage $IMG_IVBOX_APP buildOrPullImage $IMG_MONICA_APP buildOrPullImage $IMG_MONICA_WEB buildOrPullImage $IMG_AFFINE_APP buildOrPullImage $IMG_JOPLIN_APP buildOrPullImage $IMG_JOPLIN_TRANSCRIBE_APP buildOrPullImage $IMG_SUPERSET_APP buildOrPullImage $IMG_SUPERSET_WORKER buildOrPullImage $IMG_SUPERSET_WEBSOCKET buildOrPullImage $IMG_KOKORO_TTS buildOrPullImage $IMG_KOKORO_WEB buildOrPullImage $IMG_CHATTERBOX_APP buildOrPullImage $IMG_LITELLM_APP buildOrPullImage $IMG_LANGFUSE_WORKER buildOrPullImage $IMG_LANGFUSE_APP buildOrPullImage $IMG_SKYVERN_API buildOrPullImage $IMG_SKYVERN_WEB buildOrPullImage $IMG_WGER buildOrPullImage $IMG_WORKOUTCOOL buildOrPullImage $IMG_OPENRAG_BACKEND buildOrPullImage $IMG_OPENRAG_FRONTEND buildOrPullImage $IMG_OPENRAG_LANGFLOW buildOrPullImage $IMG_VOICEBOX_APP buildOrPullImage $IMG_OPENCODE_APP buildOrPullImage $IMG_OPENCODE_PLAYWRIGHT buildOrPullImage $IMG_OPENSKILLS_APP buildOrPullImage $IMG_EMAILCLASSIFIERAI_APP buildOrPullImage $IMG_HERMES_APP buildOrPullImage $IMG_HERMES_TERMINAL buildOrPullImage $IMG_HERMES_CAMOFOX buildOrPullImage $IMG_HERMES_WEBUI buildOrPullImage $IMG_AUTOKB_APP buildOrPullImage $IMG_AUTOKB_MCP buildOrPullImage $IMG_SUITECRM_APP buildOrPullImage $IMG_SUITECRM_MCP buildOrPullImage $IMG_HEDGEDOC_FRONTEND buildOrPullImage $IMG_HEDGEDOC_BACKEND buildOrPullImage $IMG_HEDGEDOC_MCP buildOrPullImage $IMG_PRESENTON_APP buildOrPullImage $IMG_PRESENTON_MCP buildOrPullImage $IMG_BASICMEMORY_APP buildOrPullImage $IMG_COGNEE_APP buildOrPullImage $IMG_COGNEE_FRONTEND buildOrPullImage $IMG_COGNEE_MCP buildOrPullImage $IMG_LIGHTRAG_APP buildOrPullImage $IMG_OPENSERP_APP buildOrPullImage $IMG_SURICATA_APP buildOrPullImage $IMG_SURICATA_WEB buildOrPullImage $IMG_CROWDSEC_APP buildOrPullImage $IMG_CROWDSEC_WEB buildOrPullImage $IMG_FASTCRW_APP buildOrPullImage $IMG_LIGHTPANDA buildOrPullImage $IMG_FASTCRW_PROXY buildOrPullImage $IMG_HEADLESS_BROWSER buildOrPullImage $IMG_WHITEBOARDRT_WEB buildOrPullImage $IMG_WHITEBOARDRT_AGENT buildOrPullImage $IMG_WHITEBOARDRT_MCP buildOrPullImage $IMG_WHITEBOARDRT_STORAGE buildOrPullImage $IMG_WHITEBOARDRT_SERVER buildOrPullImage $IMG_MDCOLLAB_BACKEND buildOrPullImage $IMG_MDCOLLAB_FRONTEND buildOrPullImage $IMG_MDCOLLAB_MCP buildOrPullImage $IMG_HALOSPEAK_APP #ADD_NEW_PULL_DOCKER_IMAGES_HERE } function pullImagesUpdatePB() { pb_add="$1" pb_cur=$(($pb_cur+$pb_add)) pb_res=$(($pb_base+$pb_cur)) draw_progress_bar $pb_res } function pullBaseServicesDockerImages() { pb_base=5 pb_cur=0 pullImage $IMG_PORTAINER if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 1 pullImage $IMG_ADGUARD if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_OPENLDAP_SERVER if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 1 pullImage $IMG_OPENLDAP_PHP if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_OPENLDAP_MANAGER if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 1 pullImage $IMG_MAILU_ADMIN if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_ANTISPAM if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 1 pullImage $IMG_MAILU_ANTIVIRUS if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_FETCHMAIL if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_FRONT if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_IMAP if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_OLETOOLS if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_SMTP if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_UNBOUND if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_WEBDAV if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_MAILU_WEBMAIL if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_DUPLICATI if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_AUTHELIA if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_REDIS if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_SYNCTHING if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_HEIMDALL if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_OFELIA if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_CADDY if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 pullImage $IMG_UPTIMEKUMA if [ $? -ne 0 ]; then return 4 fi pullImagesUpdatePB 2 rm -f $HOME/script-server.zip wget -q -O $HOME/script-server.zip https://github.com/bugy/script-server/releases/download/1.18.0/script-server.zip retVal=$? if [ $retVal -ne 0 ] || ! [ -f $HOME/script-server.zip ]; then return 4 fi } function pullCommonImages() { pullImage $IMG_POSTGRES pullImage $IMG_MYSQL pullImage $IMG_REDIS pullImage $IMG_NGINX pullImage $IMG_ALPINE pullImage $IMG_MINIO pullImage $IMG_MONGODB } function outputEncConfigFile() { cat < $CONFIG_FILE # Encrypted Configuration File $hshqlogo # General Info BEGIN IS_CONFIG_INIT="true" IS_INSTALLED="false" IS_INSTALLING="false" IS_AUTO_INSTALL="$IS_AUTO_INSTALL" # General Info END # Services Info BEGIN # Services Info END # RelayServer Settings BEGIN RELAYSERVER_NAME= RELAYSERVER_SERVER_IP="A" RELAYSERVER_EXT_EMAIL_HOSTNAME= RELAYSERVER_SUB_WG="wg" RELAYSERVER_SUB_RELAYSERVER="rs" RELAYSERVER_LE_CERT_DOMAINS="unset" RELAYSERVER_CURRENT_SSH_PORT= RELAYSERVER_REMOTE_USERNAME= RELAYSERVER_HSHQ_BASE_DIR= RELAYSERVER_HSHQ_DATA_DIR= RELAYSERVER_HSHQ_NONBACKUP_DIR= RELAYSERVER_HSHQ_SCRIPTS_DIR= RELAYSERVER_HSHQ_SECRETS_DIR= RELAYSERVER_HSHQ_STACKS_DIR= RELAYSERVER_HSHQ_SSL_DIR= RELAYSERVER_SSH_PORT= RELAYSERVER_SSH_PRIVATE_KEY_FILENAME= RELAYSERVER_SSH_PRIVATE_KEY_PHRASE= RELAYSERVER_CLIENT_DEFAULT_MTU="1372" RELAYSERVER_SERVER_DEFAULT_MTU="1372" RELAYSERVER_PERSISTENT_KEEPALIVE="20" RELAYSERVER_WG_PORTAL_PORT="8323" RELAYSERVER_WG_INTERFACE_NAME="wg0" RELAYSERVER_WG_PORT="51821" RELAYSERVER_WG_VPN_NETNAME= RELAYSERVER_WG_INTERNET_NETNAME= RELAYSERVER_WG_SV_PRIVATEKEY= RELAYSERVER_WG_SV_PUBLICKEY= RELAYSERVER_WG_SV_IP= RELAYSERVER_WG_SV_PRESHAREDKEY= RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY= RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY= RELAYSERVER_WG_SV_CLIENTDNS_IP= RELAYSERVER_WG_HS_PRIVATEKEY= RELAYSERVER_WG_HS_IP= RELAYSERVER_WG_HS_PRESHAREDKEY= RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY= RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY= RELAYSERVER_WG_HS_CLIENTDNS_IP= RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY= RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY= # RelayServer Settings END # RelayServer Services BEGIN RELAYSERVER_PORTAINER_ADMIN_USERNAME= RELAYSERVER_PORTAINER_ADMIN_PASSWORD= RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT= RELAYSERVER_ADGUARD_ADMIN_USERNAME= RELAYSERVER_ADGUARD_ADMIN_PASSWORD= RELAYSERVER_CADDYDNS_ADMIN_USERNAME= RELAYSERVER_CADDYDNS_ADMIN_PASSWORD= RELAYSERVER_FILEBROWSER_ADMIN_USERNAME= RELAYSERVER_FILEBROWSER_ADMIN_PASSWORD= RELAYSERVER_WGPORTAL_ADMIN_USERNAME= RELAYSERVER_WGPORTAL_ADMIN_EMAIL= RELAYSERVER_WGPORTAL_ADMIN_PASSWORD= RELAYSERVER_CLIENTDNS_ADMIN_USERNAME= RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD= RELAYSERVER_RSPAMD_ADMIN_USERNAME= RELAYSERVER_RSPAMD_ADMIN_PASSWORD= RELAYSERVER_SYNCTHING_ADMIN_USERNAME= RELAYSERVER_SYNCTHING_ADMIN_PASSWORD= RELAYSERVER_SYNCTHING_API_KEY= RELAYSERVER_SYNCTHING_DEVICE_ID= RELAYSERVER_SYNCTHING_FOLDER_ID= # RelayServer Services END # clientdns-user1 (Service Details) BEGIN CLIENTDNS_USER1_ADMIN_USERNAME= CLIENTDNS_USER1_ADMIN_PASSWORD= # clientdns-user1 (Service Details) END # Config File Encryption Passphrase BEGIN CONFIG_ENCRYPTION_PASSPHRASE="$CONFIG_ENCRYPTION_PASSPHRASE" # Config File Encryption Passphrase END # Docker Installation Info BEGIN XDG_RUNTIME_DIR= TRUSTED_PROXIES="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16" # Docker Installation Info END # Service Details BEGIN # Portainer (Service Details) BEGIN PORTAINER_ADMIN_USERID= PORTAINER_DB_KEY= # Portainer (Service Details) END # Script-server (Service Details) BEGIN SCRIPTSERVER_INIT_ENV="true" SCRIPTSERVER_ADMIN_USERNAME= SCRIPTSERVER_ADMIN_PASSWORD= # Script-server (Service Details) END # Authelia (Service Details) BEGIN AUTHELIA_REDIRECTION_URL= # Authelia (Service Details) END # SysUtils (Service Details) BEGIN GRAFANA_ADMIN_USERNAME= GRAFANA_ADMIN_PASSWORD= INFLUXDB_ADMIN_USERNAME= INFLUXDB_ADMIN_PASSWORD= INFLUXDB_ORG= INFLUXDB_TOKEN= INFLUXDB_HA_BUCKET= ALLOY_ADMIN_USERNAME= ALLOY_ADMIN_PASSWORD= # SysUtils (Service Details) END # OpenLDAP (Service Details) BEGIN LDAP_ADMIN_USER_USERNAME= LDAP_ADMIN_USER_PASSWORD= LDAP_ADMIN_BIND_DN= LDAP_ADMIN_BIND_PASSWORD= LDAP_READONLY_USER_USERNAME= LDAP_READONLY_USER_BIND_DN= LDAP_READONLY_USER_PASSWORD= LDAP_CONFIG_PASSWORD= LDAP_URI= LDAPS_URI= LDAP_BASE_DN= LDAP_ADMIN_USER_GROUP_NAME="admins" LDAP_PRIMARY_USER_GROUP_NAME="primaryusers" LDAP_BASIC_USER_GROUP_NAME="basicusers" LDAP_EMAIL_FROM_NAME="LDAP $(getAdminEmailName)" LDAP_ACCOUNT_REQUESTS_ENABLED="true" LDAP_ACCOUNT_REQUESTS_EMAIL= # OpenLDAP (Service Details) END # Mailu (Service Details) BEGIN EMAIL_ADMIN_USERNAME= EMAIL_ADMIN_PASSWORD= EMAIL_SMTP_USERNAME= EMAIL_SMTP_PASSWORD= EMAIL_SMTP_EMAIL_ADDRESS= SMTP_HOSTNAME="mailu-front" SMTP_HOSTPORT="25" SMTP_RELAY_HOST= SMTP_RELAY_USERNAME= SMTP_RELAY_PASSWORD= MAILU_API_TOKEN= EMAIL_JOINT_USERNAME= EMAIL_JOINT_PASSWORD= EMAIL_JOINT_EMAIL_ADDRESS= # Mailu (Service Details) END # Wazuh (Service Details) BEGIN WAZUH_USERS_DASHBOARD_USERNAME="kibanaserver" WAZUH_USERS_DASHBOARD_PASSWORD= WAZUH_API_USERNAME= WAZUH_API_PASSWORD= WAZUH_USERS_ADMIN_USERNAME= WAZUH_USERS_ADMIN_PASSWORD= WAZUH_USERS_KIBANARO_PASSWORD= WAZUH_USERS_LOGSTASH_PASSWORD= WAZUH_USERS_READALL_PASSWORD= WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD= WAZUH_MANAGER_AUTH_PASSWORD= # Wazuh (Service Details) END # Collabora (Service Details) BEGIN COLLABORA_ADMIN_USERNAME= COLLABORA_ADMIN_PASSWORD= # Collabora (Service Details) END # Nextcloud (Service Details) BEGIN NEXTCLOUD_ADMIN_USERNAME= NEXTCLOUD_ADMIN_PASSWORD= NEXTCLOUD_ADMIN_EMAIL_ADDRESS= NEXTCLOUD_DATABASE_NAME= NEXTCLOUD_DATABASE_USER= NEXTCLOUD_DATABASE_USER_PASSWORD= NEXTCLOUD_REDIS_PASSWORD= NEXTCLOUD_EMAIL_FROM_ADDRESS= DEFAULT_PHONE_REGION=US NEXTCLOUD_IMAGINARY_PORT="7557" NEXTCLOUD_PUSH_PORT="7867" NEXTCLOUD_TALKHPB_SIGNALING_SECRET= NEXTCLOUD_TALKHPB_INTERNAL_SECRET= NEXTCLOUD_TALKHPB_RECORDING_SECRET= NEXTCLOUD_HARP_PORT="8780" NEXTCLOUD_HARP_SHARED_KEY= NEXTCLOUD_DATABASE_READONLYUSER= NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD= NEXTCLOUD_TOKEN_ENCRYPTION_KEY= # Nextcloud (Service Details) END # Matrix (Service Details) BEGIN MATRIX_DATABASE_NAME= MATRIX_DATABASE_USER= MATRIX_DATABASE_USER_PASSWORD= MATRIX_REDIS_PASSWORD= MATRIX_SYNAPSE_REGISTRATION_SECRET= MATRIX_SYNAPSE_MACAROON_SECRET= MATRIX_SYNAPSE_FORM_SECRET= MATRIX_DATABASE_READONLYUSER= MATRIX_DATABASE_READONLYUSER_PASSWORD= # Matrix (Service Details) END # Duplicati (Service Details) BEGIN DUPLICATI_ADMIN_PASSWORD= DUPLICATI_SETTINGS_ENCRYPTION_KEY= # Duplicati (Service Details) END # Mastodon (Service Details) BEGIN MASTODON_ADMIN_EMAIL_ADDRESS= MASTODON_ADMIN_PASSWORD= MASTODON_ADMIN_USERNAME= MASTODON_DATABASE_NAME= MASTODON_DATABASE_USER= MASTODON_DATABASE_USER_PASSWORD= MASTODON_REDIS_PASSWORD= MASTODON_ELASTICSEARCH_PASSWORD= MASTODON_ARE_DETERMINISTIC_KEY= MASTODON_ARE_KEY_DERIVATION_SALT= MASTODON_ARE_PRIMARY_KEY= MASTODON_DATABASE_READONLYUSER= MASTODON_DATABASE_READONLYUSER_PASSWORD= # Mastodon (Service Details) END # Dozzle (Service Details) BEGIN DOZZLE_USERNAME= DOZZLE_PASSWORD= # Dozzle (Service Details) END # SearxNG (Service Details) BEGIN SEARXNG_REDIS_PASSWORD= SEARXNG_SECRET_KEY= # SearxNG (Service Details) END # Jellyfin (Service Details) BEGIN JELLYFIN_ADMIN_USERNAME= JELLYFIN_ADMIN_PASSWORD= # Jellyfin (Service Details) END # FileBrowser (Service Details) BEGIN FILEBROWSER_USERNAME= FILEBROWSER_PASSWORD= # FileBrowser (Service Details) END # PhotoPrism (Service Details) BEGIN PHOTOPRISM_INIT_ENV="true" PHOTOPRISM_ADMIN_USERNAME= PHOTOPRISM_ADMIN_PASSWORD= PHOTOPRISM_DATABASE_ROOT_PASSWORD= PHOTOPRISM_DATABASE_NAME= PHOTOPRISM_DATABASE_USER= PHOTOPRISM_DATABASE_USER_PASSWORD= PHOTOPRISM_DATABASE_READONLYUSER= PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD= # PhotoPrism (Service Details) END # Guacamole (Service Details) BEGIN GUACAMOLE_DEFAULT_ADMIN_USERNAME=guacadmin GUACAMOLE_DEFAULT_ADMIN_PASSWORD=guacadmin GUACAMOLE_DATABASE_ROOT_PASSWORD= GUACAMOLE_DATABASE_NAME= GUACAMOLE_DATABASE_USER= GUACAMOLE_DATABASE_USER_PASSWORD= GUACAMOLE_DATABASE_READONLYUSER= GUACAMOLE_DATABASE_READONLYUSER_PASSWORD= # Guacamole (Service Details) END # UptimeKuma (Service Details) BEGIN UPTIMEKUMA_USERNAME= UPTIMEKUMA_PASSWORD= UPTIMEKUMA_HEARTBEAT_INTERVAL=60 UPTIMEKUMA_HEARTBEAT_RETRIES=3 UPTIMEKUMA_RETRY_INTERVAL=20 UPTIMEKUMA_RESEND_NOTIFY=360 # UptimeKuma (Service Details) END # Wordpress (Service Details) BEGIN WORDPRESS_INIT_ENV="true" WORDPRESS_ADMIN_USERNAME= WORDPRESS_ADMIN_PASSWORD= WORDPRESS_ADMIN_EMAIL_ADDRESS= WORDPRESS_DATABASE_ROOT_PASSWORD= WORDPRESS_DATABASE_NAME= WORDPRESS_DATABASE_USER= WORDPRESS_DATABASE_USER_PASSWORD= WORDPRESS_DATABASE_READONLYUSER= WORDPRESS_DATABASE_READONLYUSER_PASSWORD= WORDPRESS_APP_PASSWORD= # Wordpress (Service Details) END # Ghost (Service Details) BEGIN GHOST_DATABASE_ROOT_PASSWORD= GHOST_DATABASE_NAME= GHOST_DATABASE_USER= GHOST_DATABASE_USER_PASSWORD= GHOST_DATABASE_READONLYUSER= GHOST_DATABASE_READONLYUSER_PASSWORD= # Ghost (Service Details) END # Wikijs (Service Details) BEGIN WIKIJS_INIT_ENV="true" WIKIJS_ADMIN_USERNAME= WIKIJS_ADMIN_EMAIL_ADDRESS= WIKIJS_ADMIN_PASSWORD= WIKIJS_ADMIN_API_KEY= WIKIJS_DATABASE_NAME= WIKIJS_DATABASE_USER= WIKIJS_DATABASE_USER_PASSWORD= WIKIJS_DATABASE_READONLYUSER= WIKIJS_DATABASE_READONLYUSER_PASSWORD= # Wikijs (Service Details) END # PeerTube (Service Details) BEGIN PEERTUBE_ADMIN_USERNAME= PEERTUBE_ADMIN_PASSWORD= PEERTUBE_ADMIN_EMAIL_ADDRESS= PEERTUBE_DATABASE_NAME= PEERTUBE_DATABASE_USER= PEERTUBE_DATABASE_USER_PASSWORD= PEERTUBE_DATABASE_READONLYUSER= PEERTUBE_DATABASE_READONLYUSER_PASSWORD= # PeerTube (Service Details) END # HomeAssistant (Service Details) BEGIN HOMEASSISTANT_DATABASE_NAME= HOMEASSISTANT_DATABASE_USER= HOMEASSISTANT_DATABASE_USER_PASSWORD= HOMEASSISTANT_TASMOADMIN_USER= HOMEASSISTANT_TASMOADMIN_USER_PASSWORD= HOMEASSISTANT_CONFIGURATOR_USER= HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD= HOMEASSISTANT_DATABASE_READONLYUSER= HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD= # HomeAssistant (Service Details) END # Gitlab (Service Details) BEGIN GITLAB_ROOT_PASSWORD= GITLAB_DATABASE_NAME= GITLAB_DATABASE_USER= GITLAB_DATABASE_USER_PASSWORD= GITLAB_REDIS_PASSWORD= GITLAB_DATABASE_READONLYUSER= GITLAB_DATABASE_READONLYUSER_PASSWORD= # Gitlab (Service Details) END # Vaultwarden (Service Details) BEGIN VAULTWARDEN_ADMIN_TOKEN= VAULTWARDEN_DATABASE_NAME= VAULTWARDEN_DATABASE_USER= VAULTWARDEN_DATABASE_USER_PASSWORD= VAULTWARDEN_DATABASE_READONLYUSER= VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD= # Vaultwarden (Service Details) END # Discourse (Service Details) BEGIN DISCOURSE_ADMIN_USERNAME= DISCOURSE_ADMIN_PASSWORD= DISCOURSE_ADMIN_EMAIL_ADDRESS= DISCOURSE_DATABASE_NAME= DISCOURSE_DATABASE_USER= DISCOURSE_DATABASE_USER_PASSWORD= DISCOURSE_REDIS_PASSWORD= DISCOURSE_DATABASE_READONLYUSER= DISCOURSE_DATABASE_READONLYUSER_PASSWORD= # Discourse (Service Details) END # Syncthing (Service Details) BEGIN SYNCTHING_ADMIN_USERNAME= SYNCTHING_ADMIN_PASSWORD= SYNCTHING_API_KEY= SYNCTHING_DEVICE_ID= # Syncthing (Service Details) END # CodeServer (Service Details) BEGIN CODESERVER_ADMIN_USERNAME= CODESERVER_ADMIN_PASSWORD= # CodeServer (Service Details) END # Shlink (Service Details) BEGIN SHLINK_DATABASE_NAME= SHLINK_DATABASE_USER= SHLINK_DATABASE_USER_PASSWORD= SHLINK_REDIS_PASSWORD= SHLINK_INITIAL_API_KEY= SHLINK_DATABASE_READONLYUSER= SHLINK_DATABASE_READONLYUSER_PASSWORD= # Shlink (Service Details) END # FireflyIII (Service Details) BEGIN FIREFLY_DATABASE_NAME= FIREFLY_DATABASE_USER= FIREFLY_DATABASE_USER_PASSWORD= FIREFLY_REDIS_PASSWORD= FIREFLY_INITIAL_API_KEY= FIREFLY_STATIC_CRON_TOKEN= FIREFLY_DATABASE_READONLYUSER= FIREFLY_DATABASE_READONLYUSER_PASSWORD= # FireflyIII (Service Details) END # Excalidraw (Service Details) BEGIN EXCALIDRAW_REDIS_PASSWORD= # Excalidraw (Service Details) END # Gitea (Service Details) BEGIN GITEA_ADMIN_USERNAME= GITEA_ADMIN_EMAIL_ADDRESS= GITEA_ADMIN_PASSWORD= GITEA_DATABASE_NAME= GITEA_DATABASE_USER= GITEA_DATABASE_USER_PASSWORD= GITEA_DATABASE_READONLYUSER= GITEA_DATABASE_READONLYUSER_PASSWORD= # Gitea (Service Details) END # Invidious (Service Details) BEGIN INVIDIOUS_DATABASE_NAME= INVIDIOUS_DATABASE_USER= INVIDIOUS_DATABASE_USER_PASSWORD= INVIDIOUS_DATABASE_READONLYUSER= INVIDIOUS_DATABASE_READONLYUSER_PASSWORD= # Invidious (Service Details) END # Mealie (Service Details) BEGIN MEALIE_ADMIN_USERNAME= MEALIE_ADMIN_EMAIL_ADDRESS= MEALIE_ADMIN_PASSWORD= MEALIE_ADMIN_API_KEY= MEALIE_DATABASE_NAME= MEALIE_DATABASE_USER= MEALIE_DATABASE_USER_PASSWORD= MEALIE_DATABASE_READONLYUSER= MEALIE_DATABASE_READONLYUSER_PASSWORD= # Mealie (Service Details) END # Kasm (Service Details) BEGIN KASM_INIT_ENV="true" KASM_ADMIN_EMAIL_ADDRESS= KASM_ADMIN_PASSWORD= KASM_USER_EMAIL_ADDRESS= KASM_USER_PASSWORD= # Kasm (Service Details) END # Remotely (Service Details) BEGIN REMOTELY_INIT_ENV="true" REMOTELY_ADMIN_USERNAME= REMOTELY_ADMIN_EMAIL_ADDRESS= REMOTELY_ADMIN_PASSWORD= # Remotely (Service Details) END # SQLPad (Service Details) BEGIN SQLPAD_ADMIN_USERNAME= SQLPAD_ADMIN_PASSWORD= # SQLPad (Service Details) END # Heimdall (Service Details) BEGIN HEIMDALL_ADMIN_USERNAME= HEIMDALL_ADMIN_PASSWORD= HEIMDALL_USER_USERNAME= HEIMDALL_USER_PASSWORD= HEIMDALL_HOMESERVERS_USERNAME= HEIMDALL_HOMESERVERS_PASSWORD= HEIMDALL_RELAYSERVER_USERNAME= HEIMDALL_RELAYSERVER_PASSWORD= HEIMDALL_WINDOW_TITLE= # Heimdall (Service Details) END # Caddy (Service Details) BEGIN CADDY_CERT_RENEW_INTERVAL=1h CADDY_CERT_INTERMEDIATE_LIFETIME=90d CADDY_CERT_LEAF_LIFETIME=7d CADDY_RESET_CONFIG_HOUROFDAY=4 CADDY_SNIPPET_TLS_HSHQ=tls-hshq CADDY_SNIPPET_RIP=rip CADDY_SNIPPET_FWDAUTH=fwd-auth CADDY_SNIPPET_TRUSTEDPROXIES=trusted-proxy-list CADDY_SNIPPET_SAFEHEADER=safe-header CADDY_SNIPPET_SAFEHEADERALLOWFRAME=safe-header-allow-frame CADDY_SNIPPET_SAFEHEADERALLOWCORS=safe-header-allow-cors CADDY_SNIPPET_SAFEHEADERCORS=safe-header-cors CADDY_SNIPPET_NOTHOMESUBNET=not-home-subnet CADDY_SNIPPET_SAFEHEADERCORSPREFLIGHT=safe-header-cors-preflight CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED=safe-header-cors-automated CADDY_SNIPPET_BASEHEADER=base-header CADDY_SNIPPET_DEFAULTCSP=default-csp CADDY_SNIPPET_RELAXEDCSP=relaxed-csp CADDY_SNIPPET_LOG_TRUE=log_true CADDY_SNIPPET_LOG_FALSE=log_false CADDY_SNIPPET_INTERNAL_GUARD=internal_guard # Caddy (Service Details) END # Calibre (Service Details) BEGIN CALIBRE_WEB_INIT_ENV="true" CALIBRE_WEB_ADMIN_USERNAME= CALIBRE_WEB_ADMIN_EMAIL_ADDRESS= CALIBRE_WEB_ADMIN_PASSWORD= # Calibre (Service Details) END # Linkwarden (Service Details) BEGIN LINKWARDEN_DATABASE_NAME= LINKWARDEN_DATABASE_USER= LINKWARDEN_DATABASE_USER_PASSWORD= LINKWARDEN_NEXTAUTH_SECRET= LINKWARDEN_OIDC_CLIENT_SECRET= LINKWARDEN_DATABASE_READONLYUSER= LINKWARDEN_DATABASE_READONLYUSER_PASSWORD= # Linkwarden (Service Details) END # Bar Assistant (Service Details) BEGIN BARASSISTANT_REDIS_PASSWORD= BARASSISTANT_MEILISEARCH_KEY= BARASSISTANT_API_KEY= BARASSISTANT_MCP_API_KEY= # Bar Assistant (Service Details) END # FreshRSS (Service Details) BEGIN FRESHRSS_INIT_ENV="true" FRESHRSS_ADMIN_USERNAME= FRESHRSS_ADMIN_PASSWORD= FRESHRSS_ADMIN_EMAIL_ADDRESS= FRESHRSS_DATABASE_NAME= FRESHRSS_DATABASE_USER= FRESHRSS_DATABASE_USER_PASSWORD= FRESHRSS_OIDC_CLIENT_SECRET= FRESHRSS_DATABASE_READONLYUSER= FRESHRSS_DATABASE_READONLYUSER_PASSWORD= # FreshRSS (Service Details) END # Keila (Service Details) BEGIN KEILA_INIT_ENV="true" KEILA_ADMIN_USERNAME= KEILA_ADMIN_EMAIL_ADDRESS= KEILA_ADMIN_PASSWORD= KEILA_ADMIN_API_KEY= KEILA_DATABASE_NAME= KEILA_DATABASE_USER= KEILA_DATABASE_USER_PASSWORD= KEILA_DATABASE_READONLYUSER= KEILA_DATABASE_READONLYUSER_PASSWORD= # Keila (Service Details) END # Wallabag (Service Details) BEGIN WALLABAG_INIT_ENV="true" WALLABAG_ADMIN_USERNAME= WALLABAG_ADMIN_EMAIL_ADDRESS= WALLABAG_ADMIN_PASSWORD= WALLABAG_DATABASE_NAME= WALLABAG_DATABASE_USER= WALLABAG_DATABASE_USER_PASSWORD= WALLABAG_ENV_SECRET= WALLABAG_REDIS_PASSWORD= WALLABAG_DATABASE_READONLYUSER= WALLABAG_DATABASE_READONLYUSER_PASSWORD= # Wallabag (Service Details) END # Jupyter (Service Details) BEGIN JUPYTER_INIT_ENV="true" JUPYTER_ADMIN_PASSWORD= # Jupyter (Service Details) END # Paperless (Service Details) BEGIN PAPERLESS_INIT_ENV="true" PAPERLESS_SECRET_KEY= PAPERLESS_REDIS_PASSWORD= PAPERLESS_ADMIN_USERNAME= PAPERLESS_ADMIN_EMAIL_ADDRESS= PAPERLESS_ADMIN_PASSWORD= PAPERLESS_DATABASE_NAME= PAPERLESS_DATABASE_USER= PAPERLESS_DATABASE_USER_PASSWORD= PAPERLESS_OIDC_CLIENT_SECRET= PAPERLESS_DATABASE_READONLYUSER= PAPERLESS_DATABASE_READONLYUSER_PASSWORD= PAPERLESS_AI_ADMIN_USERNAME= PAPERLESS_AI_ADMIN_PASSWORD= PAPERLESS_AI_API_KEY= PAPERLESS_AI_JWT_SECRET= PAPERLESS_GPT_ADMIN_USERNAME= PAPERLESS_GPT_ADMIN_PASSWORD= PAPERLESS_API_TOKEN= PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME= PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID= PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME= PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID= PAPERLESS_KNOWLEDGEBASE_TAG_NAME= PAPERLESS_KNOWLEDGEBASE_TAG_ID= PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME= PAPERLESS_TRANSCRIPTION_DOCTYPE_ID= PAPERLESS_AKB_WORKFLOW_ID= # Paperless (Service Details) END # SpeedtestTrackerLocal (Service Details) BEGIN SPEEDTEST_TRACKER_LOCAL_INIT_ENV="true" SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME= SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS= SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD= SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME= SPEEDTEST_TRACKER_LOCAL_DATABASE_USER= SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD= SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER= SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD= # SpeedtestTrackerLocal (Service Details) END # SpeedtestTrackerVPN (Service Details) BEGIN SPEEDTEST_TRACKER_VPN_INIT_ENV="true" SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME= SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS= SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD= SPEEDTEST_TRACKER_VPN_DATABASE_NAME= SPEEDTEST_TRACKER_VPN_DATABASE_USER= SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD= SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER= SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD= # SpeedtestTrackerVPN (Service Details) END # Change Detection (Service Details) BEGIN CHANGEDETECTION_INIT_ENV="true" CHANGEDETECTION_ADMIN_PASSWORD= # Change Detection (Service Details) END # Huginn (Service Details) BEGIN HUGINN_INIT_ENV="true" HUGINN_APP_SECRET_TOKEN= HUGINN_ADMIN_USERNAME= HUGINN_ADMIN_EMAIL_ADDRESS= HUGINN_ADMIN_PASSWORD= HUGINN_DATABASE_NAME= HUGINN_DATABASE_USER= HUGINN_DATABASE_USER_PASSWORD= HUGINN_DATABASE_READONLYUSER= HUGINN_DATABASE_READONLYUSER_PASSWORD= # Huginn (Service Details) END # Coturn (Service Details) BEGIN COTURN_STATIC_SECRET= # Coturn (Service Details) END # Piped (Service Details) BEGIN PIPED_DATABASE_NAME= PIPED_DATABASE_USER= PIPED_DATABASE_USER_PASSWORD= PIPED_DATABASE_READONLYUSER= PIPED_DATABASE_READONLYUSER_PASSWORD= # Piped (Service Details) END # GrampsWeb (Service Details) BEGIN GRAMPSWEB_INIT_ENV="true" GRAMPSWEB_ADMIN_USERNAME= GRAMPSWEB_ADMIN_PASSWORD= GRAMPSWEB_ADMIN_EMAIL_ADDRESS= GRAMPSWEB_SECRET_KEY= GRAMPSWEB_REDIS_PASSWORD= # GrampsWeb (Service Details) END # Penpot (Service Details) BEGIN PENPOT_INIT_ENV="true" PENPOT_REDIS_PASSWORD= PENPOT_DATABASE_NAME= PENPOT_DATABASE_USER= PENPOT_DATABASE_USER_PASSWORD= PENPOT_SECRET_KEY= PENPOT_DATABASE_READONLYUSER= PENPOT_DATABASE_READONLYUSER_PASSWORD= # Penpot (Service Details) END # EspoCRM (Service Details) BEGIN ESPOCRM_INIT_ENV="true" ESPOCRM_ADMIN_USERNAME= ESPOCRM_ADMIN_PASSWORD= ESPOCRM_DATABASE_NAME= ESPOCRM_DATABASE_ROOT_PASSWORD= ESPOCRM_DATABASE_USER= ESPOCRM_DATABASE_USER_PASSWORD= ESPOCRM_DATABASE_READONLYUSER= ESPOCRM_DATABASE_READONLYUSER_PASSWORD= # EspoCRM (Service Details) END # Immich (Service Details) BEGIN IMMICH_INIT_ENV="true" IMMICH_ADMIN_USERNAME= IMMICH_ADMIN_PASSWORD= IMMICH_ADMIN_EMAIL_ADDRESS= IMMICH_DATABASE_NAME= IMMICH_DATABASE_USER= IMMICH_DATABASE_USER_PASSWORD= IMMICH_REDIS_PASSWORD= IMMICH_OIDC_CLIENT_SECRET= IMMICH_DATABASE_READONLYUSER= IMMICH_DATABASE_READONLYUSER_PASSWORD= IMMICH_API_KEY= # Immich (Service Details) END # Homarr (Service Details) BEGIN HOMARR_INIT_ENV="true" HOMARR_ADMIN_USERNAME= HOMARR_ADMIN_EMAIL_ADDRESS= HOMARR_ADMIN_PASSWORD= HOMARR_OIDC_CLIENT_SECRET= # Homarr (Service Details) END # Matomo (Service Details) BEGIN MATOMO_INIT_ENV="true" MATOMO_ADMIN_USERNAME= MATOMO_ADMIN_PASSWORD= MATOMO_ADMIN_EMAIL_ADDRESS= MATOMO_DATABASE_NAME= MATOMO_DATABASE_ROOT_PASSWORD= MATOMO_DATABASE_USER= MATOMO_DATABASE_USER_PASSWORD= MATOMO_DATABASE_READONLYUSER= MATOMO_DATABASE_READONLYUSER_PASSWORD= # Matomo (Service Details) END # Pastefy (Service Details) BEGIN PASTEFY_INIT_ENV="true" PASTEFY_ADMIN_USERNAME= PASTEFY_ADMIN_PASSWORD= PASTEFY_ADMIN_EMAIL_ADDRESS= PASTEFY_DATABASE_NAME= PASTEFY_DATABASE_ROOT_PASSWORD= PASTEFY_DATABASE_USER= PASTEFY_DATABASE_USER_PASSWORD= PASTEFY_DATABASE_READONLYUSER= PASTEFY_DATABASE_READONLYUSER_PASSWORD= # Pastefy (Service Details) END # AIStack (Service Details) BEGIN AISTACK_INIT_ENV="true" AISTACK_MINDSDB_ADMIN_USERNAME= AISTACK_MINDSDB_ADMIN_PASSWORD= AISTACK_MINDSDB_ADMIN_EMAIL_ADDRESS= AISTACK_MINDSDB_DATABASE_NAME= AISTACK_MINDSDB_DATABASE_USER= AISTACK_MINDSDB_DATABASE_USER_PASSWORD= AISTACK_LANGFUSE_ADMIN_USERNAME= AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS= AISTACK_LANGFUSE_ADMIN_PASSWORD= AISTACK_LANGFUSE_DATABASE_NAME= AISTACK_OPENWEBUI_ADMIN_USERNAME= AISTACK_OPENWEBUI_ADMIN_EMAIL_ADDRESS= AISTACK_OPENWEBUI_ADMIN_PASSWORD= AISTACK_OPENWEBUI_OIDC_CLIENT_ID= AISTACK_OPENWEBUI_OIDC_CLIENT_SECRET= AISTACK_REDIS_PASSWORD= # AIStack (Service Details) END # Pixelfed (Service Details) BEGIN PIXELFED_INIT_ENV="true" PIXELFED_DATABASE_NAME= PIXELFED_DATABASE_ROOT_PASSWORD= PIXELFED_DATABASE_USER= PIXELFED_DATABASE_USER_PASSWORD= PIXELFED_REDIS_PASSWORD= PIXELFED_APP_KEY= PIXELFED_DATABASE_READONLYUSER= PIXELFED_DATABASE_READONLYUSER_PASSWORD= # Pixelfed (Service Details) END # Yamtrack (Service Details) BEGIN YAMTRACK_INIT_ENV="true" YAMTRACK_ADMIN_USERNAME= YAMTRACK_ADMIN_PASSWORD= YAMTRACK_REDIS_PASSWORD= YAMTRACK_DATABASE_NAME= YAMTRACK_DATABASE_USER= YAMTRACK_DATABASE_USER_PASSWORD= YAMTRACK_SECRET_KEY= YAMTRACK_OIDC_CLIENT_ID= YAMTRACK_OIDC_CLIENT_SECRET= YAMTRACK_DATABASE_READONLYUSER= YAMTRACK_DATABASE_READONLYUSER_PASSWORD= # Yamtrack (Service Details) END # Servarr (Service Details) BEGIN SERVARR_INIT_ENV="true" SONARR_ADMIN_USERNAME= SONARR_ADMIN_PASSWORD= RADARR_ADMIN_USERNAME= RADARR_ADMIN_PASSWORD= LIDARR_ADMIN_USERNAME= LIDARR_ADMIN_PASSWORD= READARR_ADMIN_USERNAME= READARR_ADMIN_PASSWORD= BAZARR_ADMIN_USERNAME= BAZARR_ADMIN_PASSWORD= MYLAR3_ADMIN_USERNAME= MYLAR3_ADMIN_PASSWORD= PROWLARR_ADMIN_USERNAME= PROWLARR_ADMIN_PASSWORD= # Servarr (Service Details) END # SABnzbd (Service Details) BEGIN SABNZBD_INIT_ENV="true" SABNZBD_ADMIN_USERNAME= SABNZBD_ADMIN_PASSWORD= # SABnzbd (Service Details) END # qBittorrent (Service Details) BEGIN QBITTORRENT_INIT_ENV="true" QBITTORRENT_ADMIN_USERNAME= QBITTORRENT_ADMIN_PASSWORD= # qBittorrent (Service Details) END # Ombi (Service Details) BEGIN OMBI_INIT_ENV="true" OMBI_ADMIN_USERNAME= OMBI_ADMIN_PASSWORD= OMBI_ADMIN_EMAIL_ADDRESS= OMBI_DATABASE_NAME= OMBI_DATABASE_ROOT_PASSWORD= OMBI_DATABASE_USER= OMBI_DATABASE_USER_PASSWORD= OMBI_DATABASE_READONLYUSER= OMBI_DATABASE_READONLYUSER_PASSWORD= # Ombi (Service Details) END # MeshCentral (Service Details) BEGIN MESHCENTRAL_INIT_ENV="true" MESHCENTRAL_DATABASE_NAME= MESHCENTRAL_DATABASE_ROOT_PASSWORD= MESHCENTRAL_DATABASE_USER= MESHCENTRAL_DATABASE_USER_PASSWORD= MESHCENTRAL_DATABASE_READONLYUSER= MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD= # MeshCentral (Service Details) END # Navidrome (Service Details) BEGIN NAVIDROME_INIT_ENV="true" NAVIDROME_ADMIN_USERNAME= NAVIDROME_ADMIN_PASSWORD= NAVIDROME_ADMIN_EMAIL_ADDRESS= # Navidrome (Service Details) END # Adminer (Service Details) BEGIN ADMINER_INIT_ENV="true" ADMINER_DATABASE_NAME= ADMINER_DATABASE_ROOT_PASSWORD= ADMINER_DATABASE_USER= ADMINER_DATABASE_USER_PASSWORD= ADMINER_DATABASE_READONLYUSER= ADMINER_DATABASE_READONLYUSER_PASSWORD= # Adminer (Service Details) END # Budibase (Service Details) BEGIN BUDIBASE_INIT_ENV="true" BUDIBASE_API_ENCRYPTION_KEY= BUDIBASE_INTERNAL_API_KEY= BUDIBASE_JWT_SECRET= BUDIBASE_ADMIN_EMAIL_ADDRESS= BUDIBASE_ADMIN_PASSWORD= BUDIBASE_COUCHDB_USER= BUDIBASE_COUCHDB_PASSWORD= BUDIBASE_MINIO_ACCESS_KEY= BUDIBASE_MINIO_SECRET_KEY= BUDIBASE_REDIS_PASSWORD= BUDIBASE_DATABASE_NAME= BUDIBASE_DATABASE_ROOT_PASSWORD= BUDIBASE_DATABASE_USER= BUDIBASE_DATABASE_USER_PASSWORD= BUDIBASE_DATABASE_READONLYUSER= BUDIBASE_DATABASE_READONLYUSER_PASSWORD= # Budibase (Service Details) END # AudioBookshelf (Service Details) BEGIN AUDIOBOOKSHELF_INIT_ENV="true" AUDIOBOOKSHELF_ADMIN_USERNAME= AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS= AUDIOBOOKSHELF_ADMIN_PASSWORD= AUDIOBOOKSHELF_JWT_SECRET= AUDIOBOOKSHELF_OIDC_CLIENT_SECRET= # AudioBookshelf (Service Details) END # StandardNotes (Service Details) BEGIN STANDARDNOTES_REDIS_PASSWORD= STANDARDNOTES_DATABASE_NAME= STANDARDNOTES_DATABASE_ROOT_PASSWORD= STANDARDNOTES_DATABASE_USER= STANDARDNOTES_DATABASE_USER_PASSWORD= STANDARDNOTES_DATABASE_READONLYUSER= STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD= # StandardNotes (Service Details) END # Metabase (Service Details) BEGIN METABASE_INIT_ENV="true" METABASE_ADMIN_USERNAME= METABASE_ADMIN_EMAIL_ADDRESS= METABASE_ADMIN_PASSWORD= METABASE_DATABASE_NAME= METABASE_DATABASE_USER= METABASE_DATABASE_USER_PASSWORD= METABASE_DATABASE_READONLYUSER= METABASE_DATABASE_READONLYUSER_PASSWORD= METABASE_API_KEY= # Metabase (Service Details) END # Kanboard (Service Details) BEGIN KANBOARD_INIT_ENV="true" KANBOARD_ADMIN_USERNAME= KANBOARD_ADMIN_EMAIL_ADDRESS= KANBOARD_ADMIN_PASSWORD= KANBOARD_DATABASE_NAME= KANBOARD_DATABASE_USER= KANBOARD_DATABASE_USER_PASSWORD= KANBOARD_DATABASE_READONLYUSER= KANBOARD_DATABASE_READONLYUSER_PASSWORD= # Kanboard (Service Details) END # Wekan (Service Details) BEGIN WEKAN_OIDC_CLIENT_SECRET= WEKAN_DATABASE_NAME= WEKAN_DATABASE_USER= WEKAN_DATABASE_USER_PASSWORD= WEKAN_DATABASE_READONLYUSER= WEKAN_DATABASE_READONLYUSER_PASSWORD= # Wekan (Service Details) END # Revolt (Service Details) BEGIN REVOLT_DATABASE_NAME= REVOLT_DATABASE_USER= REVOLT_DATABASE_USER_PASSWORD= REVOLT_REDIS_PASSWORD= REVOLT_RABBITMQ_USERNAME= REVOLT_RABBITMQ_PASSWORD= REVOLT_MINIO_KEY= REVOLT_MINIO_SECRET= REVOLT_DATABASE_READONLYUSER= REVOLT_DATABASE_READONLYUSER_PASSWORD= # Revolt (Service Details) END # FrappeHR (Service Details) BEGIN FRAPPE_HR_INIT_ENV="true" FRAPPE_HR_ADMIN_USERNAME= FRAPPE_HR_ADMIN_EMAIL_ADDRESS= FRAPPE_HR_ADMIN_PASSWORD= FRAPPE_HR_DATABASE_NAME= FRAPPE_HR_DATABASE_ROOT_PASSWORD= FRAPPE_HR_DATABASE_USER= FRAPPE_HR_DATABASE_USER_PASSWORD= FRAPPE_HR_REDIS_PASSWORD= FRAPPE_HR_DATABASE_READONLYUSER= FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD= # FrappeHR (Service Details) END # MintHCM (Service Details) BEGIN MINTHCM_INIT_ENV="true" MINTHCM_ADMIN_USERNAME= MINTHCM_ADMIN_PASSWORD= MINTHCM_ADMIN_EMAIL_ADDRESS= MINTHCM_DATABASE_NAME= MINTHCM_DATABASE_ROOT_PASSWORD= MINTHCM_DATABASE_USER= MINTHCM_DATABASE_USER_PASSWORD= MINTHCM_ES_USER= MINTHCM_ES_USER_PASSWORD= MINTHCM_DATABASE_READONLYUSER= MINTHCM_DATABASE_READONLYUSER_PASSWORD= # MintHCM (Service Details) END # CloudBeaver (Service Details) BEGIN CLOUDBEAVER_INIT_ENV="true" CLOUDBEAVER_ADMIN_USERNAME= CLOUDBEAVER_ADMIN_PASSWORD= # CloudBeaver (Service Details) END # DbGate (Service Details) BEGIN DBGATE_INIT_ENV="true" DBGATE_ADMIN_USERNAME= DBGATE_ADMIN_PASSWORD= # DbGate (Service Details) END # Twenty (Service Details) BEGIN TWENTY_INIT_ENV="true" TWENTY_ADMIN_USERNAME= TWENTY_ADMIN_EMAIL_ADDRESS= TWENTY_ADMIN_PASSWORD= TWENTY_ADMIN_API_KEY= TWENTY_AKB_API_KEY= TWENTY_DATABASE_NAME= TWENTY_DATABASE_USER= TWENTY_DATABASE_USER_PASSWORD= TWENTY_REDIS_PASSWORD= TWENTY_MINIO_KEY= TWENTY_MINIO_SECRET= TWENTY_APP_SECRET= TWENTY_ENCRYPTION_KEY= TWENTY_DATABASE_READONLYUSER= TWENTY_DATABASE_READONLYUSER_PASSWORD= # Twenty (Service Details) END # Odoo (Service Details) BEGIN ODOO_INIT_ENV="true" ODOO_ADMIN_USERNAME= ODOO_ADMIN_EMAIL_ADDRESS= ODOO_ADMIN_PASSWORD= ODOO_DATABASE_NAME= ODOO_DATABASE_USER= ODOO_DATABASE_USER_PASSWORD= ODOO_DATABASE_READONLYUSER= ODOO_DATABASE_READONLYUSER_PASSWORD= # Odoo (Service Details) END # Calcom (Service Details) BEGIN CALCOM_INIT_ENV="true" CALCOM_ADMIN_USERNAME= CALCOM_ADMIN_EMAIL_ADDRESS= CALCOM_ADMIN_PASSWORD= CALCOM_DATABASE_NAME= CALCOM_DATABASE_USER= CALCOM_DATABASE_USER_PASSWORD= CALCOM_DATABASE_READONLYUSER= CALCOM_DATABASE_READONLYUSER_PASSWORD= # Calcom (Service Details) END # Rallly (Service Details) BEGIN RALLLY_INIT_ENV="true" RALLLY_ADMIN_USERNAME= RALLLY_ADMIN_EMAIL_ADDRESS= RALLLY_ADMIN_PASSWORD= RALLLY_DATABASE_NAME= RALLLY_DATABASE_USER= RALLLY_DATABASE_USER_PASSWORD= RALLLY_MINIO_KEY= RALLLY_MINIO_SECRET= RALLLY_DATABASE_READONLYUSER= RALLLY_DATABASE_READONLYUSER_PASSWORD= # Rallly (Service Details) END # EasyAppointments (Service Details) BEGIN EASYAPPOINTMENTS_INIT_ENV="true" EASYAPPOINTMENTS_ADMIN_USERNAME= EASYAPPOINTMENTS_ADMIN_PASSWORD= EASYAPPOINTMENTS_ADMIN_EMAIL_ADDRESS= EASYAPPOINTMENTS_DATABASE_NAME= EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD= EASYAPPOINTMENTS_DATABASE_USER= EASYAPPOINTMENTS_DATABASE_USER_PASSWORD= EASYAPPOINTMENTS_DATABASE_READONLYUSER= EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD= # EasyAppointments (Service Details) END # OpenProject (Service Details) BEGIN OPENPROJECT_INIT_ENV="true" OPENPROJECT_ADMIN_USERNAME= OPENPROJECT_ADMIN_EMAIL_ADDRESS= OPENPROJECT_ADMIN_PASSWORD= OPENPROJECT_DATABASE_NAME= OPENPROJECT_DATABASE_USER= OPENPROJECT_DATABASE_USER_PASSWORD= OPENPROJECT_MINIO_KEY= OPENPROJECT_MINIO_SECRET= OPENPROJECT_DATABASE_READONLYUSER= OPENPROJECT_DATABASE_READONLYUSER_PASSWORD= OPENPROJECT_SECRET_KEY_BASE= # OpenProject (Service Details) END # Zammad (Service Details) BEGIN ZAMMAD_INIT_ENV="true" ZAMMAD_ADMIN_USERNAME= ZAMMAD_ADMIN_EMAIL_ADDRESS= ZAMMAD_ADMIN_PASSWORD= ZAMMAD_DATABASE_NAME= ZAMMAD_DATABASE_ROOT_PASSWORD= ZAMMAD_DATABASE_USER= ZAMMAD_DATABASE_USER_PASSWORD= ZAMMAD_REDIS_PASSWORD= ZAMMAD_ES_USERNAME= ZAMMAD_ES_PASSWORD= ZAMMAD_MINIO_KEY= ZAMMAD_MINIO_SECRET= ZAMMAD_DEDICATED_EMAIL_USERNAME= ZAMMAD_DEDICATED_EMAIL_ADDRESS= ZAMMAD_DEDICATED_EMAIL_PASSWORD= ZAMMAD_DATABASE_READONLYUSER= ZAMMAD_DATABASE_READONLYUSER_PASSWORD= # Zammad (Service Details) END # Zulip (Service Details) BEGIN ZULIP_INIT_ENV="true" ZULIP_ADMIN_USERNAME= ZULIP_ADMIN_EMAIL_ADDRESS= ZULIP_ADMIN_PASSWORD= ZULIP_DATABASE_NAME= ZULIP_DATABASE_USER= ZULIP_DATABASE_USER_PASSWORD= ZULIP_REDIS_PASSWORD= ZULIP_RABBITMQ_USERNAME= ZULIP_RABBITMQ_PASSWORD= ZULIP_MEMCACHE_PASSWORD= ZULIP_DATABASE_READONLYUSER= ZULIP_DATABASE_READONLYUSER_PASSWORD= # Zulip (Service Details) END # KillBill (Service Details) BEGIN KILLBILL_INIT_ENV="true" KILLBILL_ADMIN_USERNAME= KILLBILL_ADMIN_EMAIL_ADDRESS= KILLBILL_ADMIN_PASSWORD= KILLBILL_DATABASE_USER= KILLBILL_DATABASE_USER_PASSWORD= KILLBILL_KB_DATABASE_NAME= KILLBILL_KAUI_DATABASE_NAME= KILLBILL_REDIS_PASSWORD= KILLBILL_API_KEY= KILLBILL_API_SECRET= KILLBILL_KB_DATABASE_READONLYUSER= KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD= KILLBILL_KAUI_DATABASE_READONLYUSER= KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD= # KillBill (Service Details) END # InvoiceShelf (Service Details) BEGIN INVOICESHELF_INIT_ENV="true" INVOICESHELF_ADMIN_USERNAME= INVOICESHELF_ADMIN_EMAIL_ADDRESS= INVOICESHELF_ADMIN_PASSWORD= INVOICESHELF_ADMIN_API_KEY= INVOICESHELF_DATABASE_NAME= INVOICESHELF_DATABASE_USER= INVOICESHELF_DATABASE_USER_PASSWORD= INVOICESHELF_DATABASE_READONLYUSER= INVOICESHELF_DATABASE_READONLYUSER_PASSWORD= # InvoiceShelf (Service Details) END # InvoiceNinja (Service Details) BEGIN INVOICENINJA_INIT_ENV="true" INVOICENINJA_ADMIN_USERNAME= INVOICENINJA_ADMIN_EMAIL_ADDRESS= INVOICENINJA_ADMIN_PASSWORD= INVOICENINJA_DATABASE_NAME= INVOICENINJA_DATABASE_ROOT_PASSWORD= INVOICENINJA_DATABASE_USER= INVOICENINJA_DATABASE_USER_PASSWORD= INVOICENINJA_REDIS_PASSWORD= INVOICENINJA_MINIO_KEY= INVOICENINJA_MINIO_SECRET= INVOICENINJA_API_SECRET= INVOICENINJA_UPDATE_SECRET= INVOICENINJA_WEBCRON_SECRET= INVOICENINJA_DATABASE_READONLYUSER= INVOICENINJA_DATABASE_READONLYUSER_PASSWORD= # InvoiceNinja (Service Details) END # Dolibarr (Service Details) BEGIN DOLIBARR_INIT_ENV="true" DOLIBARR_ADMIN_USERNAME= DOLIBARR_ADMIN_EMAIL_ADDRESS= DOLIBARR_ADMIN_PASSWORD= DOLIBARR_DATABASE_NAME= DOLIBARR_DATABASE_ROOT_PASSWORD= DOLIBARR_DATABASE_USER= DOLIBARR_DATABASE_USER_PASSWORD= DOLIBARR_REDIS_PASSWORD= DOLIBARR_CRON_SECRET= DOLIBARR_DATABASE_READONLYUSER= DOLIBARR_DATABASE_READONLYUSER_PASSWORD= DOLIBARR_INSTANCE_UNIQUE_ID= DOLIBARR_API_KEY= DOLIBARR_MCP_API_KEY= DOLIBARR_MCP_REDIS_PASSWORD= # Dolibarr (Service Details) END # n8n (Service Details) BEGIN N8N_INIT_ENV="true" N8N_ADMIN_USERNAME= N8N_ADMIN_EMAIL_ADDRESS= N8N_ADMIN_PASSWORD= N8N_DATABASE_NAME= N8N_DATABASE_USER= N8N_DATABASE_USER_PASSWORD= N8N_REDIS_PASSWORD= N8N_ENCRYPTION_KEY= N8N_DATABASE_READONLYUSER= N8N_DATABASE_READONLYUSER_PASSWORD= # n8n (Service Details) END # Automatisch (Service Details) BEGIN AUTOMATISCH_INIT_ENV="true" AUTOMATISCH_ADMIN_USERNAME= AUTOMATISCH_ADMIN_EMAIL_ADDRESS= AUTOMATISCH_ADMIN_PASSWORD= AUTOMATISCH_DATABASE_NAME= AUTOMATISCH_DATABASE_USER= AUTOMATISCH_DATABASE_USER_PASSWORD= AUTOMATISCH_REDIS_PASSWORD= AUTOMATISCH_ENCRYPTION_KEY= AUTOMATISCH_WEBHOOK_SECRET_KEY= AUTOMATISCH_APP_SECRET_KEY= AUTOMATISCH_DATABASE_READONLYUSER= AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD= # Automatisch (Service Details) END # ActivePieces (Service Details) BEGIN ACTIVEPIECES_INIT_ENV="true" ACTIVEPIECES_ADMIN_USERNAME= ACTIVEPIECES_ADMIN_EMAIL_ADDRESS= ACTIVEPIECES_ADMIN_PASSWORD= ACTIVEPIECES_DATABASE_NAME= ACTIVEPIECES_DATABASE_USER= ACTIVEPIECES_DATABASE_USER_PASSWORD= ACTIVEPIECES_REDIS_PASSWORD= ACTIVEPIECES_ENCRYPTION_KEY= ACTIVEPIECES_API_KEY= ACTIVEPIECES_JWT_SECRET= ACTIVEPIECES_DATABASE_READONLYUSER= ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD= # ActivePieces (Service Details) END # Beszel (Service Details) BEGIN BESZEL_INIT_ENV="true" BESZEL_ADMIN_USERNAME= BESZEL_ADMIN_EMAIL_ADDRESS= BESZEL_ADMIN_PASSWORD= # Beszel (Service Details) END # Taiga (Service Details) BEGIN TAIGA_INIT_ENV="true" TAIGA_ADMIN_USERNAME= TAIGA_ADMIN_EMAIL_ADDRESS= TAIGA_ADMIN_PASSWORD= TAIGA_DATABASE_NAME= TAIGA_DATABASE_USER= TAIGA_DATABASE_USER_PASSWORD= TAIGA_SECRET_KEY= TAIGA_RABBITMQ_USERNAME= TAIGA_RABBITMQ_PASSWORD= TAIGA_RABBITMQ_ERLANG_COOKIE= TAIGA_DATABASE_READONLYUSER= TAIGA_DATABASE_READONLYUSER_PASSWORD= # Taiga (Service Details) END # OpenSign (Service Details) BEGIN OPENSIGN_INIT_ENV="true" OPENSIGN_ADMIN_USERNAME= OPENSIGN_ADMIN_EMAIL_ADDRESS= OPENSIGN_ADMIN_PASSWORD= OPENSIGN_DATABASE_NAME= OPENSIGN_DATABASE_USER= OPENSIGN_DATABASE_USER_PASSWORD= OPENSIGN_MASTER_KEY= OPENSIGN_CERTIFICATE_PASS_PHRASE= OPENSIGN_DATABASE_READONLYUSER= OPENSIGN_DATABASE_READONLYUSER_PASSWORD= # OpenSign (Service Details) END # DocuSeal (Service Details) BEGIN DOCUSEAL_INIT_ENV="true" DOCUSEAL_ADMIN_USERNAME= DOCUSEAL_ADMIN_EMAIL_ADDRESS= DOCUSEAL_ADMIN_PASSWORD= DOCUSEAL_DATABASE_NAME= DOCUSEAL_DATABASE_USER= DOCUSEAL_DATABASE_USER_PASSWORD= DOCUSEAL_DATABASE_READONLYUSER= DOCUSEAL_DATABASE_READONLYUSER_PASSWORD= # DocuSeal (Service Details) END # ControlR (Service Details) BEGIN CONTROLR_INIT_ENV="true" CONTROLR_ADMIN_USERNAME= CONTROLR_ADMIN_EMAIL_ADDRESS= CONTROLR_ADMIN_PASSWORD= CONTROLR_DATABASE_NAME= CONTROLR_DATABASE_USER= CONTROLR_DATABASE_USER_PASSWORD= CONTROLR_ASPIRE_TOKEN= CONTROLR_ASPIRE_APIKEY= CONTROLR_DATABASE_READONLYUSER= CONTROLR_DATABASE_READONLYUSER_PASSWORD= # ControlR (Service Details) END # Akaunting (Service Details) BEGIN AKAUNTING_INIT_ENV="true" AKAUNTING_ADMIN_USERNAME= AKAUNTING_ADMIN_EMAIL_ADDRESS= AKAUNTING_ADMIN_PASSWORD= AKAUNTING_DATABASE_NAME= AKAUNTING_DATABASE_ROOT_PASSWORD= AKAUNTING_DATABASE_USER= AKAUNTING_DATABASE_USER_PASSWORD= AKAUNTING_DATABASE_READONLYUSER= AKAUNTING_DATABASE_READONLYUSER_PASSWORD= # Akaunting (Service Details) END # Axelor (Service Details) BEGIN AXELOR_INIT_ENV="true" AXELOR_ADMIN_USERNAME= AXELOR_ADMIN_EMAIL_ADDRESS= AXELOR_ADMIN_PASSWORD= AXELOR_DATABASE_NAME= AXELOR_DATABASE_USER= AXELOR_DATABASE_USER_PASSWORD= AXELOR_NEXTAUTH_SECRET= AXELOR_GOOVEE_ADMIN_USERNAME= AXELOR_GOOVEE_ADMIN_PASSWORD= AXELOR_DATABASE_READONLYUSER= AXELOR_DATABASE_READONLYUSER_PASSWORD= # Axelor (Service Details) END # ConvertX (Service Details) BEGIN CONVERTX_INIT_ENV="true" CONVERTX_ADMIN_USERNAME= CONVERTX_ADMIN_EMAIL_ADDRESS= CONVERTX_ADMIN_PASSWORD= CONVERTX_JWT_SECRET= # ConvertX (Service Details) END # Kopia (Service Details) BEGIN KOPIA_INIT_ENV="true" KOPIA_ADMIN_USERNAME= KOPIA_ADMIN_PASSWORD= # Kopia (Service Details) END # Ollama (Service Details) BEGIN OLLAMA_INIT_ENV="true" OLLAMA_API_KEY= # Ollama (Service Details) END # LocalAI (Service Details) BEGIN LOCALAI_INIT_ENV="true" LOCALAI_ADMIN_USERNAME= LOCALAI_ADMIN_EMAIL_ADDRESS= LOCALAI_ADMIN_PASSWORD= LOCALAI_API_KEY= LOCALAI_SSHBOX_PASSWORD= # LocalAI (Service Details) END # ComfyUI (Service Details) BEGIN COMFYUI_INIT_ENV="true" COMFYUI_API_KEY= # ComfyUI (Service Details) END # Langflow (Service Details) BEGIN LANGFLOW_INIT_ENV="true" LANGFLOW_ADMIN_USERNAME= LANGFLOW_ADMIN_EMAIL_ADDRESS= LANGFLOW_ADMIN_PASSWORD= LANGFLOW_DATABASE_NAME= LANGFLOW_DATABASE_USER= LANGFLOW_DATABASE_USER_PASSWORD= LANGFLOW_DATABASE_READONLYUSER= LANGFLOW_DATABASE_READONLYUSER_PASSWORD= LANGFLOW_API_KEY= LANGFLOW_SECRET_KEY= LANGFLOW_LANGFUSE_PUBLIC_KEY= LANGFLOW_LANGFUSE_SECRET_KEY= # Langflow (Service Details) END # AnythingLLM (Service Details) BEGIN ANYTHINGLLM_INIT_ENV="true" ANYTHINGLLM_ADMIN_USERNAME= ANYTHINGLLM_ADMIN_EMAIL_ADDRESS= ANYTHINGLLM_ADMIN_PASSWORD= ANYTHINGLLM_SIG_KEY= ANYTHINGLLM_SIG_SALT= ANYTHINGLLM_JWT_SECRET= # AnythingLLM (Service Details) END # Firecrawl (Service Details) BEGIN FIRECRAWL_INIT_ENV="true" FIRECRAWL_DATABASE_NAME= FIRECRAWL_DATABASE_USER= FIRECRAWL_DATABASE_USER_PASSWORD= FIRECRAWL_REDIS_PASSWORD= FIRECRAWL_BULL_AUTH_KEY= FIRECRAWL_API_KEY= FIRECRAWL_RABBITMQ_USERNAME= FIRECRAWL_RABBITMQ_PASSWORD= # Firecrawl (Service Details) END # LibreChat (Service Details) BEGIN LIBRECHAT_INIT_ENV="true" LIBRECHAT_ADMIN_USERNAME= LIBRECHAT_ADMIN_EMAIL_ADDRESS= LIBRECHAT_ADMIN_PASSWORD= LIBRECHAT_DATABASE_NAME= LIBRECHAT_DATABASE_USER= LIBRECHAT_DATABASE_USER_PASSWORD= LIBRECHAT_REDIS_PASSWORD= LIBRECHAT_MONGODB_DATABASE= LIBRECHAT_MONGODB_USER= LIBRECHAT_MONGODB_USER_PASSWORD= LIBRECHAT_CREDS_KEY= LIBRECHAT_CREDS_IV= LIBRECHAT_JWT_SECRET= LIBRECHAT_JWT_REFRESH_SECRET= LIBRECHAT_MEILI_MASTER_KEY= LIBRECHAT_OIDC_CLIENT_SECRET= LIBRECHAT_DATABASE_READONLYUSER= LIBRECHAT_DATABASE_READONLYUSER_PASSWORD= LIBRECHAT_MONGODB_READONLYUSER= LIBRECHAT_MONGODB_READONLYUSER_PASSWORD= # LibreChat (Service Details) END # OpenWebUI (Service Details) BEGIN OPENWEBUI_INIT_ENV="true" OPENWEBUI_ADMIN_USERNAME= OPENWEBUI_ADMIN_EMAIL_ADDRESS= OPENWEBUI_ADMIN_PASSWORD= OPENWEBUI_DATABASE_NAME= OPENWEBUI_DATABASE_USER= OPENWEBUI_DATABASE_USER_PASSWORD= OPENWEBUI_REDIS_PASSWORD= OPENWEBUI_OIDC_CLIENT_ID= OPENWEBUI_OIDC_CLIENT_SECRET= OPENWEBUI_SECRET_KEY= OPENWEBUI_DATABASE_READONLYUSER= OPENWEBUI_DATABASE_READONLYUSER_PASSWORD= OPENWEBUI_ADMIN_API_KEY= OPENWEBUI_QDRANT_API_KEY= OPENWEBUI_OPENTERMINAL_API_KEY= OPENWEBUI_MCPO_API_KEY= OPENWEBUI_PIPELINES_API_KEY= OPENWEBUI_ADMIN_UUID= OPENWEBUI_PRIMARYUSERS_UUID= # OpenWebUI (Service Details) END # Khoj (Service Details) BEGIN KHOJ_INIT_ENV="true" KHOJ_ADMIN_USERNAME= KHOJ_ADMIN_EMAIL_ADDRESS= KHOJ_ADMIN_PASSWORD= KHOJ_DATABASE_NAME= KHOJ_DATABASE_USER= KHOJ_DATABASE_USER_PASSWORD= KHOJ_DJANGO_SECRET_KEY= KHOJ_DATABASE_READONLYUSER= KHOJ_DATABASE_READONLYUSER_PASSWORD= # Khoj (Service Details) END # LobeChat (Service Details) BEGIN LOBECHAT_INIT_ENV="true" LOBECHAT_ADMIN_USERNAME= LOBECHAT_ADMIN_EMAIL_ADDRESS= LOBECHAT_ADMIN_PASSWORD= LOBECHAT_DATABASE_NAME= LOBECHAT_DATABASE_USER= LOBECHAT_DATABASE_USER_PASSWORD= LOBECHAT_REDIS_PASSWORD= LOBECHAT_NEXTAUTH_SECRET= LOBECHAT_KEYVAULTS_SECRET= LOBECHAT_MINIO_KEY= LOBECHAT_MINIO_SECRET= LOBECHAT_OIDC_CLIENT_ID= LOBECHAT_OIDC_CLIENT_SECRET= LOBECHAT_DATABASE_READONLYUSER= LOBECHAT_DATABASE_READONLYUSER_PASSWORD= # LobeChat (Service Details) END # RAGFlow (Service Details) BEGIN RAGFLOW_INIT_ENV="true" RAGFLOW_ADMIN_USERNAME= RAGFLOW_ADMIN_EMAIL_ADDRESS= RAGFLOW_ADMIN_PASSWORD= RAGFLOW_ADMIN_API_KEY= RAGFLOW_DATABASE_NAME= RAGFLOW_DATABASE_USER= RAGFLOW_DATABASE_USER_PASSWORD= RAGFLOW_DATABASE_ROOT_PASSWORD= RAGFLOW_REDIS_PASSWORD= RAGFLOW_MINIO_KEY= RAGFLOW_MINIO_SECRET= RAGFLOW_SECRET_KEY= RAGFLOW_OIDC_CLIENT_ID= RAGFLOW_OIDC_CLIENT_SECRET= RAGFLOW_MCPSERVER_API_KEY= RAGFLOW_SANDBOX_EXECUTOR_MANAGER_API_TOKEN= RAGFLOW_DATABASE_READONLYUSER= RAGFLOW_DATABASE_READONLYUSER_PASSWORD= RAGFLOW_TEAM_API_KEY= RAGFLOW_TEAM_TENANT_ID= # RAGFlow (Service Details) END # TabbyML (Service Details) BEGIN TABBYML_INIT_ENV="true" TABBYML_ADMIN_USERNAME= TABBYML_ADMIN_EMAIL_ADDRESS= TABBYML_ADMIN_PASSWORD= TABBYML_JWT_SECRET= # TabbyML (Service Details) END # DeepWikiOpen (Service Details) BEGIN DEEPWIKI_OPEN_INIT_ENV="true" DEEPWIKI_OPEN_AUTH_CODE= # DeepWikiOpen (Service Details) END # Docling (Service Details) BEGIN DOCLING_INIT_ENV="true" DOCLING_REDIS_PASSWORD= DOCLING_API_KEY= # Docling (Service Details) END # Dify (Service Details) BEGIN DIFY_INIT_ENV="true" DIFY_ADMIN_USERNAME= DIFY_ADMIN_EMAIL_ADDRESS= DIFY_ADMIN_PASSWORD= DIFY_DATABASE_NAME= DIFY_DATABASE_USER= DIFY_DATABASE_USER_PASSWORD= DIFY_REDIS_PASSWORD= DIFY_SECRET_KEY= DIFY_MINIO_KEY= DIFY_MINIO_SECRET= DIFY_QDRANT_API_KEY= DIFY_CODE_EXECUTION_API_KEY= DIFY_SANDBOX_API_KEY= DIFY_PLUGIN_DAEMON_KEY= DIFY_PLUGIN_DIFY_INNER_API_KEY= DIFY_DATABASE_READONLYUSER= DIFY_DATABASE_READONLYUSER_PASSWORD= # Dify (Service Details) END # MindsDB (Service Details) BEGIN MINDSDB_INIT_ENV="true" MINDSDB_ADMIN_USERNAME= MINDSDB_ADMIN_EMAIL_ADDRESS= MINDSDB_ADMIN_PASSWORD= MINDSDB_DATABASE_NAME= MINDSDB_DATABASE_USER= MINDSDB_DATABASE_USER_PASSWORD= MINDSDB_DATABASE_READONLYUSER= MINDSDB_DATABASE_READONLYUSER_PASSWORD= MINDSDB_REDIS_PASSWORD= # MindsDB (Service Details) END # WaterCrawl (Service Details) BEGIN WATERCRAWL_INIT_ENV="true" WATERCRAWL_ADMIN_USERNAME= WATERCRAWL_ADMIN_EMAIL_ADDRESS= WATERCRAWL_ADMIN_PASSWORD= WATERCRAWL_DATABASE_NAME= WATERCRAWL_DATABASE_USER= WATERCRAWL_DATABASE_USER_PASSWORD= WATERCRAWL_DATABASE_READONLYUSER= WATERCRAWL_DATABASE_READONLYUSER_PASSWORD= WATERCRAWL_REDIS_PASSWORD= WATERCRAWL_SECRET_KEY= WATERCRAWL_API_ENCRYPTION_KEY= WATERCRAWL_PLAYWRIGHT_API_KEY= WATERCRAWL_MINIO_KEY= WATERCRAWL_MINIO_SECRET= # WaterCrawl (Service Details) END # Flowise (Service Details) BEGIN FLOWISE_INIT_ENV="true" FLOWISE_ADMIN_USERNAME= FLOWISE_ADMIN_EMAIL_ADDRESS= FLOWISE_ADMIN_PASSWORD= FLOWISE_DATABASE_NAME= FLOWISE_DATABASE_USER= FLOWISE_DATABASE_USER_PASSWORD= FLOWISE_DATABASE_READONLYUSER= FLOWISE_DATABASE_READONLYUSER_PASSWORD= FLOWISE_REDIS_PASSWORD= FLOWISE_JWT_SECRET= FLOWISE_JWT_REFRESH_SECRET= FLOWISE_EXPRESS_SESSION_SECRET= FLOWISE_TOKEN_HASH_SECRET= FLOWISE_MINIO_KEY= FLOWISE_MINIO_SECRET= # Flowise (Service Details) END # SurfSense (Service Details) BEGIN SURFSENSE_INIT_ENV="true" SURFSENSE_ADMIN_USERNAME= SURFSENSE_ADMIN_EMAIL_ADDRESS= SURFSENSE_ADMIN_PASSWORD= SURFSENSE_SECRET_KEY= # SurfSense (Service Details) END # NocoDB (Service Details) BEGIN NOCODB_INIT_ENV="true" NOCODB_ADMIN_USERNAME= NOCODB_ADMIN_EMAIL_ADDRESS= NOCODB_ADMIN_PASSWORD= NOCODB_DATABASE_NAME= NOCODB_DATABASE_USER= NOCODB_DATABASE_USER_PASSWORD= NOCODB_DATABASE_READONLYUSER= NOCODB_DATABASE_READONLYUSER_PASSWORD= NOCODB_REDIS_PASSWORD= NOCODB_AUTH_JWT_SECRET= NOCODB_MINIO_KEY= NOCODB_MINIO_SECRET= # NocoDB (Service Details) END # Ente (Service Details) BEGIN ENTE_INIT_ENV="true" ENTE_ADMIN_USERNAME= ENTE_ADMIN_EMAIL_ADDRESS= ENTE_ADMIN_PASSWORD= ENTE_DATABASE_NAME= ENTE_DATABASE_USER= ENTE_DATABASE_USER_PASSWORD= ENTE_DATABASE_READONLYUSER= ENTE_DATABASE_READONLYUSER_PASSWORD= ENTE_MINIO_KEY= ENTE_MINIO_SECRET= ENTE_MUSEUM_KEY= ENTE_MUSEUM_HASH= ENTE_MUSEUM_JWT_SECRET= # Ente (Service Details) END # Morphic (Service Details) BEGIN MORPHIC_INIT_ENV="true" MORPHIC_ADMIN_USERNAME= MORPHIC_ADMIN_EMAIL_ADDRESS= MORPHIC_ADMIN_PASSWORD= MORPHIC_DATABASE_NAME= MORPHIC_DATABASE_USER= MORPHIC_DATABASE_USER_PASSWORD= MORPHIC_DATABASE_READONLYUSER= MORPHIC_DATABASE_READONLYUSER_PASSWORD= MORPHIC_REDIS_PASSWORD= MORPHIC_SUPABASE_DASHBOARD_USERNAME= MORPHIC_SUPABASE_DASHBOARD_PASSWORD= MORPHIC_SUPABASE_ANON_KEY= MORPHIC_SUPABASE_JWT_SECRET= MORPHIC_SUPABASE_DB_ENC_KEY= MORPHIC_SUPABASE_PG_META_CRYPTO_KEY= MORPHIC_SUPABASE_SECRET_KEY_BASE= MORPHIC_SUPABASE_SERVICE_ROLE_KEY= MORPHIC_SUPABASE_VAULT_ENC_KEY= MORPHIC_SUPABASE_LOGFLARE_PRIVATE_ACCESS_TOKEN= MORPHIC_SUPABASE_LOGFLARE_PUBLIC_ACCESS_TOKEN= # Morphic (Service Details) END # OpenNotebook (Service Details) BEGIN OPENNOTEBOOK_INIT_ENV="true" OPENNOTEBOOK_ADMIN_USERNAME= OPENNOTEBOOK_ADMIN_EMAIL_ADDRESS= OPENNOTEBOOK_ADMIN_PASSWORD= OPENNOTEBOOK_SURREALDB_NAME= OPENNOTEBOOK_SURREALDB_USER= OPENNOTEBOOK_SURREALDB_PASSWORD= OPENNOTEBOOK_ENCRYPTION_KEY= # OpenNotebook (Service Details) END # Appsmith (Service Details) BEGIN APPSMITH_INIT_ENV="true" APPSMITH_ADMIN_USERNAME= APPSMITH_ADMIN_EMAIL_ADDRESS= APPSMITH_ADMIN_PASSWORD= APPSMITH_SUPERVISOR_USER= APPSMITH_SUPERVISOR_PASSWORD= # Appsmith (Service Details) END # Trilium (Service Details) BEGIN TRILIUM_INIT_ENV="true" TRILIUM_ADMIN_USERNAME= TRILIUM_ADMIN_PASSWORD= TRILIUM_OIDC_CLIENT_ID= TRILIUM_OIDC_CLIENT_SECRET= # Trilium (Service Details) END # DocsGPT (Service Details) BEGIN DOCSGPT_INIT_ENV="true" DOCSGPT_ADMIN_USERNAME= DOCSGPT_ADMIN_EMAIL_ADDRESS= DOCSGPT_ADMIN_PASSWORD= DOCSGPT_DATABASE_NAME= DOCSGPT_DATABASE_USER= DOCSGPT_DATABASE_USER_PASSWORD= DOCSGPT_DATABASE_READONLYUSER= DOCSGPT_DATABASE_READONLYUSER_PASSWORD= DOCSGPT_REDIS_PASSWORD= DOCSGPT_API_KEY= DOCSGPT_JWT_SECRET= # DocsGPT (Service Details) END # Memos (Service Details) BEGIN MEMOS_INIT_ENV="true" MEMOS_ADMIN_USERNAME= MEMOS_ADMIN_EMAIL_ADDRESS= MEMOS_ADMIN_PASSWORD= MEMOS_DATABASE_NAME= MEMOS_DATABASE_USER= MEMOS_DATABASE_USER_PASSWORD= MEMOS_DATABASE_READONLYUSER= MEMOS_DATABASE_READONLYUSER_PASSWORD= MEMOS_OIDC_CLIENT_ID= MEMOS_OIDC_CLIENT_SECRET= # Memos (Service Details) END # SillyTavern (Service Details) BEGIN SILLYTAVERN_INIT_ENV="true" # SillyTavern (Service Details) END # Lemonade (Service Details) BEGIN LEMONADE_INIT_ENV="true" LEMONADE_API_KEY= # Lemonade (Service Details) END # Speakr (Service Details) BEGIN SPEAKR_INIT_ENV="true" SPEAKR_ADMIN_USERNAME= SPEAKR_ADMIN_EMAIL_ADDRESS= SPEAKR_ADMIN_PASSWORD= SPEAKR_DATABASE_NAME= SPEAKR_DATABASE_USER= SPEAKR_DATABASE_USER_PASSWORD= SPEAKR_DATABASE_READONLYUSER= SPEAKR_DATABASE_READONLYUSER_PASSWORD= SPEAKR_OIDC_CLIENT_ID= SPEAKR_OIDC_CLIENT_SECRET= # Speakr (Service Details) END # InsanelyFastWhisper (Service Details) BEGIN INSANELYFASTWHISPER_INIT_ENV="true" INSANELYFASTWHISPER_API_KEY= # InsanelyFastWhisper (Service Details) END # IVBox (Service Details) BEGIN IVBOX_INIT_ENV="true" # IVBox (Service Details) END # Monica (Service Details) BEGIN MONICA_INIT_ENV="true" MONICA_ADMIN_USERNAME= MONICA_ADMIN_EMAIL_ADDRESS= MONICA_ADMIN_PASSWORD= MONICA_DATABASE_NAME= MONICA_DATABASE_ROOT_PASSWORD= MONICA_DATABASE_USER= MONICA_DATABASE_USER_PASSWORD= MONICA_DATABASE_READONLYUSER= MONICA_DATABASE_READONLYUSER_PASSWORD= MONICA_REDIS_PASSWORD= MONICA_APP_KEY= MONICA_MEILISEARCH_KEY= MONICA_MEMCACHE_PASSWORD= # Monica (Service Details) END # AFFiNE (Service Details) BEGIN AFFINE_INIT_ENV="true" AFFINE_ADMIN_USERNAME= AFFINE_ADMIN_EMAIL_ADDRESS= AFFINE_ADMIN_PASSWORD= AFFINE_DATABASE_NAME= AFFINE_DATABASE_USER= AFFINE_DATABASE_USER_PASSWORD= AFFINE_DATABASE_READONLYUSER= AFFINE_DATABASE_READONLYUSER_PASSWORD= AFFINE_REDIS_PASSWORD= AFFINE_OIDC_CLIENT_ID= AFFINE_OIDC_CLIENT_SECRET= # AFFiNE (Service Details) END # Joplin (Service Details) BEGIN JOPLIN_INIT_ENV="true" JOPLIN_ADMIN_USERNAME= JOPLIN_ADMIN_EMAIL_ADDRESS= JOPLIN_ADMIN_PASSWORD= JOPLIN_DATABASE_NAME= JOPLIN_DATABASE_USER= JOPLIN_DATABASE_USER_PASSWORD= JOPLIN_DATABASE_READONLYUSER= JOPLIN_DATABASE_READONLYUSER_PASSWORD= JOPLIN_TRANSCRIBE_DATABASE_NAME= JOPLIN_TRANSCRIBE_DATABASE_USER= JOPLIN_TRANSCRIBE_DATABASE_USER_PASSWORD= JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER= JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER_PASSWORD= JOPLIN_TRANSCRIBE_API_KEY= # Joplin (Service Details) END # Superset (Service Details) BEGIN SUPERSET_INIT_ENV="true" SUPERSET_ADMIN_USERNAME= SUPERSET_ADMIN_EMAIL_ADDRESS= SUPERSET_ADMIN_PASSWORD= SUPERSET_DATABASE_NAME= SUPERSET_DATABASE_USER= SUPERSET_DATABASE_USER_PASSWORD= SUPERSET_DATABASE_READONLYUSER= SUPERSET_DATABASE_READONLYUSER_PASSWORD= SUPERSET_SECRET_KEY= SUPERSET_JWT_SECRET= # Superset (Service Details) END # Kokoro (Service Details) BEGIN KOKORO_INIT_ENV="true" KOKORO_API_KEY= # Kokoro (Service Details) END # Chatterbox (Service Details) BEGIN CHATTERBOX_INIT_ENV="true" CHATTERBOX_API_KEY= # Chatterbox (Service Details) END # LiteLLM (Service Details) BEGIN LITELLM_INIT_ENV="true" LITELLM_ADMIN_USERNAME= LITELLM_ADMIN_EMAIL_ADDRESS= LITELLM_ADMIN_PASSWORD= LITELLM_DATABASE_NAME= LITELLM_DATABASE_USER= LITELLM_DATABASE_USER_PASSWORD= LITELLM_DATABASE_READONLYUSER= LITELLM_DATABASE_READONLYUSER_PASSWORD= LITELLM_REDIS_PASSWORD= LITELLM_MASTER_KEY= LITELLM_OIDC_CLIENT_ID= LITELLM_OIDC_CLIENT_SECRET= LITELLM_SALT_KEY= LITELLM_AUTH_KEY= # LiteLLM (Service Details) END # Langfuse (Service Details) BEGIN LANGFUSE_INIT_ENV="true" LANGFUSE_ADMIN_USERNAME= LANGFUSE_ADMIN_EMAIL_ADDRESS= LANGFUSE_ADMIN_PASSWORD= LANGFUSE_DATABASE_NAME= LANGFUSE_DATABASE_USER= LANGFUSE_DATABASE_USER_PASSWORD= LANGFUSE_DATABASE_READONLYUSER= LANGFUSE_DATABASE_READONLYUSER_PASSWORD= LANGFUSE_REDIS_PASSWORD= LANGFUSE_PUBLIC_KEY= LANGFUSE_SECRET_KEY= LANGFUSE_INIT_PROJECT_ID= LANGFUSE_MINIO_KEY= LANGFUSE_MINIO_SECRET= LANGFUSE_OIDC_CLIENT_ID= LANGFUSE_OIDC_CLIENT_SECRET= LANGFUSE_SALT= LANGFUSE_ENCRYPTION_KEY= LANGFUSE_NEXTAUTH_SECRET= LANGFUSE_CLICKHOUSE_DB= LANGFUSE_CLICKHOUSE_USER= LANGFUSE_CLICKHOUSE_PASSWORD= # Langfuse (Service Details) END # Skyvern (Service Details) BEGIN SKYVERN_INIT_ENV="true" SKYVERN_ADMIN_USERNAME= SKYVERN_ADMIN_EMAIL_ADDRESS= SKYVERN_ADMIN_PASSWORD= SKYVERN_DATABASE_NAME= SKYVERN_DATABASE_USER= SKYVERN_DATABASE_USER_PASSWORD= SKYVERN_DATABASE_READONLYUSER= SKYVERN_DATABASE_READONLYUSER_PASSWORD= SKYVERN_API_KEY= # Skyvern (Service Details) END # Wger (Service Details) BEGIN WGER_INIT_ENV="true" WGER_ADMIN_USERNAME= WGER_ADMIN_EMAIL_ADDRESS= WGER_ADMIN_PASSWORD= WGER_DATABASE_NAME= WGER_DATABASE_USER= WGER_DATABASE_USER_PASSWORD= WGER_DATABASE_READONLYUSER= WGER_DATABASE_READONLYUSER_PASSWORD= WGER_REDIS_PASSWORD= WGER_SECRET_KEY= WGER_SIGNING_KEY= WGER_CELERY_FLOWER_PASSWORD= # Wger (Service Details) END # WorkoutCool (Service Details) BEGIN WORKOUTCOOL_INIT_ENV="true" WORKOUTCOOL_ADMIN_USERNAME= WORKOUTCOOL_ADMIN_EMAIL_ADDRESS= WORKOUTCOOL_ADMIN_PASSWORD= WORKOUTCOOL_DATABASE_NAME= WORKOUTCOOL_DATABASE_USER= WORKOUTCOOL_DATABASE_USER_PASSWORD= WORKOUTCOOL_DATABASE_READONLYUSER= WORKOUTCOOL_DATABASE_READONLYUSER_PASSWORD= WORKOUTCOOL_BETTER_AUTH_SECRET= # WorkoutCool (Service Details) END # OpenRag (Service Details) BEGIN OPENRAG_INIT_ENV="true" OPENRAG_ADMIN_USERNAME= OPENRAG_ADMIN_EMAIL_ADDRESS= OPENRAG_ADMIN_PASSWORD= OPENRAG_OPENSEARCH_USERNAME= OPENRAG_OPENSEARCH_PASSWORD= OPENRAG_LANGFLOW_ADMIN_USERNAME= OPENRAG_LANGFLOW_ADMIN_PASSWORD= OPENRAG_LANGFLOW_API_KEY= OPENRAG_LANGFLOW_SECRET_KEY= OPENRAG_LANGFUSE_SECRET_KEY= OPENRAG_LANGFUSE_PUBLIC_KEY= # OpenRag (Service Details) END # Voicebox (Service Details) BEGIN VOICEBOX_INIT_ENV="true" VOICEBOX_ADMIN_USERNAME= VOICEBOX_ADMIN_EMAIL_ADDRESS= VOICEBOX_ADMIN_PASSWORD= # Voicebox (Service Details) END # OpenCode (Service Details) BEGIN OPENCODE_INIT_ENV="true" OPENCODE_ADMIN_USERNAME= OPENCODE_ADMIN_PASSWORD= OPENCODE_CODESERVER_USERNAME= OPENCODE_CODESERVER_PASSWORD= # OpenCode (Service Details) END # OpenSkills (Service Details) BEGIN OPENSKILLS_INIT_ENV="true" OPENSKILLS_API_KEY= # OpenSkills (Service Details) END # EmailClassifierAI (Service Details) BEGIN EMAILCLASSIFIERAI_INIT_ENV="true" EMAILCLASSIFIERAI_ADMIN_USERNAME= EMAILCLASSIFIERAI_ADMIN_PASSWORD= EMAILCLASSIFIERAI_REDIS_PASSWORD= EMAILCLASSIFIERAI_FLASK_SECRET= # EmailClassifierAI (Service Details) END # HermesAgent (Service Details) BEGIN HERMES_AGENT_INIT_ENV=true HERMES_AGENT_ADMIN_USERNAME= HERMES_AGENT_ADMIN_PASSWORD= HERMES_AGENT_SUDO_PASSWORD= HERMES_AGENT_API_KEY= HERMES_AGENT_WEBUI_PASSWORD= # HermesAgent (Service Details) END # AutoKB (Service Details) BEGIN AUTOKB_INIT_ENV=true AUTOKB_ADMIN_USERNAME= AUTOKB_ADMIN_PASSWORD= AUTOKB_DATABASE_NAME= AUTOKB_DATABASE_USER= AUTOKB_DATABASE_USER_PASSWORD= AUTOKB_DATABASE_READONLYUSER= AUTOKB_DATABASE_READONLYUSER_PASSWORD= AUTOKB_REDIS_PASSWORD= AUTOKB_API_KEY= AUTOKB_BACKEND_API_KEY= AUTOKB_WEBHOOK_API_KEY= AUTOKB_ENCRYPTION_KEY= AUTOKB_ENCRYPTION_SALT= AUTOKB_SHARED_OWUI_TARGET_ID= AUTOKB_SHARED_RAGFLOW_TARGET_ID= # AutoKB (Service Details) END # SuiteCRM (Service Details) BEGIN SUITECRM_INIT_ENV=true SUITECRM_ADMIN_USERNAME= SUITECRM_ADMIN_EMAIL_ADDRESS= SUITECRM_ADMIN_PASSWORD= SUITECRM_DATABASE_NAME= SUITECRM_DATABASE_ROOT_PASSWORD= SUITECRM_DATABASE_USER= SUITECRM_DATABASE_USER_PASSWORD= SUITECRM_DATABASE_READONLYUSER= SUITECRM_DATABASE_READONLYUSER_PASSWORD= SUITECRM_REDIS_PASSWORD= SUITECRM_APP_SECRET= # SuiteCRM (Service Details) END # HedgeDoc (Service Details) BEGIN HEDGEDOC_INIT_ENV=true HEDGEDOC_ADMIN_USERNAME= HEDGEDOC_ADMIN_EMAIL_ADDRESS= HEDGEDOC_ADMIN_PASSWORD= HEDGEDOC_ADMIN_API_KEY= HEDGEDOC_DATABASE_NAME= HEDGEDOC_DATABASE_USER= HEDGEDOC_DATABASE_USER_PASSWORD= HEDGEDOC_DATABASE_READONLYUSER= HEDGEDOC_DATABASE_READONLYUSER_PASSWORD= HEDGEDOC_SESSION_SECRET= # HedgeDoc (Service Details) END # Presenton (Service Details) BEGIN PRESENTON_INIT_ENV=true PRESENTON_ADMIN_USERNAME= PRESENTON_ADMIN_EMAIL_ADDRESS= PRESENTON_ADMIN_PASSWORD= PRESENTON_ADMIN_API_KEY= # Presenton (Service Details) END # BasicMemory (Service Details) BEGIN BASICMEMORY_INIT_ENV=true BASICMEMORY_ADMIN_USERNAME= BASICMEMORY_ADMIN_EMAIL_ADDRESS= BASICMEMORY_ADMIN_PASSWORD= BASICMEMORY_DATABASE_NAME= BASICMEMORY_DATABASE_USER= BASICMEMORY_DATABASE_USER_PASSWORD= BASICMEMORY_DATABASE_READONLYUSER= BASICMEMORY_DATABASE_READONLYUSER_PASSWORD= # BasicMemory (Service Details) END # Cognee (Service Details) BEGIN COGNEE_INIT_ENV=true COGNEE_ADMIN_USERNAME= COGNEE_ADMIN_EMAIL_ADDRESS= COGNEE_ADMIN_PASSWORD= COGNEE_DATABASE_NAME= COGNEE_DATABASE_USER= COGNEE_DATABASE_USER_PASSWORD= COGNEE_DATABASE_READONLYUSER= COGNEE_DATABASE_READONLYUSER_PASSWORD= COGNEE_REDIS_PASSWORD= COGNEE_JWT_SECRET= COGNEE_VERIFICATION_TOKEN_SECRET= COGNEE_RESET_PASSWORD_TOKEN_SECRET= # Cognee (Service Details) END # LightRAG (Service Details) BEGIN LIGHTRAG_INIT_ENV=true LIGHTRAG_ADMIN_USERNAME= LIGHTRAG_ADMIN_EMAIL_ADDRESS= LIGHTRAG_ADMIN_PASSWORD= LIGHTRAG_DATABASE_NAME= LIGHTRAG_DATABASE_USER= LIGHTRAG_DATABASE_USER_PASSWORD= LIGHTRAG_DATABASE_READONLYUSER= LIGHTRAG_DATABASE_READONLYUSER_PASSWORD= LIGHTRAG_TOKEN_SECRET= LIGHTRAG_API_KEY= LIGHTRAG_QDRANT_API_KEY= LIGHTRAG_NEO4J_DATABASE= LIGHTRAG_NEO4J_USER= LIGHTRAG_NEO4J_PASSWORD= # LightRAG (Service Details) END # OpenSERP (Service Details) BEGIN OPENSERP_INIT_ENV=true # OpenSERP (Service Details) END # Suricata (Service Details) BEGIN SURICATA_INIT_ENV=true SURICATA_ADMIN_USERNAME= SURICATA_ADMIN_PASSWORD= # Suricata (Service Details) END # CrowdSec (Service Details) BEGIN CROWDSEC_INIT_ENV=true CROWDSEC_ADMIN_USERNAME= CROWDSEC_ADMIN_PASSWORD= CROWDSEC_LAPI_USERNAME= CROWDSEC_LAPI_PASSWORD= CROWDSEC_API_KEY= # CrowdSec (Service Details) END # fastCRW (Service Details) BEGIN FASTCRW_INIT_ENV=true FASTCRW_API_KEY= # fastCRW (Service Details) END # WhiteboardRT (Service Details) BEGIN WHITEBOARDRT_INIT_ENV=true WHITEBOARDRT_REDIS_PASSWORD= # WhiteboardRT (Service Details) END # mdCollab (Service Details) BEGIN MDCOLLAB_INIT_ENV=true MDCOLLAB_ADMIN_USERNAME= MDCOLLAB_ADMIN_EMAIL_ADDRESS= MDCOLLAB_ADMIN_PASSWORD= MDCOLLAB_DATABASE_NAME= MDCOLLAB_DATABASE_USER= MDCOLLAB_DATABASE_USER_PASSWORD= MDCOLLAB_DATABASE_READONLYUSER= MDCOLLAB_DATABASE_READONLYUSER_PASSWORD= MDCOLLAB_SESSION_SECRET= MDCOLLAB_ADMIN_API_KEY= # mdCollab (Service Details) END # HaloSpeak (Service Details) BEGIN HALOSPEAK_INIT_ENV=true HALOSPEAK_API_KEY= # HaloSpeak (Service Details) END # Service Details END EOFCF } function outputPlainTextRootConfig() { sudo tee $HSHQ_PLAINTEXT_ROOT_CONFIG >/dev/null </dev/null < $HSHQ_SECRETS_DIR/smtp_password.txt chmod 0400 $HSHQ_SECRETS_DIR/smtp_password.txt fi if [ -z "$EMAIL_SMTP_EMAIL_ADDRESS" ]; then EMAIL_SMTP_EMAIL_ADDRESS="$EMAIL_SMTP_USERNAME@$HOMESERVER_DOMAIN" updateConfigVar EMAIL_SMTP_EMAIL_ADDRESS $EMAIL_SMTP_EMAIL_ADDRESS rm -f $HSHQ_SECRETS_DIR/smtp_username.txt echo $EMAIL_SMTP_EMAIL_ADDRESS > $HSHQ_SECRETS_DIR/smtp_username.txt chmod 0400 $HSHQ_SECRETS_DIR/smtp_username.txt fi if [ -z "$EMAIL_JOINT_USERNAME" ]; then EMAIL_JOINT_USERNAME=info updateConfigVar EMAIL_JOINT_USERNAME $EMAIL_JOINT_USERNAME fi if [ -z "$EMAIL_JOINT_PASSWORD" ]; then EMAIL_JOINT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar EMAIL_JOINT_PASSWORD $EMAIL_JOINT_PASSWORD fi if [ -z "$EMAIL_JOINT_EMAIL_ADDRESS" ]; then EMAIL_JOINT_EMAIL_ADDRESS="$EMAIL_JOINT_USERNAME@$HOMESERVER_DOMAIN" updateConfigVar EMAIL_JOINT_EMAIL_ADDRESS $EMAIL_JOINT_EMAIL_ADDRESS fi if [ -z "$MAILU_API_TOKEN" ]; then MAILU_API_TOKEN=$(pwgen -c -n 32 1) MAILU_API_TOKEN="${MAILU_API_TOKEN^^}" updateConfigVar MAILU_API_TOKEN $MAILU_API_TOKEN fi if [ -z "$CERTS_EMAIL_ADDRESS" ]; then CERTS_EMAIL_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS updatePlaintextRootConfigVar CERTS_EMAIL_ADDRESS $CERTS_EMAIL_ADDRESS fi if [ -z "$LDAP_ACCOUNT_REQUESTS_EMAIL" ]; then LDAP_ACCOUNT_REQUESTS_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS updateConfigVar LDAP_ACCOUNT_REQUESTS_EMAIL $LDAP_ACCOUNT_REQUESTS_EMAIL fi if [ -z "$NEXTCLOUD_EMAIL_FROM_ADDRESS" ]; then NEXTCLOUD_EMAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS updateConfigVar NEXTCLOUD_EMAIL_FROM_ADDRESS $NEXTCLOUD_EMAIL_FROM_ADDRESS fi if [ -z "$PORTAINER_ADMIN_USERNAME" ]; then PORTAINER_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_portainer" updatePlaintextRootConfigVar PORTAINER_ADMIN_USERNAME $PORTAINER_ADMIN_USERNAME fi if [ -z "$PORTAINER_ADMIN_PASSWORD" ]; then PORTAINER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updatePlaintextRootConfigVar PORTAINER_ADMIN_PASSWORD $PORTAINER_ADMIN_PASSWORD fi if [ -z "$ADGUARD_ADMIN_USERNAME" ]; then ADGUARD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_adguard" updatePlaintextRootConfigVar ADGUARD_ADMIN_USERNAME $ADGUARD_ADMIN_USERNAME fi if [ -z "$ADGUARD_ADMIN_PASSWORD" ]; then ADGUARD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updatePlaintextRootConfigVar ADGUARD_ADMIN_PASSWORD $ADGUARD_ADMIN_PASSWORD fi if [ -z "$SCRIPTSERVER_ADMIN_USERNAME" ]; then SCRIPTSERVER_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_scriptserver" updateConfigVar SCRIPTSERVER_ADMIN_USERNAME $SCRIPTSERVER_ADMIN_USERNAME fi if [ -z "$SCRIPTSERVER_ADMIN_PASSWORD" ]; then SCRIPTSERVER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SCRIPTSERVER_ADMIN_PASSWORD $SCRIPTSERVER_ADMIN_PASSWORD fi if [ -z "$GRAFANA_ADMIN_USERNAME" ]; then GRAFANA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_grafana" updateConfigVar GRAFANA_ADMIN_USERNAME $GRAFANA_ADMIN_USERNAME fi if [ -z "$GRAFANA_ADMIN_PASSWORD" ]; then GRAFANA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GRAFANA_ADMIN_PASSWORD $GRAFANA_ADMIN_PASSWORD fi if [ -z "$INFLUXDB_ADMIN_USERNAME" ]; then INFLUXDB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_influxdb" updateConfigVar INFLUXDB_ADMIN_USERNAME $INFLUXDB_ADMIN_USERNAME fi if [ -z "$INFLUXDB_ADMIN_PASSWORD" ]; then INFLUXDB_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INFLUXDB_ADMIN_PASSWORD $INFLUXDB_ADMIN_PASSWORD fi if [ -z "$INFLUXDB_ORG" ]; then INFLUXDB_ORG=$HOMESERVER_DOMAIN updateConfigVar INFLUXDB_ORG $INFLUXDB_ORG fi if [ -z "$INFLUXDB_TOKEN" ]; then INFLUXDB_TOKEN=$(pwgen -c -n 64 1) updateConfigVar INFLUXDB_TOKEN $INFLUXDB_TOKEN fi if [ -z "$INFLUXDB_HA_BUCKET" ]; then INFLUXDB_HA_BUCKET="home_assistant" updateConfigVar INFLUXDB_HA_BUCKET $INFLUXDB_HA_BUCKET fi if [ -z "$ALLOY_ADMIN_USERNAME" ]; then ALLOY_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_alloy" updateConfigVar ALLOY_ADMIN_USERNAME $ALLOY_ADMIN_USERNAME fi if [ -z "$ALLOY_ADMIN_PASSWORD" ]; then ALLOY_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ALLOY_ADMIN_PASSWORD $ALLOY_ADMIN_PASSWORD fi if [ -z "$LDAP_BASE_DN" ]; then LDAP_BASE_DN=$(echo "dc="$(echo $HOMESERVER_DOMAIN | sed 's/\./,dc=/g')) updateConfigVar LDAP_BASE_DN $LDAP_BASE_DN fi if [ -z "$LDAP_ADMIN_BIND_DN" ]; then LDAP_ADMIN_BIND_DN="cn=admin,$LDAP_BASE_DN" updateConfigVar LDAP_ADMIN_BIND_DN $LDAP_ADMIN_BIND_DN fi if [ -z "$LDAP_ADMIN_BIND_PASSWORD" ]; then LDAP_ADMIN_BIND_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LDAP_ADMIN_BIND_PASSWORD $LDAP_ADMIN_BIND_PASSWORD fi if [ -z "$LDAP_ADMIN_USER_USERNAME" ]; then LDAP_ADMIN_USER_USERNAME=$ADMIN_USERNAME_BASE updateConfigVar LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_USERNAME fi if [ -z "$LDAP_ADMIN_USER_PASSWORD" ]; then LDAP_ADMIN_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LDAP_ADMIN_USER_PASSWORD $LDAP_ADMIN_USER_PASSWORD fi if [ -z "$WAZUH_API_USERNAME" ]; then WAZUH_API_USERNAME=$ADMIN_USERNAME_BASE"_wazuh_api" updateConfigVar WAZUH_API_USERNAME $WAZUH_API_USERNAME fi if [ -z "$WAZUH_API_PASSWORD" ]; then WAZUH_API_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_API_PASSWORD $WAZUH_API_PASSWORD fi if [ -z "$WAZUH_USERS_DASHBOARD_PASSWORD" ]; then WAZUH_USERS_DASHBOARD_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_USERS_DASHBOARD_PASSWORD $WAZUH_USERS_DASHBOARD_PASSWORD fi if [ -z "$WAZUH_USERS_ADMIN_USERNAME" ]; then WAZUH_USERS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_wazuh" updateConfigVar WAZUH_USERS_ADMIN_USERNAME $WAZUH_USERS_ADMIN_USERNAME fi if [ -z "$WAZUH_USERS_ADMIN_PASSWORD" ]; then WAZUH_USERS_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_USERS_ADMIN_PASSWORD $WAZUH_USERS_ADMIN_PASSWORD fi if [ -z "$WAZUH_USERS_KIBANARO_PASSWORD" ]; then WAZUH_USERS_KIBANARO_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_USERS_KIBANARO_PASSWORD $WAZUH_USERS_KIBANARO_PASSWORD fi if [ -z "$WAZUH_USERS_LOGSTASH_PASSWORD" ]; then WAZUH_USERS_LOGSTASH_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_USERS_LOGSTASH_PASSWORD $WAZUH_USERS_LOGSTASH_PASSWORD fi if [ -z "$WAZUH_USERS_READALL_PASSWORD" ]; then WAZUH_USERS_READALL_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_USERS_READALL_PASSWORD $WAZUH_USERS_READALL_PASSWORD fi if [ -z "$WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD" ]; then WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD $WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD fi if [ -z "$WAZUH_MANAGER_AUTH_PASSWORD" ]; then WAZUH_MANAGER_AUTH_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WAZUH_MANAGER_AUTH_PASSWORD $WAZUH_MANAGER_AUTH_PASSWORD fi if [ -z "$COLLABORA_ADMIN_USERNAME" ]; then COLLABORA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_collabora" updateConfigVar COLLABORA_ADMIN_USERNAME $COLLABORA_ADMIN_USERNAME fi if [ -z "$COLLABORA_ADMIN_PASSWORD" ]; then COLLABORA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar COLLABORA_ADMIN_PASSWORD $COLLABORA_ADMIN_PASSWORD fi if [ -z "$NEXTCLOUD_ADMIN_USERNAME" ]; then NEXTCLOUD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_nextcloud" updateConfigVar NEXTCLOUD_ADMIN_USERNAME $NEXTCLOUD_ADMIN_USERNAME fi if [ -z "$NEXTCLOUD_ADMIN_EMAIL_ADDRESS" ]; then NEXTCLOUD_ADMIN_EMAIL_ADDRESS=$NEXTCLOUD_ADMIN_USERNAME"@"$HOMESERVER_DOMAIN updateConfigVar NEXTCLOUD_ADMIN_EMAIL_ADDRESS $NEXTCLOUD_ADMIN_EMAIL_ADDRESS fi if [ -z "$NEXTCLOUD_ADMIN_PASSWORD" ]; then NEXTCLOUD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NEXTCLOUD_ADMIN_PASSWORD $NEXTCLOUD_ADMIN_PASSWORD fi if [ -z "$NEXTCLOUD_DATABASE_NAME" ]; then NEXTCLOUD_DATABASE_NAME="nextclouddb" updateConfigVar NEXTCLOUD_DATABASE_NAME $NEXTCLOUD_DATABASE_NAME fi if [ -z "$NEXTCLOUD_DATABASE_USER" ]; then NEXTCLOUD_DATABASE_USER="nextcloud-user" updateConfigVar NEXTCLOUD_DATABASE_USER $NEXTCLOUD_DATABASE_USER fi if [ -z "$NEXTCLOUD_DATABASE_USER_PASSWORD" ]; then NEXTCLOUD_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NEXTCLOUD_DATABASE_USER_PASSWORD $NEXTCLOUD_DATABASE_USER_PASSWORD fi if [ -z "$NEXTCLOUD_TALKHPB_SIGNALING_SECRET" ]; then NEXTCLOUD_TALKHPB_SIGNALING_SECRET=$(openssl rand --hex 32) updateConfigVar NEXTCLOUD_TALKHPB_SIGNALING_SECRET $NEXTCLOUD_TALKHPB_SIGNALING_SECRET fi if [ -z "$NEXTCLOUD_TALKHPB_INTERNAL_SECRET" ]; then NEXTCLOUD_TALKHPB_INTERNAL_SECRET=$(openssl rand --hex 32) updateConfigVar NEXTCLOUD_TALKHPB_INTERNAL_SECRET $NEXTCLOUD_TALKHPB_INTERNAL_SECRET fi if [ -z "$NEXTCLOUD_TALKHPB_RECORDING_SECRET" ]; then NEXTCLOUD_TALKHPB_RECORDING_SECRET=$(openssl rand --hex 32) updateConfigVar NEXTCLOUD_TALKHPB_RECORDING_SECRET $NEXTCLOUD_TALKHPB_RECORDING_SECRET fi if [ -z "$NEXTCLOUD_HARP_SHARED_KEY" ]; then NEXTCLOUD_HARP_SHARED_KEY=$(pwgen -c -n 32 1) updateConfigVar NEXTCLOUD_HARP_SHARED_KEY $NEXTCLOUD_HARP_SHARED_KEY fi if [ -z "$NEXTCLOUD_TOKEN_ENCRYPTION_KEY" ]; then NEXTCLOUD_TOKEN_ENCRYPTION_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") updateConfigVar NEXTCLOUD_TOKEN_ENCRYPTION_KEY $NEXTCLOUD_TOKEN_ENCRYPTION_KEY fi if [ -z "$MATRIX_DATABASE_NAME" ]; then MATRIX_DATABASE_NAME="matrixdb" updateConfigVar MATRIX_DATABASE_NAME $MATRIX_DATABASE_NAME fi if [ -z "$MATRIX_DATABASE_USER" ]; then MATRIX_DATABASE_USER="matrix-user" updateConfigVar MATRIX_DATABASE_USER $MATRIX_DATABASE_USER fi if [ -z "$MATRIX_DATABASE_USER_PASSWORD" ]; then MATRIX_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MATRIX_DATABASE_USER_PASSWORD $MATRIX_DATABASE_USER_PASSWORD fi if [ -z "$WIKIJS_DATABASE_NAME" ]; then WIKIJS_DATABASE_NAME="wikijsdb" updateConfigVar WIKIJS_DATABASE_NAME $WIKIJS_DATABASE_NAME fi if [ -z "$WIKIJS_DATABASE_USER" ]; then WIKIJS_DATABASE_USER="wikijs-user" updateConfigVar WIKIJS_DATABASE_USER $WIKIJS_DATABASE_USER fi if [ -z "$WIKIJS_DATABASE_USER_PASSWORD" ]; then WIKIJS_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WIKIJS_DATABASE_USER_PASSWORD $WIKIJS_DATABASE_USER_PASSWORD fi if [ -z "$DUPLICATI_ADMIN_PASSWORD" ]; then DUPLICATI_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DUPLICATI_ADMIN_PASSWORD $DUPLICATI_ADMIN_PASSWORD fi if [ -z "$DUPLICATI_SETTINGS_ENCRYPTION_KEY" ]; then DUPLICATI_SETTINGS_ENCRYPTION_KEY=$(pwgen -c -n 32 1) updateConfigVar DUPLICATI_SETTINGS_ENCRYPTION_KEY $DUPLICATI_SETTINGS_ENCRYPTION_KEY fi if [ -z "$MASTODON_ADMIN_USERNAME" ]; then MASTODON_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_mastodon" updateConfigVar MASTODON_ADMIN_USERNAME $MASTODON_ADMIN_USERNAME fi if [ -z "$MASTODON_ADMIN_EMAIL_ADDRESS" ]; then MASTODON_ADMIN_EMAIL_ADDRESS=$MASTODON_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MASTODON_ADMIN_EMAIL_ADDRESS $MASTODON_ADMIN_EMAIL_ADDRESS fi if [ -z "$MASTODON_DATABASE_NAME" ]; then MASTODON_DATABASE_NAME=mastodondb updateConfigVar MASTODON_DATABASE_NAME $MASTODON_DATABASE_NAME fi if [ -z "$MASTODON_DATABASE_USER" ]; then MASTODON_DATABASE_USER=mastodon-user updateConfigVar MASTODON_DATABASE_USER $MASTODON_DATABASE_USER fi if [ -z "$MASTODON_DATABASE_USER_PASSWORD" ]; then MASTODON_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MASTODON_DATABASE_USER_PASSWORD $MASTODON_DATABASE_USER_PASSWORD fi if [ -z "$MASTODON_ARE_DETERMINISTIC_KEY" ]; then MASTODON_ARE_DETERMINISTIC_KEY=$(pwgen -c -n 32 1) updateConfigVar MASTODON_ARE_DETERMINISTIC_KEY $MASTODON_ARE_DETERMINISTIC_KEY fi if [ -z "$MASTODON_ARE_KEY_DERIVATION_SALT" ]; then MASTODON_ARE_KEY_DERIVATION_SALT=$(pwgen -c -n 32 1) updateConfigVar MASTODON_ARE_KEY_DERIVATION_SALT $MASTODON_ARE_KEY_DERIVATION_SALT fi if [ -z "$MASTODON_ARE_PRIMARY_KEY" ]; then MASTODON_ARE_PRIMARY_KEY=$(pwgen -c -n 32 1) updateConfigVar MASTODON_ARE_PRIMARY_KEY $MASTODON_ARE_PRIMARY_KEY fi if [ -z "$DOZZLE_USERNAME" ]; then DOZZLE_USERNAME=$ADMIN_USERNAME_BASE"_dozzle" updateConfigVar DOZZLE_USERNAME $DOZZLE_USERNAME fi if [ -z "$DOZZLE_PASSWORD" ]; then DOZZLE_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOZZLE_PASSWORD $DOZZLE_PASSWORD fi if [ -z "$JELLYFIN_ADMIN_USERNAME" ]; then JELLYFIN_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_jellyfin" updateConfigVar JELLYFIN_ADMIN_USERNAME $JELLYFIN_ADMIN_USERNAME fi if [ -z "$JELLYFIN_ADMIN_PASSWORD" ]; then JELLYFIN_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar JELLYFIN_ADMIN_PASSWORD $JELLYFIN_ADMIN_PASSWORD fi if [ -z "$FILEBROWSER_USERNAME" ]; then FILEBROWSER_USERNAME=$ADMIN_USERNAME_BASE"_filebrowser" updateConfigVar FILEBROWSER_USERNAME $FILEBROWSER_USERNAME fi if [ -z "$FILEBROWSER_PASSWORD" ]; then FILEBROWSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FILEBROWSER_PASSWORD $FILEBROWSER_PASSWORD fi if [ -z "$PHOTOPRISM_DATABASE_NAME" ]; then PHOTOPRISM_DATABASE_NAME="photoprismdb" updateConfigVar PHOTOPRISM_DATABASE_NAME $PHOTOPRISM_DATABASE_NAME fi if [ -z "$PHOTOPRISM_DATABASE_ROOT_PASSWORD" ]; then PHOTOPRISM_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PHOTOPRISM_DATABASE_ROOT_PASSWORD $PHOTOPRISM_DATABASE_ROOT_PASSWORD fi if [ -z "$PHOTOPRISM_DATABASE_USER" ]; then PHOTOPRISM_DATABASE_USER="photoprism-user" updateConfigVar PHOTOPRISM_DATABASE_USER $PHOTOPRISM_DATABASE_USER fi if [ -z "$PHOTOPRISM_DATABASE_USER_PASSWORD" ]; then PHOTOPRISM_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PHOTOPRISM_DATABASE_USER_PASSWORD $PHOTOPRISM_DATABASE_USER_PASSWORD fi if [ -z "$PHOTOPRISM_ADMIN_USERNAME" ]; then PHOTOPRISM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_photoprism" updateConfigVar PHOTOPRISM_ADMIN_USERNAME $PHOTOPRISM_ADMIN_USERNAME fi if [ -z "$PHOTOPRISM_ADMIN_PASSWORD" ]; then PHOTOPRISM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PHOTOPRISM_ADMIN_PASSWORD $PHOTOPRISM_ADMIN_PASSWORD fi if [ -z "$GUACAMOLE_DATABASE_NAME" ]; then GUACAMOLE_DATABASE_NAME="guacamoledb" updateConfigVar GUACAMOLE_DATABASE_NAME $GUACAMOLE_DATABASE_NAME fi if [ -z "$GUACAMOLE_DATABASE_USER" ]; then GUACAMOLE_DATABASE_USER="guacamole-user" updateConfigVar GUACAMOLE_DATABASE_USER $GUACAMOLE_DATABASE_USER fi if [ -z "$GUACAMOLE_DATABASE_USER_PASSWORD" ]; then GUACAMOLE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GUACAMOLE_DATABASE_USER_PASSWORD $GUACAMOLE_DATABASE_USER_PASSWORD fi if [ -z "$WORDPRESS_ADMIN_USERNAME" ]; then WORDPRESS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_wordpress" updateConfigVar WORDPRESS_ADMIN_USERNAME $WORDPRESS_ADMIN_USERNAME fi if [ -z "$WORDPRESS_ADMIN_PASSWORD" ]; then WORDPRESS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WORDPRESS_ADMIN_PASSWORD $WORDPRESS_ADMIN_PASSWORD fi if [ -z "$WORDPRESS_ADMIN_EMAIL_ADDRESS" ]; then WORDPRESS_ADMIN_EMAIL_ADDRESS=$WORDPRESS_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar WORDPRESS_ADMIN_EMAIL_ADDRESS $WORDPRESS_ADMIN_EMAIL_ADDRESS fi if [ -z "$WORDPRESS_DATABASE_NAME" ]; then WORDPRESS_DATABASE_NAME=wordpressdb updateConfigVar WORDPRESS_DATABASE_NAME $WORDPRESS_DATABASE_NAME fi if [ -z "$WORDPRESS_DATABASE_ROOT_PASSWORD" ]; then WORDPRESS_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WORDPRESS_DATABASE_ROOT_PASSWORD $WORDPRESS_DATABASE_ROOT_PASSWORD fi if [ -z "$WORDPRESS_DATABASE_USER" ]; then WORDPRESS_DATABASE_USER=wordpress-user updateConfigVar WORDPRESS_DATABASE_USER $WORDPRESS_DATABASE_USER fi if [ -z "$WORDPRESS_DATABASE_USER_PASSWORD" ]; then WORDPRESS_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WORDPRESS_DATABASE_USER_PASSWORD $WORDPRESS_DATABASE_USER_PASSWORD fi if [ -z "$GHOST_DATABASE_NAME" ]; then GHOST_DATABASE_NAME=ghostdb updateConfigVar GHOST_DATABASE_NAME $GHOST_DATABASE_NAME fi if [ -z "$GHOST_DATABASE_USER" ]; then GHOST_DATABASE_USER=ghost-user updateConfigVar GHOST_DATABASE_USER $GHOST_DATABASE_USER fi if [ -z "$GHOST_DATABASE_USER_PASSWORD" ]; then GHOST_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GHOST_DATABASE_USER_PASSWORD $GHOST_DATABASE_USER_PASSWORD fi if [ -z "$WIKIJS_ADMIN_USERNAME" ]; then WIKIJS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_wikijs" updateConfigVar WIKIJS_ADMIN_USERNAME $WIKIJS_ADMIN_USERNAME fi if [ -z "$WIKIJS_ADMIN_EMAIL_ADDRESS" ]; then WIKIJS_ADMIN_EMAIL_ADDRESS=$WIKIJS_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar WIKIJS_ADMIN_EMAIL_ADDRESS $WIKIJS_ADMIN_EMAIL_ADDRESS fi if [ -z "$WIKIJS_ADMIN_PASSWORD" ]; then WIKIJS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WIKIJS_ADMIN_PASSWORD $WIKIJS_ADMIN_PASSWORD fi if [ -z "$PEERTUBE_ADMIN_USERNAME" ]; then PEERTUBE_ADMIN_USERNAME="root" updateConfigVar PEERTUBE_ADMIN_USERNAME $PEERTUBE_ADMIN_USERNAME fi if [ -z "$PEERTUBE_ADMIN_EMAIL_ADDRESS" ]; then PEERTUBE_ADMIN_EMAIL_ADDRESS=$ADMIN_USERNAME_BASE"_peertube@"$HOMESERVER_DOMAIN updateConfigVar PEERTUBE_ADMIN_EMAIL_ADDRESS $PEERTUBE_ADMIN_EMAIL_ADDRESS fi if [ -z "$PEERTUBE_ADMIN_PASSWORD" ]; then PEERTUBE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PEERTUBE_ADMIN_PASSWORD $PEERTUBE_ADMIN_PASSWORD fi if [ -z "$PEERTUBE_DATABASE_NAME" ]; then PEERTUBE_DATABASE_NAME=peertubedb updateConfigVar PEERTUBE_DATABASE_NAME $PEERTUBE_DATABASE_NAME fi if [ -z "$PEERTUBE_DATABASE_USER" ]; then PEERTUBE_DATABASE_USER=peertube-user updateConfigVar PEERTUBE_DATABASE_USER $PEERTUBE_DATABASE_USER fi if [ -z "$PEERTUBE_DATABASE_USER_PASSWORD" ]; then PEERTUBE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PEERTUBE_DATABASE_USER_PASSWORD $PEERTUBE_DATABASE_USER_PASSWORD fi if [ -z "$HOMEASSISTANT_DATABASE_NAME" ]; then HOMEASSISTANT_DATABASE_NAME=homeassistantdb updateConfigVar HOMEASSISTANT_DATABASE_NAME $HOMEASSISTANT_DATABASE_NAME fi if [ -z "$HOMEASSISTANT_DATABASE_USER" ]; then HOMEASSISTANT_DATABASE_USER=homeassistant-user updateConfigVar HOMEASSISTANT_DATABASE_USER $HOMEASSISTANT_DATABASE_USER fi if [ -z "$HOMEASSISTANT_DATABASE_USER_PASSWORD" ]; then HOMEASSISTANT_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HOMEASSISTANT_DATABASE_USER_PASSWORD $HOMEASSISTANT_DATABASE_USER_PASSWORD fi if [ -z "$HOMEASSISTANT_TASMOADMIN_USER" ]; then HOMEASSISTANT_TASMOADMIN_USER=hass_tasmoadmin_user updateConfigVar HOMEASSISTANT_TASMOADMIN_USER $HOMEASSISTANT_TASMOADMIN_USER fi if [ -z "$HOMEASSISTANT_TASMOADMIN_USER_PASSWORD" ]; then HOMEASSISTANT_TASMOADMIN_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HOMEASSISTANT_TASMOADMIN_USER_PASSWORD $HOMEASSISTANT_TASMOADMIN_USER_PASSWORD fi if [ -z "$HOMEASSISTANT_CONFIGURATOR_USER" ]; then HOMEASSISTANT_CONFIGURATOR_USER=hass_configurator_user updateConfigVar HOMEASSISTANT_CONFIGURATOR_USER $HOMEASSISTANT_CONFIGURATOR_USER fi if [ -z "$HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD" ]; then HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD $HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD fi if [ -z "$GITLAB_DATABASE_NAME" ]; then GITLAB_DATABASE_NAME=gitlabdb updateConfigVar GITLAB_DATABASE_NAME $GITLAB_DATABASE_NAME fi if [ -z "$GITLAB_DATABASE_USER" ]; then GITLAB_DATABASE_USER=gitlab-user updateConfigVar GITLAB_DATABASE_USER $GITLAB_DATABASE_USER fi if [ -z "$GITLAB_DATABASE_USER_PASSWORD" ]; then GITLAB_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GITLAB_DATABASE_USER_PASSWORD $GITLAB_DATABASE_USER_PASSWORD fi if [ -z "$VAULTWARDEN_ADMIN_TOKEN" ]; then VAULTWARDEN_ADMIN_TOKEN=$(pwgen -c -n 64 1) updateConfigVar VAULTWARDEN_ADMIN_TOKEN $VAULTWARDEN_ADMIN_TOKEN fi if [ -z "$VAULTWARDEN_DATABASE_NAME" ]; then VAULTWARDEN_DATABASE_NAME=vaultwardendb updateConfigVar VAULTWARDEN_DATABASE_NAME $VAULTWARDEN_DATABASE_NAME fi if [ -z "$VAULTWARDEN_DATABASE_USER" ]; then VAULTWARDEN_DATABASE_USER=vaultwarden-user updateConfigVar VAULTWARDEN_DATABASE_USER $VAULTWARDEN_DATABASE_USER fi if [ -z "$VAULTWARDEN_DATABASE_USER_PASSWORD" ]; then VAULTWARDEN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar VAULTWARDEN_DATABASE_USER_PASSWORD $VAULTWARDEN_DATABASE_USER_PASSWORD fi if [ -z "$DISCOURSE_ADMIN_USERNAME" ]; then DISCOURSE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_discourse" if [ ${#DISCOURSE_ADMIN_USERNAME} -gt 20 ]; then DISCOURSE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE fi updateConfigVar DISCOURSE_ADMIN_USERNAME $DISCOURSE_ADMIN_USERNAME fi if [ -z "$DISCOURSE_ADMIN_EMAIL_ADDRESS" ]; then DISCOURSE_ADMIN_EMAIL_ADDRESS=$ADMIN_USERNAME_BASE"_discourse@"$HOMESERVER_DOMAIN updateConfigVar DISCOURSE_ADMIN_EMAIL_ADDRESS $DISCOURSE_ADMIN_EMAIL_ADDRESS fi if [ -z "$DISCOURSE_ADMIN_PASSWORD" ]; then DISCOURSE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DISCOURSE_ADMIN_PASSWORD $DISCOURSE_ADMIN_PASSWORD fi if [ -z "$DISCOURSE_DATABASE_NAME" ]; then DISCOURSE_DATABASE_NAME=discoursedb updateConfigVar DISCOURSE_DATABASE_NAME $DISCOURSE_DATABASE_NAME fi if [ -z "$DISCOURSE_DATABASE_USER" ]; then DISCOURSE_DATABASE_USER=discourse-user updateConfigVar DISCOURSE_DATABASE_USER $DISCOURSE_DATABASE_USER fi if [ -z "$DISCOURSE_DATABASE_USER_PASSWORD" ]; then DISCOURSE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DISCOURSE_DATABASE_USER_PASSWORD $DISCOURSE_DATABASE_USER_PASSWORD fi if [ -z "$SYNCTHING_ADMIN_USERNAME" ]; then SYNCTHING_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_syncthing" updateConfigVar SYNCTHING_ADMIN_USERNAME $SYNCTHING_ADMIN_USERNAME fi if [ -z "$SYNCTHING_ADMIN_PASSWORD" ]; then SYNCTHING_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SYNCTHING_ADMIN_PASSWORD $SYNCTHING_ADMIN_PASSWORD fi if [ -z "$CODESERVER_ADMIN_USERNAME" ]; then CODESERVER_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_codeserver" updateConfigVar CODESERVER_ADMIN_USERNAME $CODESERVER_ADMIN_USERNAME fi if [ -z "$CODESERVER_ADMIN_PASSWORD" ]; then CODESERVER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CODESERVER_ADMIN_PASSWORD $CODESERVER_ADMIN_PASSWORD fi if [ -z "$SHLINK_DATABASE_NAME" ]; then SHLINK_DATABASE_NAME=shlinkdb updateConfigVar SHLINK_DATABASE_NAME $SHLINK_DATABASE_NAME fi if [ -z "$SHLINK_DATABASE_USER" ]; then SHLINK_DATABASE_USER=shlink-user updateConfigVar SHLINK_DATABASE_USER $SHLINK_DATABASE_USER fi if [ -z "$SHLINK_DATABASE_USER_PASSWORD" ]; then SHLINK_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SHLINK_DATABASE_USER_PASSWORD $SHLINK_DATABASE_USER_PASSWORD fi if [ -z "$FIREFLY_DATABASE_NAME" ]; then FIREFLY_DATABASE_NAME=fireflydb updateConfigVar FIREFLY_DATABASE_NAME $FIREFLY_DATABASE_NAME fi if [ -z "$FIREFLY_DATABASE_USER" ]; then FIREFLY_DATABASE_USER=firefly-user updateConfigVar FIREFLY_DATABASE_USER $FIREFLY_DATABASE_USER fi if [ -z "$FIREFLY_DATABASE_USER_PASSWORD" ]; then FIREFLY_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FIREFLY_DATABASE_USER_PASSWORD $FIREFLY_DATABASE_USER_PASSWORD fi if [ -z "$FIREFLY_STATIC_CRON_TOKEN" ]; then FIREFLY_STATIC_CRON_TOKEN=$(pwgen -c -n 32 1) updateConfigVar FIREFLY_STATIC_CRON_TOKEN $FIREFLY_STATIC_CRON_TOKEN fi if [ -z "$INVIDIOUS_DATABASE_NAME" ]; then INVIDIOUS_DATABASE_NAME=invidiousdb updateConfigVar INVIDIOUS_DATABASE_NAME $INVIDIOUS_DATABASE_NAME fi if [ -z "$INVIDIOUS_DATABASE_USER" ]; then INVIDIOUS_DATABASE_USER=invidious-user updateConfigVar INVIDIOUS_DATABASE_USER $INVIDIOUS_DATABASE_USER fi if [ -z "$INVIDIOUS_DATABASE_USER_PASSWORD" ]; then INVIDIOUS_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVIDIOUS_DATABASE_USER_PASSWORD $INVIDIOUS_DATABASE_USER_PASSWORD fi if [ -z "$GITEA_ADMIN_USERNAME" ]; then GITEA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_gitea" updateConfigVar GITEA_ADMIN_USERNAME $GITEA_ADMIN_USERNAME fi if [ -z "$GITEA_ADMIN_EMAIL_ADDRESS" ]; then GITEA_ADMIN_EMAIL_ADDRESS=$GITEA_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar GITEA_ADMIN_EMAIL_ADDRESS $GITEA_ADMIN_EMAIL_ADDRESS fi if [ -z "$GITEA_ADMIN_PASSWORD" ]; then GITEA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GITEA_ADMIN_PASSWORD $GITEA_ADMIN_PASSWORD fi if [ -z "$GITEA_DATABASE_NAME" ]; then GITEA_DATABASE_NAME=giteadb updateConfigVar GITEA_DATABASE_NAME $GITEA_DATABASE_NAME fi if [ -z "$GITEA_DATABASE_USER" ]; then GITEA_DATABASE_USER=gitea-user updateConfigVar GITEA_DATABASE_USER $GITEA_DATABASE_USER fi if [ -z "$GITEA_DATABASE_USER_PASSWORD" ]; then GITEA_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GITEA_DATABASE_USER_PASSWORD $GITEA_DATABASE_USER_PASSWORD fi if [ -z "$SQLPAD_ADMIN_USERNAME" ]; then SQLPAD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_sqlpad" updateConfigVar SQLPAD_ADMIN_USERNAME $SQLPAD_ADMIN_USERNAME fi if [ -z "$SQLPAD_ADMIN_PASSWORD" ]; then SQLPAD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SQLPAD_ADMIN_PASSWORD $SQLPAD_ADMIN_PASSWORD fi if [ -z "$HEIMDALL_ADMIN_USERNAME" ]; then HEIMDALL_ADMIN_USERNAME="Admin" updateConfigVar HEIMDALL_ADMIN_USERNAME $HEIMDALL_ADMIN_USERNAME fi if [ -z "$HEIMDALL_ADMIN_PASSWORD" ]; then HEIMDALL_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HEIMDALL_ADMIN_PASSWORD $HEIMDALL_ADMIN_PASSWORD fi if [ -z "$HEIMDALL_USER_USERNAME" ]; then HEIMDALL_USER_USERNAME="Users" updateConfigVar HEIMDALL_USER_USERNAME $HEIMDALL_USER_USERNAME fi if [ -z "$HEIMDALL_USER_PASSWORD" ]; then HEIMDALL_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HEIMDALL_USER_PASSWORD $HEIMDALL_USER_PASSWORD fi if [ -z "$HEIMDALL_HOMESERVERS_USERNAME" ]; then HEIMDALL_HOMESERVERS_USERNAME="RemoteServers" updateConfigVar HEIMDALL_HOMESERVERS_USERNAME $HEIMDALL_HOMESERVERS_USERNAME fi if [ -z "$HEIMDALL_HOMESERVERS_PASSWORD" ]; then HEIMDALL_HOMESERVERS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HEIMDALL_HOMESERVERS_PASSWORD $HEIMDALL_HOMESERVERS_PASSWORD fi if [ -z "$HEIMDALL_RELAYSERVER_USERNAME" ]; then HEIMDALL_RELAYSERVER_USERNAME="RelayServer" updateConfigVar HEIMDALL_RELAYSERVER_USERNAME $HEIMDALL_RELAYSERVER_USERNAME fi if [ -z "$HEIMDALL_RELAYSERVER_PASSWORD" ]; then HEIMDALL_RELAYSERVER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HEIMDALL_RELAYSERVER_PASSWORD $HEIMDALL_RELAYSERVER_PASSWORD fi if [ -z "$UPTIMEKUMA_USERNAME" ]; then UPTIMEKUMA_USERNAME=$ADMIN_USERNAME_BASE"_uptimekuma" updateConfigVar UPTIMEKUMA_USERNAME $UPTIMEKUMA_USERNAME fi if [ -z "$UPTIMEKUMA_PASSWORD" ]; then UPTIMEKUMA_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar UPTIMEKUMA_PASSWORD $UPTIMEKUMA_PASSWORD fi if [ -z "$MEALIE_ADMIN_USERNAME" ]; then MEALIE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_mealie" updateConfigVar MEALIE_ADMIN_USERNAME $MEALIE_ADMIN_USERNAME fi if [ -z "$MEALIE_ADMIN_EMAIL_ADDRESS" ]; then MEALIE_ADMIN_EMAIL_ADDRESS=$MEALIE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MEALIE_ADMIN_EMAIL_ADDRESS $MEALIE_ADMIN_EMAIL_ADDRESS fi if [ -z "$MEALIE_ADMIN_PASSWORD" ]; then MEALIE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MEALIE_ADMIN_PASSWORD $MEALIE_ADMIN_PASSWORD fi if [ -z "$MEALIE_DATABASE_NAME" ]; then MEALIE_DATABASE_NAME=mealiedb updateConfigVar MEALIE_DATABASE_NAME $MEALIE_DATABASE_NAME fi if [ -z "$MEALIE_DATABASE_USER" ]; then MEALIE_DATABASE_USER=mealie-user updateConfigVar MEALIE_DATABASE_USER $MEALIE_DATABASE_USER fi if [ -z "$MEALIE_DATABASE_USER_PASSWORD" ]; then MEALIE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MEALIE_DATABASE_USER_PASSWORD $MEALIE_DATABASE_USER_PASSWORD fi if [ -z "$BARASSISTANT_REDIS_PASSWORD" ]; then BARASSISTANT_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BARASSISTANT_REDIS_PASSWORD $BARASSISTANT_REDIS_PASSWORD fi if [ -z "$BARASSISTANT_MEILISEARCH_KEY" ]; then BARASSISTANT_MEILISEARCH_KEY=$(pwgen -c -n 64 1) updateConfigVar BARASSISTANT_MEILISEARCH_KEY $BARASSISTANT_MEILISEARCH_KEY fi if [ -z "$BARASSISTANT_MCP_API_KEY" ]; then BARASSISTANT_MCP_API_KEY=$(pwgen -c -n 32 1) updateConfigVar BARASSISTANT_MCP_API_KEY $BARASSISTANT_MCP_API_KEY fi if [ -z "$BARASSISTANT_API_KEY" ]; then BARASSISTANT_API_KEY=$(pwgen -s 40 1) updateConfigVar BARASSISTANT_API_KEY $BARASSISTANT_API_KEY fi if [ -z "$KASM_ADMIN_EMAIL_ADDRESS" ]; then KASM_ADMIN_EMAIL_ADDRESS=admin@kasm.local updateConfigVar KASM_ADMIN_EMAIL_ADDRESS $KASM_ADMIN_EMAIL_ADDRESS fi if [ -z "$KASM_ADMIN_PASSWORD" ]; then KASM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KASM_ADMIN_PASSWORD $KASM_ADMIN_PASSWORD fi if [ -z "$KASM_USER_EMAIL_ADDRESS" ]; then KASM_USER_EMAIL_ADDRESS=user@kasm.local updateConfigVar KASM_USER_EMAIL_ADDRESS $KASM_USER_EMAIL_ADDRESS fi if [ -z "$KASM_USER_PASSWORD" ]; then KASM_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KASM_USER_PASSWORD $KASM_USER_PASSWORD fi if [ -z "$REMOTELY_ADMIN_USERNAME" ]; then REMOTELY_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_remotely" updateConfigVar REMOTELY_ADMIN_USERNAME $REMOTELY_ADMIN_USERNAME fi if [ -z "$REMOTELY_ADMIN_EMAIL_ADDRESS" ]; then REMOTELY_ADMIN_EMAIL_ADDRESS=$REMOTELY_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar REMOTELY_ADMIN_EMAIL_ADDRESS $REMOTELY_ADMIN_EMAIL_ADDRESS fi if [ -z "$REMOTELY_ADMIN_PASSWORD" ]; then REMOTELY_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar REMOTELY_ADMIN_PASSWORD $REMOTELY_ADMIN_PASSWORD fi if [ -z "$CALIBRE_WEB_ADMIN_USERNAME" ]; then CALIBRE_WEB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_calibre" updateConfigVar CALIBRE_WEB_ADMIN_USERNAME $CALIBRE_WEB_ADMIN_USERNAME fi if [ -z "$CALIBRE_WEB_ADMIN_EMAIL_ADDRESS" ]; then CALIBRE_WEB_ADMIN_EMAIL_ADDRESS=$CALIBRE_WEB_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar CALIBRE_WEB_ADMIN_EMAIL_ADDRESS $CALIBRE_WEB_ADMIN_EMAIL_ADDRESS fi if [ -z "$CALIBRE_WEB_ADMIN_PASSWORD" ]; then CALIBRE_WEB_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar CALIBRE_WEB_ADMIN_PASSWORD $CALIBRE_WEB_ADMIN_PASSWORD fi if [ -z "$LINKWARDEN_DATABASE_NAME" ]; then LINKWARDEN_DATABASE_NAME=linkwardendb updateConfigVar LINKWARDEN_DATABASE_NAME $LINKWARDEN_DATABASE_NAME fi if [ -z "$LINKWARDEN_DATABASE_USER" ]; then LINKWARDEN_DATABASE_USER=linkwarden-user updateConfigVar LINKWARDEN_DATABASE_USER $LINKWARDEN_DATABASE_USER fi if [ -z "$LINKWARDEN_DATABASE_USER_PASSWORD" ]; then LINKWARDEN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LINKWARDEN_DATABASE_USER_PASSWORD $LINKWARDEN_DATABASE_USER_PASSWORD fi if [ -z "$LINKWARDEN_OIDC_CLIENT_SECRET" ]; then LINKWARDEN_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar LINKWARDEN_OIDC_CLIENT_SECRET $LINKWARDEN_OIDC_CLIENT_SECRET fi if [ -z "$FRESHRSS_ADMIN_USERNAME" ]; then FRESHRSS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_freshrss" updateConfigVar FRESHRSS_ADMIN_USERNAME $FRESHRSS_ADMIN_USERNAME fi if [ -z "$FRESHRSS_ADMIN_EMAIL_ADDRESS" ]; then FRESHRSS_ADMIN_EMAIL_ADDRESS=$FRESHRSS_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar FRESHRSS_ADMIN_EMAIL_ADDRESS $FRESHRSS_ADMIN_EMAIL_ADDRESS fi if [ -z "$FRESHRSS_ADMIN_PASSWORD" ]; then FRESHRSS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRESHRSS_ADMIN_PASSWORD $FRESHRSS_ADMIN_PASSWORD fi if [ -z "$FRESHRSS_DATABASE_NAME" ]; then FRESHRSS_DATABASE_NAME=freshrssdb updateConfigVar FRESHRSS_DATABASE_NAME $FRESHRSS_DATABASE_NAME fi if [ -z "$FRESHRSS_DATABASE_USER" ]; then FRESHRSS_DATABASE_USER=freshrss-user updateConfigVar FRESHRSS_DATABASE_USER $FRESHRSS_DATABASE_USER fi if [ -z "$FRESHRSS_DATABASE_USER_PASSWORD" ]; then FRESHRSS_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRESHRSS_DATABASE_USER_PASSWORD $FRESHRSS_DATABASE_USER_PASSWORD fi if [ -z "$FRESHRSS_OIDC_CLIENT_SECRET" ]; then FRESHRSS_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar FRESHRSS_OIDC_CLIENT_SECRET $FRESHRSS_OIDC_CLIENT_SECRET fi if [ -z "$KEILA_ADMIN_USERNAME" ]; then KEILA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_keila" updateConfigVar KEILA_ADMIN_USERNAME $KEILA_ADMIN_USERNAME fi if [ -z "$KEILA_ADMIN_EMAIL_ADDRESS" ]; then KEILA_ADMIN_EMAIL_ADDRESS=$KEILA_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar KEILA_ADMIN_EMAIL_ADDRESS $KEILA_ADMIN_EMAIL_ADDRESS fi if [ -z "$KEILA_ADMIN_PASSWORD" ]; then KEILA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KEILA_ADMIN_PASSWORD $KEILA_ADMIN_PASSWORD fi if [ -z "$KEILA_DATABASE_NAME" ]; then KEILA_DATABASE_NAME=keiladb updateConfigVar KEILA_DATABASE_NAME $KEILA_DATABASE_NAME fi if [ -z "$KEILA_DATABASE_USER" ]; then KEILA_DATABASE_USER=keila-user updateConfigVar KEILA_DATABASE_USER $KEILA_DATABASE_USER fi if [ -z "$KEILA_DATABASE_USER_PASSWORD" ]; then KEILA_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KEILA_DATABASE_USER_PASSWORD $KEILA_DATABASE_USER_PASSWORD fi if [ -z "$WALLABAG_ADMIN_USERNAME" ]; then WALLABAG_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_wallabag" updateConfigVar WALLABAG_ADMIN_USERNAME $WALLABAG_ADMIN_USERNAME fi if [ -z "$WALLABAG_ADMIN_EMAIL_ADDRESS" ]; then WALLABAG_ADMIN_EMAIL_ADDRESS=$WALLABAG_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar WALLABAG_ADMIN_EMAIL_ADDRESS $WALLABAG_ADMIN_EMAIL_ADDRESS fi if [ -z "$WALLABAG_ADMIN_PASSWORD" ]; then WALLABAG_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WALLABAG_ADMIN_PASSWORD $WALLABAG_ADMIN_PASSWORD fi if [ -z "$WALLABAG_DATABASE_NAME" ]; then WALLABAG_DATABASE_NAME=wallabagdb updateConfigVar WALLABAG_DATABASE_NAME $WALLABAG_DATABASE_NAME fi if [ -z "$WALLABAG_DATABASE_USER" ]; then WALLABAG_DATABASE_USER=wallabag-user updateConfigVar WALLABAG_DATABASE_USER $WALLABAG_DATABASE_USER fi if [ -z "$WALLABAG_DATABASE_USER_PASSWORD" ]; then WALLABAG_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WALLABAG_DATABASE_USER_PASSWORD $WALLABAG_DATABASE_USER_PASSWORD fi if [ -z "$JUPYTER_ADMIN_PASSWORD" ]; then JUPYTER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar JUPYTER_ADMIN_PASSWORD $JUPYTER_ADMIN_PASSWORD fi if [ -z "$PAPERLESS_ADMIN_USERNAME" ]; then PAPERLESS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_paperless" updateConfigVar PAPERLESS_ADMIN_USERNAME $PAPERLESS_ADMIN_USERNAME fi if [ -z "$PAPERLESS_ADMIN_EMAIL_ADDRESS" ]; then PAPERLESS_ADMIN_EMAIL_ADDRESS=$PAPERLESS_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar PAPERLESS_ADMIN_EMAIL_ADDRESS $PAPERLESS_ADMIN_EMAIL_ADDRESS fi if [ -z "$PAPERLESS_ADMIN_PASSWORD" ]; then PAPERLESS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PAPERLESS_ADMIN_PASSWORD $PAPERLESS_ADMIN_PASSWORD fi if [ -z "$PAPERLESS_DATABASE_NAME" ]; then PAPERLESS_DATABASE_NAME=paperlessdb updateConfigVar PAPERLESS_DATABASE_NAME $PAPERLESS_DATABASE_NAME fi if [ -z "$PAPERLESS_DATABASE_USER" ]; then PAPERLESS_DATABASE_USER=paperless-user updateConfigVar PAPERLESS_DATABASE_USER $PAPERLESS_DATABASE_USER fi if [ -z "$PAPERLESS_DATABASE_USER_PASSWORD" ]; then PAPERLESS_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PAPERLESS_DATABASE_USER_PASSWORD $PAPERLESS_DATABASE_USER_PASSWORD fi if [ -z "$PAPERLESS_OIDC_CLIENT_SECRET" ]; then PAPERLESS_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar PAPERLESS_OIDC_CLIENT_SECRET $PAPERLESS_OIDC_CLIENT_SECRET fi if [ -z "$PAPERLESS_AI_ADMIN_USERNAME" ]; then PAPERLESS_AI_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_paperless_ai" updateConfigVar PAPERLESS_AI_ADMIN_USERNAME $PAPERLESS_AI_ADMIN_USERNAME fi if [ -z "$PAPERLESS_AI_ADMIN_PASSWORD" ]; then PAPERLESS_AI_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar PAPERLESS_AI_ADMIN_PASSWORD $PAPERLESS_AI_ADMIN_PASSWORD fi if [ -z "$PAPERLESS_AI_API_KEY" ]; then PAPERLESS_AI_API_KEY=$(openssl rand -hex 64) updateConfigVar PAPERLESS_AI_API_KEY $PAPERLESS_AI_API_KEY fi if [ -z "$PAPERLESS_AI_JWT_SECRET" ]; then PAPERLESS_AI_JWT_SECRET=$(openssl rand -hex 64) updateConfigVar PAPERLESS_AI_JWT_SECRET $PAPERLESS_AI_JWT_SECRET fi if [ -z "$PAPERLESS_GPT_ADMIN_USERNAME" ]; then PAPERLESS_GPT_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_paperless_gpt" updateConfigVar PAPERLESS_GPT_ADMIN_USERNAME $PAPERLESS_GPT_ADMIN_USERNAME fi if [ -z "$PAPERLESS_GPT_ADMIN_PASSWORD" ]; then PAPERLESS_GPT_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar PAPERLESS_GPT_ADMIN_PASSWORD $PAPERLESS_GPT_ADMIN_PASSWORD fi if [ -z "$PAPERLESS_API_TOKEN" ]; then PAPERLESS_API_TOKEN=$(openssl rand -hex 20) updateConfigVar PAPERLESS_API_TOKEN $PAPERLESS_API_TOKEN fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME" ]; then SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_speedtest_tracker_local" updateConfigVar SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME $SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS" ]; then SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS=$SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS $SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD" ]; then SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD $SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME" ]; then SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME=speedtesttrackerlocaldb updateConfigVar SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME $SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_DATABASE_USER" ]; then SPEEDTEST_TRACKER_LOCAL_DATABASE_USER=speedtest-tracker-local-user updateConfigVar SPEEDTEST_TRACKER_LOCAL_DATABASE_USER $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD" ]; then SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD fi if [ -z "$SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME" ]; then SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_speedtest_tracker_vpn" updateConfigVar SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME $SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME fi if [ -z "$SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS" ]; then SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS=$SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS $SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS fi if [ -z "$SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD" ]; then SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD $SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD fi if [ -z "$SPEEDTEST_TRACKER_VPN_DATABASE_NAME" ]; then SPEEDTEST_TRACKER_VPN_DATABASE_NAME=speedtesttrackervpndb updateConfigVar SPEEDTEST_TRACKER_VPN_DATABASE_NAME $SPEEDTEST_TRACKER_VPN_DATABASE_NAME fi if [ -z "$SPEEDTEST_TRACKER_VPN_DATABASE_USER" ]; then SPEEDTEST_TRACKER_VPN_DATABASE_USER=speedtest-tracker-vpn-user updateConfigVar SPEEDTEST_TRACKER_VPN_DATABASE_USER $SPEEDTEST_TRACKER_VPN_DATABASE_USER fi if [ -z "$SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD" ]; then SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD $SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD fi if [ -z "$CHANGEDETECTION_ADMIN_PASSWORD" ]; then CHANGEDETECTION_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CHANGEDETECTION_ADMIN_PASSWORD $CHANGEDETECTION_ADMIN_PASSWORD fi if [ -z "$HUGINN_ADMIN_USERNAME" ]; then HUGINN_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_huginn" updateConfigVar HUGINN_ADMIN_USERNAME $HUGINN_ADMIN_USERNAME fi if [ -z "$HUGINN_ADMIN_EMAIL_ADDRESS" ]; then HUGINN_ADMIN_EMAIL_ADDRESS=$HUGINN_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar HUGINN_ADMIN_EMAIL_ADDRESS $HUGINN_ADMIN_EMAIL_ADDRESS fi if [ -z "$HUGINN_ADMIN_PASSWORD" ]; then HUGINN_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HUGINN_ADMIN_PASSWORD $HUGINN_ADMIN_PASSWORD fi if [ -z "$HUGINN_DATABASE_NAME" ]; then HUGINN_DATABASE_NAME=huginndb updateConfigVar HUGINN_DATABASE_NAME $HUGINN_DATABASE_NAME fi if [ -z "$HUGINN_DATABASE_USER" ]; then HUGINN_DATABASE_USER=huginn-user updateConfigVar HUGINN_DATABASE_USER $HUGINN_DATABASE_USER fi if [ -z "$HUGINN_DATABASE_USER_PASSWORD" ]; then HUGINN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HUGINN_DATABASE_USER_PASSWORD $HUGINN_DATABASE_USER_PASSWORD fi if [ -z "$COTURN_STATIC_SECRET" ]; then COTURN_STATIC_SECRET=$(pwgen -c -n 64 1) updateConfigVar COTURN_STATIC_SECRET $COTURN_STATIC_SECRET fi if [ -z "$PIPED_DATABASE_NAME" ]; then PIPED_DATABASE_NAME=pipeddb updateConfigVar PIPED_DATABASE_NAME $PIPED_DATABASE_NAME fi if [ -z "$PIPED_DATABASE_USER" ]; then PIPED_DATABASE_USER=piped-user updateConfigVar PIPED_DATABASE_USER $PIPED_DATABASE_USER fi if [ -z "$PIPED_DATABASE_USER_PASSWORD" ]; then PIPED_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PIPED_DATABASE_USER_PASSWORD $PIPED_DATABASE_USER_PASSWORD fi if [ -z "$GRAMPSWEB_ADMIN_USERNAME" ]; then GRAMPSWEB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_grampsweb" updateConfigVar GRAMPSWEB_ADMIN_USERNAME $GRAMPSWEB_ADMIN_USERNAME fi if [ -z "$GRAMPSWEB_ADMIN_PASSWORD" ]; then GRAMPSWEB_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GRAMPSWEB_ADMIN_PASSWORD $GRAMPSWEB_ADMIN_PASSWORD fi if [ -z "$GRAMPSWEB_ADMIN_EMAIL_ADDRESS" ]; then GRAMPSWEB_ADMIN_EMAIL_ADDRESS=$GRAMPSWEB_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar GRAMPSWEB_ADMIN_EMAIL_ADDRESS $GRAMPSWEB_ADMIN_EMAIL_ADDRESS fi if [ -z "$GRAMPSWEB_SECRET_KEY" ]; then GRAMPSWEB_SECRET_KEY=$(pwgen -c -n 43 1) updateConfigVar GRAMPSWEB_SECRET_KEY $GRAMPSWEB_SECRET_KEY fi if [ -z "$GRAMPSWEB_REDIS_PASSWORD" ]; then GRAMPSWEB_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GRAMPSWEB_REDIS_PASSWORD $GRAMPSWEB_REDIS_PASSWORD fi if [ -z "$PENPOT_REDIS_PASSWORD" ]; then PENPOT_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PENPOT_REDIS_PASSWORD $PENPOT_REDIS_PASSWORD fi if [ -z "$PENPOT_DATABASE_NAME" ]; then PENPOT_DATABASE_NAME=penpotdb updateConfigVar PENPOT_DATABASE_NAME $PENPOT_DATABASE_NAME fi if [ -z "$PENPOT_DATABASE_USER" ]; then PENPOT_DATABASE_USER=penpot-user updateConfigVar PENPOT_DATABASE_USER $PENPOT_DATABASE_USER fi if [ -z "$PENPOT_DATABASE_USER_PASSWORD" ]; then PENPOT_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PENPOT_DATABASE_USER_PASSWORD $PENPOT_DATABASE_USER_PASSWORD fi if [ -z "$PENPOT_SECRET_KEY" ]; then PENPOT_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar PENPOT_SECRET_KEY $PENPOT_SECRET_KEY fi if [ -z "$ESPOCRM_ADMIN_USERNAME" ]; then ESPOCRM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_espocrm" updateConfigVar ESPOCRM_ADMIN_USERNAME $ESPOCRM_ADMIN_USERNAME fi if [ -z "$ESPOCRM_ADMIN_PASSWORD" ]; then ESPOCRM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ESPOCRM_ADMIN_PASSWORD $ESPOCRM_ADMIN_PASSWORD fi if [ -z "$ESPOCRM_DATABASE_NAME" ]; then ESPOCRM_DATABASE_NAME=espocrmdb updateConfigVar ESPOCRM_DATABASE_NAME $ESPOCRM_DATABASE_NAME fi if [ -z "$ESPOCRM_DATABASE_ROOT_PASSWORD" ]; then ESPOCRM_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ESPOCRM_DATABASE_ROOT_PASSWORD $ESPOCRM_DATABASE_ROOT_PASSWORD fi if [ -z "$ESPOCRM_DATABASE_USER" ]; then ESPOCRM_DATABASE_USER=espocrm-user updateConfigVar ESPOCRM_DATABASE_USER $ESPOCRM_DATABASE_USER fi if [ -z "$ESPOCRM_DATABASE_USER_PASSWORD" ]; then ESPOCRM_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ESPOCRM_DATABASE_USER_PASSWORD $ESPOCRM_DATABASE_USER_PASSWORD fi if [ -z "$IMMICH_ADMIN_USERNAME" ]; then IMMICH_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_immich" updateConfigVar IMMICH_ADMIN_USERNAME $IMMICH_ADMIN_USERNAME fi if [ -z "$IMMICH_ADMIN_PASSWORD" ]; then IMMICH_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar IMMICH_ADMIN_PASSWORD $IMMICH_ADMIN_PASSWORD fi if [ -z "$IMMICH_ADMIN_EMAIL_ADDRESS" ]; then IMMICH_ADMIN_EMAIL_ADDRESS=$IMMICH_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar IMMICH_ADMIN_EMAIL_ADDRESS $IMMICH_ADMIN_EMAIL_ADDRESS fi if [ -z "$IMMICH_DATABASE_NAME" ]; then IMMICH_DATABASE_NAME=immichdb updateConfigVar IMMICH_DATABASE_NAME $IMMICH_DATABASE_NAME fi if [ -z "$IMMICH_DATABASE_USER" ]; then IMMICH_DATABASE_USER=immich-user updateConfigVar IMMICH_DATABASE_USER $IMMICH_DATABASE_USER fi if [ -z "$IMMICH_DATABASE_USER_PASSWORD" ]; then IMMICH_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar IMMICH_DATABASE_USER_PASSWORD $IMMICH_DATABASE_USER_PASSWORD fi if [ -z "$IMMICH_REDIS_PASSWORD" ]; then IMMICH_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar IMMICH_REDIS_PASSWORD $IMMICH_REDIS_PASSWORD fi if [ -z "$IMMICH_OIDC_CLIENT_SECRET" ]; then IMMICH_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar IMMICH_OIDC_CLIENT_SECRET $IMMICH_OIDC_CLIENT_SECRET fi if [ -z "$IMMICH_API_KEY" ]; then IMMICH_API_KEY=$(pwgen -c -n 41 1) updateConfigVar IMMICH_API_KEY $IMMICH_API_KEY fi if [ -z "$HOMARR_ADMIN_USERNAME" ]; then HOMARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_homarr" updateConfigVar HOMARR_ADMIN_USERNAME $HOMARR_ADMIN_USERNAME fi if [ -z "$HOMARR_ADMIN_EMAIL_ADDRESS" ]; then HOMARR_ADMIN_EMAIL_ADDRESS=$HOMARR_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar HOMARR_ADMIN_EMAIL_ADDRESS $HOMARR_ADMIN_EMAIL_ADDRESS fi if [ -z "$HOMARR_ADMIN_PASSWORD" ]; then HOMARR_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar HOMARR_ADMIN_PASSWORD $HOMARR_ADMIN_PASSWORD fi if [ -z "$HOMARR_OIDC_CLIENT_SECRET" ]; then HOMARR_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar HOMARR_OIDC_CLIENT_SECRET $HOMARR_OIDC_CLIENT_SECRET fi if [ -z "$MATOMO_ADMIN_USERNAME" ]; then MATOMO_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_matomo" updateConfigVar MATOMO_ADMIN_USERNAME $MATOMO_ADMIN_USERNAME fi if [ -z "$MATOMO_ADMIN_PASSWORD" ]; then MATOMO_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MATOMO_ADMIN_PASSWORD $MATOMO_ADMIN_PASSWORD fi if [ -z "$MATOMO_ADMIN_EMAIL_ADDRESS" ]; then MATOMO_ADMIN_EMAIL_ADDRESS=$MATOMO_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MATOMO_ADMIN_EMAIL_ADDRESS $MATOMO_ADMIN_EMAIL_ADDRESS fi if [ -z "$MATOMO_DATABASE_NAME" ]; then MATOMO_DATABASE_NAME=matomodb updateConfigVar MATOMO_DATABASE_NAME $MATOMO_DATABASE_NAME fi if [ -z "$MATOMO_DATABASE_ROOT_PASSWORD" ]; then MATOMO_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MATOMO_DATABASE_ROOT_PASSWORD $MATOMO_DATABASE_ROOT_PASSWORD fi if [ -z "$MATOMO_DATABASE_USER" ]; then MATOMO_DATABASE_USER=matomo-user updateConfigVar MATOMO_DATABASE_USER $MATOMO_DATABASE_USER fi if [ -z "$MATOMO_DATABASE_USER_PASSWORD" ]; then MATOMO_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MATOMO_DATABASE_USER_PASSWORD $MATOMO_DATABASE_USER_PASSWORD fi if [ -z "$PASTEFY_ADMIN_USERNAME" ]; then PASTEFY_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_pastefy" updateConfigVar PASTEFY_ADMIN_USERNAME $PASTEFY_ADMIN_USERNAME fi if [ -z "$PASTEFY_ADMIN_PASSWORD" ]; then PASTEFY_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PASTEFY_ADMIN_PASSWORD $PASTEFY_ADMIN_PASSWORD fi if [ -z "$PASTEFY_ADMIN_EMAIL_ADDRESS" ]; then PASTEFY_ADMIN_EMAIL_ADDRESS=$PASTEFY_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar PASTEFY_ADMIN_EMAIL_ADDRESS $PASTEFY_ADMIN_EMAIL_ADDRESS fi if [ -z "$PASTEFY_DATABASE_NAME" ]; then PASTEFY_DATABASE_NAME=pastefydb updateConfigVar PASTEFY_DATABASE_NAME $PASTEFY_DATABASE_NAME fi if [ -z "$PASTEFY_DATABASE_ROOT_PASSWORD" ]; then PASTEFY_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PASTEFY_DATABASE_ROOT_PASSWORD $PASTEFY_DATABASE_ROOT_PASSWORD fi if [ -z "$PASTEFY_DATABASE_USER" ]; then PASTEFY_DATABASE_USER=pastefy-user updateConfigVar PASTEFY_DATABASE_USER $PASTEFY_DATABASE_USER fi if [ -z "$PASTEFY_DATABASE_USER_PASSWORD" ]; then PASTEFY_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PASTEFY_DATABASE_USER_PASSWORD $PASTEFY_DATABASE_USER_PASSWORD fi if [ -z "$AISTACK_MINDSDB_ADMIN_USERNAME" ]; then AISTACK_MINDSDB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_aistack_mindsdb" updateConfigVar AISTACK_MINDSDB_ADMIN_USERNAME $AISTACK_MINDSDB_ADMIN_USERNAME fi if [ -z "$AISTACK_MINDSDB_ADMIN_PASSWORD" ]; then AISTACK_MINDSDB_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AISTACK_MINDSDB_ADMIN_PASSWORD $AISTACK_MINDSDB_ADMIN_PASSWORD fi if [ -z "$AISTACK_MINDSDB_ADMIN_EMAIL_ADDRESS" ]; then AISTACK_MINDSDB_ADMIN_EMAIL_ADDRESS=$AISTACK_MINDSDB_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AISTACK_MINDSDB_ADMIN_EMAIL_ADDRESS $AISTACK_MINDSDB_ADMIN_EMAIL_ADDRESS fi if [ -z "$AISTACK_MINDSDB_DATABASE_NAME" ]; then AISTACK_MINDSDB_DATABASE_NAME=mindsdb updateConfigVar AISTACK_MINDSDB_DATABASE_NAME $AISTACK_MINDSDB_DATABASE_NAME fi if [ -z "$AISTACK_MINDSDB_DATABASE_USER" ]; then AISTACK_MINDSDB_DATABASE_USER=mindsdb-user updateConfigVar AISTACK_MINDSDB_DATABASE_USER $AISTACK_MINDSDB_DATABASE_USER fi if [ -z "$AISTACK_MINDSDB_DATABASE_USER_PASSWORD" ]; then AISTACK_MINDSDB_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AISTACK_MINDSDB_DATABASE_USER_PASSWORD $AISTACK_MINDSDB_DATABASE_USER_PASSWORD fi if [ -z "$AISTACK_LANGFUSE_ADMIN_USERNAME" ]; then AISTACK_LANGFUSE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_aistack_langfuse" updateConfigVar AISTACK_LANGFUSE_ADMIN_USERNAME $AISTACK_LANGFUSE_ADMIN_USERNAME fi if [ -z "$AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS" ]; then AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS=$AISTACK_LANGFUSE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS $AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS fi if [ -z "$AISTACK_LANGFUSE_ADMIN_PASSWORD" ]; then AISTACK_LANGFUSE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AISTACK_LANGFUSE_ADMIN_PASSWORD $AISTACK_LANGFUSE_ADMIN_PASSWORD fi if [ -z "$AISTACK_LANGFUSE_DATABASE_NAME" ]; then AISTACK_LANGFUSE_DATABASE_NAME=langfuse updateConfigVar AISTACK_LANGFUSE_DATABASE_NAME $AISTACK_LANGFUSE_DATABASE_NAME fi if [ -z "$AISTACK_OPENWEBUI_ADMIN_USERNAME" ]; then AISTACK_OPENWEBUI_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_aistack_openwebui" updateConfigVar AISTACK_OPENWEBUI_ADMIN_USERNAME $AISTACK_OPENWEBUI_ADMIN_USERNAME fi if [ -z "$AISTACK_OPENWEBUI_ADMIN_EMAIL_ADDRESS" ]; then AISTACK_OPENWEBUI_ADMIN_EMAIL_ADDRESS=$AISTACK_OPENWEBUI_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AISTACK_OPENWEBUI_ADMIN_EMAIL_ADDRESS $AISTACK_OPENWEBUI_ADMIN_EMAIL_ADDRESS fi if [ -z "$AISTACK_OPENWEBUI_ADMIN_PASSWORD" ]; then AISTACK_OPENWEBUI_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AISTACK_OPENWEBUI_ADMIN_PASSWORD $AISTACK_OPENWEBUI_ADMIN_PASSWORD fi if [ -z "$AISTACK_OPENWEBUI_OIDC_CLIENT_ID" ]; then AISTACK_OPENWEBUI_OIDC_CLIENT_ID=aistack-openwebui updateConfigVar AISTACK_OPENWEBUI_OIDC_CLIENT_ID $AISTACK_OPENWEBUI_OIDC_CLIENT_ID fi if [ -z "$AISTACK_OPENWEBUI_OIDC_CLIENT_SECRET" ]; then AISTACK_OPENWEBUI_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar AISTACK_OPENWEBUI_OIDC_CLIENT_SECRET $AISTACK_OPENWEBUI_OIDC_CLIENT_SECRET fi if [ -z "$AISTACK_REDIS_PASSWORD" ]; then AISTACK_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AISTACK_REDIS_PASSWORD $AISTACK_REDIS_PASSWORD fi if [ -z "$PIXELFED_DATABASE_NAME" ]; then PIXELFED_DATABASE_NAME=pixelfeddb updateConfigVar PIXELFED_DATABASE_NAME $PIXELFED_DATABASE_NAME fi if [ -z "$PIXELFED_DATABASE_ROOT_PASSWORD" ]; then PIXELFED_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PIXELFED_DATABASE_ROOT_PASSWORD $PIXELFED_DATABASE_ROOT_PASSWORD fi if [ -z "$PIXELFED_DATABASE_USER" ]; then PIXELFED_DATABASE_USER=pixelfed-user updateConfigVar PIXELFED_DATABASE_USER $PIXELFED_DATABASE_USER fi if [ -z "$PIXELFED_DATABASE_USER_PASSWORD" ]; then PIXELFED_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PIXELFED_DATABASE_USER_PASSWORD $PIXELFED_DATABASE_USER_PASSWORD fi if [ -z "$PIXELFED_REDIS_PASSWORD" ]; then PIXELFED_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PIXELFED_REDIS_PASSWORD $PIXELFED_REDIS_PASSWORD fi if [ -z "$PIXELFED_APP_KEY" ]; then PIXELFED_APP_KEY=$(openssl rand -base64 32) updateConfigVar PIXELFED_APP_KEY $PIXELFED_APP_KEY fi if [ -z "$YAMTRACK_ADMIN_USERNAME" ]; then YAMTRACK_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_yamtrack" updateConfigVar YAMTRACK_ADMIN_USERNAME $YAMTRACK_ADMIN_USERNAME fi if [ -z "$YAMTRACK_ADMIN_PASSWORD" ]; then YAMTRACK_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar YAMTRACK_ADMIN_PASSWORD $YAMTRACK_ADMIN_PASSWORD fi if [ -z "$YAMTRACK_REDIS_PASSWORD" ]; then YAMTRACK_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar YAMTRACK_REDIS_PASSWORD $YAMTRACK_REDIS_PASSWORD fi if [ -z "$YAMTRACK_DATABASE_NAME" ]; then YAMTRACK_DATABASE_NAME=yamtrackdb updateConfigVar YAMTRACK_DATABASE_NAME $YAMTRACK_DATABASE_NAME fi if [ -z "$YAMTRACK_DATABASE_USER" ]; then YAMTRACK_DATABASE_USER=yamtrack-user updateConfigVar YAMTRACK_DATABASE_USER $YAMTRACK_DATABASE_USER fi if [ -z "$YAMTRACK_DATABASE_USER_PASSWORD" ]; then YAMTRACK_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar YAMTRACK_DATABASE_USER_PASSWORD $YAMTRACK_DATABASE_USER_PASSWORD fi if [ -z "$YAMTRACK_SECRET_KEY" ]; then YAMTRACK_SECRET_KEY=$(openssl rand -hex 25) updateConfigVar YAMTRACK_SECRET_KEY $YAMTRACK_SECRET_KEY fi if [ -z "$YAMTRACK_OIDC_CLIENT_ID" ]; then YAMTRACK_OIDC_CLIENT_ID=yamtrack updateConfigVar YAMTRACK_OIDC_CLIENT_ID $YAMTRACK_OIDC_CLIENT_ID fi if [ -z "$YAMTRACK_OIDC_CLIENT_SECRET" ]; then YAMTRACK_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar YAMTRACK_OIDC_CLIENT_SECRET $YAMTRACK_OIDC_CLIENT_SECRET fi if [ -z "$SONARR_ADMIN_USERNAME" ]; then SONARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_sonarr" updateConfigVar SONARR_ADMIN_USERNAME $SONARR_ADMIN_USERNAME fi if [ -z "$SONARR_ADMIN_PASSWORD" ]; then SONARR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SONARR_ADMIN_PASSWORD $SONARR_ADMIN_PASSWORD fi if [ -z "$RADARR_ADMIN_USERNAME" ]; then RADARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_radarr" updateConfigVar RADARR_ADMIN_USERNAME $RADARR_ADMIN_USERNAME fi if [ -z "$RADARR_ADMIN_PASSWORD" ]; then RADARR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RADARR_ADMIN_PASSWORD $RADARR_ADMIN_PASSWORD fi if [ -z "$LIDARR_ADMIN_USERNAME" ]; then LIDARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_lidarr" updateConfigVar LIDARR_ADMIN_USERNAME $LIDARR_ADMIN_USERNAME fi if [ -z "$LIDARR_ADMIN_PASSWORD" ]; then LIDARR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIDARR_ADMIN_PASSWORD $LIDARR_ADMIN_PASSWORD fi if [ -z "$READARR_ADMIN_USERNAME" ]; then READARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_readarr" updateConfigVar READARR_ADMIN_USERNAME $READARR_ADMIN_USERNAME fi if [ -z "$READARR_ADMIN_PASSWORD" ]; then READARR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar READARR_ADMIN_PASSWORD $READARR_ADMIN_PASSWORD fi if [ -z "$BAZARR_ADMIN_USERNAME" ]; then BAZARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_bazarr" updateConfigVar BAZARR_ADMIN_USERNAME $BAZARR_ADMIN_USERNAME fi if [ -z "$BAZARR_ADMIN_PASSWORD" ]; then BAZARR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BAZARR_ADMIN_PASSWORD $BAZARR_ADMIN_PASSWORD fi if [ -z "$MYLAR3_ADMIN_USERNAME" ]; then MYLAR3_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_mylar3" updateConfigVar MYLAR3_ADMIN_USERNAME $MYLAR3_ADMIN_USERNAME fi if [ -z "$MYLAR3_ADMIN_PASSWORD" ]; then MYLAR3_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MYLAR3_ADMIN_PASSWORD $MYLAR3_ADMIN_PASSWORD fi if [ -z "$PROWLARR_ADMIN_USERNAME" ]; then PROWLARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_prowlarr" updateConfigVar PROWLARR_ADMIN_USERNAME $PROWLARR_ADMIN_USERNAME fi if [ -z "$PROWLARR_ADMIN_PASSWORD" ]; then PROWLARR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PROWLARR_ADMIN_PASSWORD $PROWLARR_ADMIN_PASSWORD fi if [ -z "$SABNZBD_ADMIN_USERNAME" ]; then SABNZBD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_sabnzbd" updateConfigVar SABNZBD_ADMIN_USERNAME $SABNZBD_ADMIN_USERNAME fi if [ -z "$SABNZBD_ADMIN_PASSWORD" ]; then SABNZBD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SABNZBD_ADMIN_PASSWORD $SABNZBD_ADMIN_PASSWORD fi if [ -z "$QBITTORRENT_ADMIN_USERNAME" ]; then QBITTORRENT_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_qbittorrent" updateConfigVar QBITTORRENT_ADMIN_USERNAME $QBITTORRENT_ADMIN_USERNAME fi if [ -z "$QBITTORRENT_ADMIN_PASSWORD" ]; then QBITTORRENT_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar QBITTORRENT_ADMIN_PASSWORD $QBITTORRENT_ADMIN_PASSWORD fi if [ -z "$OMBI_ADMIN_USERNAME" ]; then OMBI_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_ombi" updateConfigVar OMBI_ADMIN_USERNAME $OMBI_ADMIN_USERNAME fi if [ -z "$OMBI_ADMIN_PASSWORD" ]; then OMBI_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OMBI_ADMIN_PASSWORD $OMBI_ADMIN_PASSWORD fi if [ -z "$OMBI_ADMIN_EMAIL_ADDRESS" ]; then OMBI_ADMIN_EMAIL_ADDRESS=$OMBI_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar OMBI_ADMIN_EMAIL_ADDRESS $OMBI_ADMIN_EMAIL_ADDRESS fi if [ -z "$OMBI_DATABASE_NAME" ]; then OMBI_DATABASE_NAME=ombidb updateConfigVar OMBI_DATABASE_NAME $OMBI_DATABASE_NAME fi if [ -z "$OMBI_DATABASE_ROOT_PASSWORD" ]; then OMBI_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OMBI_DATABASE_ROOT_PASSWORD $OMBI_DATABASE_ROOT_PASSWORD fi if [ -z "$OMBI_DATABASE_USER" ]; then OMBI_DATABASE_USER=ombi-user updateConfigVar OMBI_DATABASE_USER $OMBI_DATABASE_USER fi if [ -z "$OMBI_DATABASE_USER_PASSWORD" ]; then OMBI_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OMBI_DATABASE_USER_PASSWORD $OMBI_DATABASE_USER_PASSWORD fi if [ -z "$MESHCENTRAL_DATABASE_NAME" ]; then MESHCENTRAL_DATABASE_NAME=meshcentraldb updateConfigVar MESHCENTRAL_DATABASE_NAME $MESHCENTRAL_DATABASE_NAME fi if [ -z "$MESHCENTRAL_DATABASE_ROOT_PASSWORD" ]; then MESHCENTRAL_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MESHCENTRAL_DATABASE_ROOT_PASSWORD $MESHCENTRAL_DATABASE_ROOT_PASSWORD fi if [ -z "$MESHCENTRAL_DATABASE_USER" ]; then MESHCENTRAL_DATABASE_USER=meshcentral-user updateConfigVar MESHCENTRAL_DATABASE_USER $MESHCENTRAL_DATABASE_USER fi if [ -z "$MESHCENTRAL_DATABASE_USER_PASSWORD" ]; then MESHCENTRAL_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MESHCENTRAL_DATABASE_USER_PASSWORD $MESHCENTRAL_DATABASE_USER_PASSWORD fi if [ -z "$NAVIDROME_ADMIN_USERNAME" ]; then NAVIDROME_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_navidrome" updateConfigVar NAVIDROME_ADMIN_USERNAME $NAVIDROME_ADMIN_USERNAME fi if [ -z "$NAVIDROME_ADMIN_PASSWORD" ]; then NAVIDROME_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NAVIDROME_ADMIN_PASSWORD $NAVIDROME_ADMIN_PASSWORD fi if [ -z "$NAVIDROME_ADMIN_EMAIL_ADDRESS" ]; then NAVIDROME_ADMIN_EMAIL_ADDRESS=$NAVIDROME_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar NAVIDROME_ADMIN_EMAIL_ADDRESS $NAVIDROME_ADMIN_EMAIL_ADDRESS fi if [ -z "$ADMINER_DATABASE_NAME" ]; then ADMINER_DATABASE_NAME=adminerdb updateConfigVar ADMINER_DATABASE_NAME $ADMINER_DATABASE_NAME fi if [ -z "$ADMINER_DATABASE_ROOT_PASSWORD" ]; then ADMINER_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ADMINER_DATABASE_ROOT_PASSWORD $ADMINER_DATABASE_ROOT_PASSWORD fi if [ -z "$ADMINER_DATABASE_USER" ]; then ADMINER_DATABASE_USER=adminer-user updateConfigVar ADMINER_DATABASE_USER $ADMINER_DATABASE_USER fi if [ -z "$ADMINER_DATABASE_USER_PASSWORD" ]; then ADMINER_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ADMINER_DATABASE_USER_PASSWORD $ADMINER_DATABASE_USER_PASSWORD fi if [ -z "$BUDIBASE_API_ENCRYPTION_KEY" ]; then BUDIBASE_API_ENCRYPTION_KEY=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_API_ENCRYPTION_KEY $BUDIBASE_API_ENCRYPTION_KEY fi if [ -z "$BUDIBASE_INTERNAL_API_KEY" ]; then BUDIBASE_INTERNAL_API_KEY=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_INTERNAL_API_KEY $BUDIBASE_INTERNAL_API_KEY fi if [ -z "$BUDIBASE_JWT_SECRET" ]; then BUDIBASE_JWT_SECRET=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_JWT_SECRET $BUDIBASE_JWT_SECRET fi if [ -z "$BUDIBASE_ADMIN_EMAIL_ADDRESS" ]; then BUDIBASE_ADMIN_EMAIL_ADDRESS=$ADMIN_USERNAME_BASE"_budibase"@$HOMESERVER_DOMAIN updateConfigVar BUDIBASE_ADMIN_EMAIL_ADDRESS $BUDIBASE_ADMIN_EMAIL_ADDRESS fi if [ -z "$BUDIBASE_ADMIN_PASSWORD" ]; then BUDIBASE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_ADMIN_PASSWORD $BUDIBASE_ADMIN_PASSWORD fi if [ -z "$BUDIBASE_COUCHDB_USER" ]; then BUDIBASE_COUCHDB_USER=budibase-user updateConfigVar BUDIBASE_COUCHDB_USER $BUDIBASE_COUCHDB_USER fi if [ -z "$BUDIBASE_COUCHDB_PASSWORD" ]; then BUDIBASE_COUCHDB_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_COUCHDB_PASSWORD $BUDIBASE_COUCHDB_PASSWORD fi if [ -z "$BUDIBASE_MINIO_ACCESS_KEY" ]; then BUDIBASE_MINIO_ACCESS_KEY=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_MINIO_ACCESS_KEY $BUDIBASE_MINIO_ACCESS_KEY fi if [ -z "$BUDIBASE_MINIO_SECRET_KEY" ]; then BUDIBASE_MINIO_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_MINIO_SECRET_KEY $BUDIBASE_MINIO_SECRET_KEY fi if [ -z "$BUDIBASE_REDIS_PASSWORD" ]; then BUDIBASE_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_REDIS_PASSWORD $BUDIBASE_REDIS_PASSWORD fi if [ -z "$BUDIBASE_DATABASE_NAME" ]; then BUDIBASE_DATABASE_NAME=budibasedb updateConfigVar BUDIBASE_DATABASE_NAME $BUDIBASE_DATABASE_NAME fi if [ -z "$BUDIBASE_DATABASE_ROOT_PASSWORD" ]; then BUDIBASE_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_DATABASE_ROOT_PASSWORD $BUDIBASE_DATABASE_ROOT_PASSWORD fi if [ -z "$BUDIBASE_DATABASE_USER" ]; then BUDIBASE_DATABASE_USER=budibase-user updateConfigVar BUDIBASE_DATABASE_USER $BUDIBASE_DATABASE_USER fi if [ -z "$BUDIBASE_DATABASE_USER_PASSWORD" ]; then BUDIBASE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_DATABASE_USER_PASSWORD $BUDIBASE_DATABASE_USER_PASSWORD fi if [ -z "$AUDIOBOOKSHELF_ADMIN_USERNAME" ]; then AUDIOBOOKSHELF_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_audiobookshelf" updateConfigVar AUDIOBOOKSHELF_ADMIN_USERNAME $AUDIOBOOKSHELF_ADMIN_USERNAME fi if [ -z "$AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS" ]; then AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS=$AUDIOBOOKSHELF_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS $AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS fi if [ -z "$AUDIOBOOKSHELF_ADMIN_PASSWORD" ]; then AUDIOBOOKSHELF_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUDIOBOOKSHELF_ADMIN_PASSWORD $AUDIOBOOKSHELF_ADMIN_PASSWORD fi if [ -z "$AUDIOBOOKSHELF_JWT_SECRET" ]; then AUDIOBOOKSHELF_JWT_SECRET=$(pwgen -c -n 32 1) updateConfigVar AUDIOBOOKSHELF_JWT_SECRET $AUDIOBOOKSHELF_JWT_SECRET fi if [ -z "$AUDIOBOOKSHELF_OIDC_CLIENT_SECRET" ]; then AUDIOBOOKSHELF_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar AUDIOBOOKSHELF_OIDC_CLIENT_SECRET $AUDIOBOOKSHELF_OIDC_CLIENT_SECRET fi if [ -z "$STANDARDNOTES_REDIS_PASSWORD" ]; then STANDARDNOTES_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar STANDARDNOTES_REDIS_PASSWORD $STANDARDNOTES_REDIS_PASSWORD fi if [ -z "$STANDARDNOTES_DATABASE_NAME" ]; then STANDARDNOTES_DATABASE_NAME=standardnotesdb updateConfigVar STANDARDNOTES_DATABASE_NAME $STANDARDNOTES_DATABASE_NAME fi if [ -z "$STANDARDNOTES_DATABASE_ROOT_PASSWORD" ]; then STANDARDNOTES_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar STANDARDNOTES_DATABASE_ROOT_PASSWORD $STANDARDNOTES_DATABASE_ROOT_PASSWORD fi if [ -z "$STANDARDNOTES_DATABASE_USER" ]; then STANDARDNOTES_DATABASE_USER=standardnotes-user updateConfigVar STANDARDNOTES_DATABASE_USER $STANDARDNOTES_DATABASE_USER fi if [ -z "$STANDARDNOTES_DATABASE_USER_PASSWORD" ]; then STANDARDNOTES_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar STANDARDNOTES_DATABASE_USER_PASSWORD $STANDARDNOTES_DATABASE_USER_PASSWORD fi if [ -z "$METABASE_ADMIN_USERNAME" ]; then METABASE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_metabase" updateConfigVar METABASE_ADMIN_USERNAME $METABASE_ADMIN_USERNAME fi if [ -z "$METABASE_ADMIN_EMAIL_ADDRESS" ]; then METABASE_ADMIN_EMAIL_ADDRESS=$METABASE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar METABASE_ADMIN_EMAIL_ADDRESS $METABASE_ADMIN_EMAIL_ADDRESS fi if [ -z "$METABASE_ADMIN_PASSWORD" ]; then METABASE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar METABASE_ADMIN_PASSWORD $METABASE_ADMIN_PASSWORD fi if [ -z "$METABASE_DATABASE_NAME" ]; then METABASE_DATABASE_NAME=metabasedb updateConfigVar METABASE_DATABASE_NAME $METABASE_DATABASE_NAME fi if [ -z "$METABASE_DATABASE_USER" ]; then METABASE_DATABASE_USER=metabase-user updateConfigVar METABASE_DATABASE_USER $METABASE_DATABASE_USER fi if [ -z "$METABASE_DATABASE_USER_PASSWORD" ]; then METABASE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar METABASE_DATABASE_USER_PASSWORD $METABASE_DATABASE_USER_PASSWORD fi if [ -z "$METABASE_API_KEY" ]; then METABASE_API_KEY=mb_$(openssl rand -base64 32) updateConfigVar METABASE_API_KEY $METABASE_API_KEY fi if [ -z "$KANBOARD_ADMIN_USERNAME" ]; then KANBOARD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_kanboard" updateConfigVar KANBOARD_ADMIN_USERNAME $KANBOARD_ADMIN_USERNAME fi if [ -z "$KANBOARD_ADMIN_EMAIL_ADDRESS" ]; then KANBOARD_ADMIN_EMAIL_ADDRESS=$KANBOARD_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar KANBOARD_ADMIN_EMAIL_ADDRESS $KANBOARD_ADMIN_EMAIL_ADDRESS fi if [ -z "$KANBOARD_ADMIN_PASSWORD" ]; then KANBOARD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KANBOARD_ADMIN_PASSWORD $KANBOARD_ADMIN_PASSWORD fi if [ -z "$KANBOARD_DATABASE_NAME" ]; then KANBOARD_DATABASE_NAME=kanboarddb updateConfigVar KANBOARD_DATABASE_NAME $KANBOARD_DATABASE_NAME fi if [ -z "$KANBOARD_DATABASE_USER" ]; then KANBOARD_DATABASE_USER=kanboard-user updateConfigVar KANBOARD_DATABASE_USER $KANBOARD_DATABASE_USER fi if [ -z "$KANBOARD_DATABASE_USER_PASSWORD" ]; then KANBOARD_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KANBOARD_DATABASE_USER_PASSWORD $KANBOARD_DATABASE_USER_PASSWORD fi if [ -z "$WEKAN_OIDC_CLIENT_SECRET" ]; then WEKAN_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar WEKAN_OIDC_CLIENT_SECRET $WEKAN_OIDC_CLIENT_SECRET fi if [ -z "$WEKAN_DATABASE_NAME" ]; then WEKAN_DATABASE_NAME=wekandb updateConfigVar WEKAN_DATABASE_NAME $WEKAN_DATABASE_NAME fi if [ -z "$WEKAN_DATABASE_USER" ]; then WEKAN_DATABASE_USER=wekan-user updateConfigVar WEKAN_DATABASE_USER $WEKAN_DATABASE_USER fi if [ -z "$WEKAN_DATABASE_USER_PASSWORD" ]; then WEKAN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WEKAN_DATABASE_USER_PASSWORD $WEKAN_DATABASE_USER_PASSWORD fi if [ -z "$REVOLT_DATABASE_NAME" ]; then REVOLT_DATABASE_NAME=revoltdb updateConfigVar REVOLT_DATABASE_NAME $REVOLT_DATABASE_NAME fi if [ -z "$REVOLT_DATABASE_USER" ]; then REVOLT_DATABASE_USER=revolt-user updateConfigVar REVOLT_DATABASE_USER $REVOLT_DATABASE_USER fi if [ -z "$REVOLT_DATABASE_USER_PASSWORD" ]; then REVOLT_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar REVOLT_DATABASE_USER_PASSWORD $REVOLT_DATABASE_USER_PASSWORD fi if [ -z "$REVOLT_REDIS_PASSWORD" ]; then REVOLT_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar REVOLT_REDIS_PASSWORD $REVOLT_REDIS_PASSWORD fi if [ -z "$REVOLT_RABBITMQ_USERNAME" ]; then REVOLT_RABBITMQ_USERNAME=$ADMIN_USERNAME_BASE"_revolt_rmq" updateConfigVar REVOLT_RABBITMQ_USERNAME $REVOLT_RABBITMQ_USERNAME fi if [ -z "$REVOLT_RABBITMQ_PASSWORD" ]; then REVOLT_RABBITMQ_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar REVOLT_RABBITMQ_PASSWORD $REVOLT_RABBITMQ_PASSWORD fi if [ -z "$REVOLT_MINIO_KEY" ]; then REVOLT_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar REVOLT_MINIO_KEY $REVOLT_MINIO_KEY fi if [ -z "$REVOLT_MINIO_SECRET" ]; then REVOLT_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar REVOLT_MINIO_SECRET $REVOLT_MINIO_SECRET fi if [ -z "$FRAPPE_HR_ADMIN_USERNAME" ]; then FRAPPE_HR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_frappe_hr" updateConfigVar FRAPPE_HR_ADMIN_USERNAME $FRAPPE_HR_ADMIN_USERNAME fi if [ -z "$FRAPPE_HR_ADMIN_EMAIL_ADDRESS" ]; then FRAPPE_HR_ADMIN_EMAIL_ADDRESS=$FRAPPE_HR_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar FRAPPE_HR_ADMIN_EMAIL_ADDRESS $FRAPPE_HR_ADMIN_EMAIL_ADDRESS fi if [ -z "$FRAPPE_HR_ADMIN_PASSWORD" ]; then FRAPPE_HR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRAPPE_HR_ADMIN_PASSWORD $FRAPPE_HR_ADMIN_PASSWORD fi if [ -z "$FRAPPE_HR_DATABASE_NAME" ]; then FRAPPE_HR_DATABASE_NAME=frappe-hrdb updateConfigVar FRAPPE_HR_DATABASE_NAME $FRAPPE_HR_DATABASE_NAME fi if [ -z "$FRAPPE_HR_DATABASE_ROOT_PASSWORD" ]; then FRAPPE_HR_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRAPPE_HR_DATABASE_ROOT_PASSWORD $FRAPPE_HR_DATABASE_ROOT_PASSWORD fi if [ -z "$FRAPPE_HR_DATABASE_USER" ]; then FRAPPE_HR_DATABASE_USER=frappe-hrdb updateConfigVar FRAPPE_HR_DATABASE_USER $FRAPPE_HR_DATABASE_USER fi if [ -z "$FRAPPE_HR_DATABASE_USER_PASSWORD" ]; then FRAPPE_HR_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRAPPE_HR_DATABASE_USER_PASSWORD $FRAPPE_HR_DATABASE_USER_PASSWORD fi if [ -z "$FRAPPE_HR_REDIS_PASSWORD" ]; then FRAPPE_HR_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRAPPE_HR_REDIS_PASSWORD $FRAPPE_HR_REDIS_PASSWORD fi if [ -z "$MINTHCM_ADMIN_USERNAME" ]; then MINTHCM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_minthcm" updateConfigVar MINTHCM_ADMIN_USERNAME $MINTHCM_ADMIN_USERNAME fi if [ -z "$MINTHCM_ADMIN_PASSWORD" ]; then MINTHCM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINTHCM_ADMIN_PASSWORD $MINTHCM_ADMIN_PASSWORD fi if [ -z "$MINTHCM_ADMIN_EMAIL_ADDRESS" ]; then MINTHCM_ADMIN_EMAIL_ADDRESS=$MINTHCM_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MINTHCM_ADMIN_EMAIL_ADDRESS $MINTHCM_ADMIN_EMAIL_ADDRESS fi if [ -z "$MINTHCM_DATABASE_NAME" ]; then MINTHCM_DATABASE_NAME=minthcmdb updateConfigVar MINTHCM_DATABASE_NAME $MINTHCM_DATABASE_NAME fi if [ -z "$MINTHCM_DATABASE_ROOT_PASSWORD" ]; then MINTHCM_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINTHCM_DATABASE_ROOT_PASSWORD $MINTHCM_DATABASE_ROOT_PASSWORD fi if [ -z "$MINTHCM_DATABASE_USER" ]; then MINTHCM_DATABASE_USER=minthcm-user updateConfigVar MINTHCM_DATABASE_USER $MINTHCM_DATABASE_USER fi if [ -z "$MINTHCM_DATABASE_USER_PASSWORD" ]; then MINTHCM_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINTHCM_DATABASE_USER_PASSWORD $MINTHCM_DATABASE_USER_PASSWORD fi if [ -z "$MINTHCM_ES_USER" ]; then MINTHCM_ES_USER=minthcm-user updateConfigVar MINTHCM_ES_USER $MINTHCM_ES_USER fi if [ -z "$MINTHCM_ES_USER_PASSWORD" ]; then MINTHCM_ES_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINTHCM_ES_USER_PASSWORD $MINTHCM_ES_USER_PASSWORD fi if [ -z "$CLOUDBEAVER_ADMIN_USERNAME" ]; then CLOUDBEAVER_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_cloudbeaver" updateConfigVar CLOUDBEAVER_ADMIN_USERNAME $CLOUDBEAVER_ADMIN_USERNAME fi if [ -z "$CLOUDBEAVER_ADMIN_PASSWORD" ]; then CLOUDBEAVER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CLOUDBEAVER_ADMIN_PASSWORD $CLOUDBEAVER_ADMIN_PASSWORD fi if [ -z "$DBGATE_ADMIN_USERNAME" ]; then DBGATE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_dbgate" updateConfigVar DBGATE_ADMIN_USERNAME $DBGATE_ADMIN_USERNAME fi if [ -z "$DBGATE_ADMIN_PASSWORD" ]; then DBGATE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DBGATE_ADMIN_PASSWORD $DBGATE_ADMIN_PASSWORD fi if [ -z "$TWENTY_ADMIN_USERNAME" ]; then TWENTY_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_twenty" updateConfigVar TWENTY_ADMIN_USERNAME $TWENTY_ADMIN_USERNAME fi if [ -z "$TWENTY_ADMIN_EMAIL_ADDRESS" ]; then TWENTY_ADMIN_EMAIL_ADDRESS=$TWENTY_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar TWENTY_ADMIN_EMAIL_ADDRESS $TWENTY_ADMIN_EMAIL_ADDRESS fi if [ -z "$TWENTY_ADMIN_PASSWORD" ]; then TWENTY_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TWENTY_ADMIN_PASSWORD $TWENTY_ADMIN_PASSWORD fi if [ -z "$TWENTY_DATABASE_NAME" ]; then TWENTY_DATABASE_NAME=twentydb updateConfigVar TWENTY_DATABASE_NAME $TWENTY_DATABASE_NAME fi if [ -z "$TWENTY_DATABASE_USER" ]; then TWENTY_DATABASE_USER=twenty-user updateConfigVar TWENTY_DATABASE_USER $TWENTY_DATABASE_USER fi if [ -z "$TWENTY_DATABASE_USER_PASSWORD" ]; then TWENTY_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TWENTY_DATABASE_USER_PASSWORD $TWENTY_DATABASE_USER_PASSWORD fi if [ -z "$TWENTY_REDIS_PASSWORD" ]; then TWENTY_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TWENTY_REDIS_PASSWORD $TWENTY_REDIS_PASSWORD fi if [ -z "$TWENTY_MINIO_KEY" ]; then TWENTY_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar TWENTY_MINIO_KEY $TWENTY_MINIO_KEY fi if [ -z "$TWENTY_MINIO_SECRET" ]; then TWENTY_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar TWENTY_MINIO_SECRET $TWENTY_MINIO_SECRET fi if [ -z "$TWENTY_APP_SECRET" ]; then TWENTY_APP_SECRET=$(openssl rand -base64 32) updateConfigVar TWENTY_APP_SECRET $TWENTY_APP_SECRET fi if [ -z "$TWENTY_ENCRYPTION_KEY" ]; then TWENTY_ENCRYPTION_KEY=$(openssl rand -base64 32) updateConfigVar TWENTY_ENCRYPTION_KEY $TWENTY_ENCRYPTION_KEY fi if [ -z "$ODOO_ADMIN_USERNAME" ]; then ODOO_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_odoo" updateConfigVar ODOO_ADMIN_USERNAME $ODOO_ADMIN_USERNAME fi if [ -z "$ODOO_ADMIN_EMAIL_ADDRESS" ]; then ODOO_ADMIN_EMAIL_ADDRESS=$ODOO_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar ODOO_ADMIN_EMAIL_ADDRESS $ODOO_ADMIN_EMAIL_ADDRESS fi if [ -z "$ODOO_ADMIN_PASSWORD" ]; then ODOO_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ODOO_ADMIN_PASSWORD $ODOO_ADMIN_PASSWORD fi if [ -z "$ODOO_DATABASE_NAME" ]; then ODOO_DATABASE_NAME=odoodb updateConfigVar ODOO_DATABASE_NAME $ODOO_DATABASE_NAME fi if [ -z "$ODOO_DATABASE_USER" ]; then ODOO_DATABASE_USER=odoo-user updateConfigVar ODOO_DATABASE_USER $ODOO_DATABASE_USER fi if [ -z "$ODOO_DATABASE_USER_PASSWORD" ]; then ODOO_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ODOO_DATABASE_USER_PASSWORD $ODOO_DATABASE_USER_PASSWORD fi if [ -z "$CALCOM_ADMIN_USERNAME" ]; then CALCOM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_calcom" updateConfigVar CALCOM_ADMIN_USERNAME $CALCOM_ADMIN_USERNAME fi if [ -z "$CALCOM_ADMIN_EMAIL_ADDRESS" ]; then CALCOM_ADMIN_EMAIL_ADDRESS=$CALCOM_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar CALCOM_ADMIN_EMAIL_ADDRESS $CALCOM_ADMIN_EMAIL_ADDRESS fi if [ -z "$CALCOM_ADMIN_PASSWORD" ]; then CALCOM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CALCOM_ADMIN_PASSWORD $CALCOM_ADMIN_PASSWORD fi if [ -z "$CALCOM_DATABASE_NAME" ]; then CALCOM_DATABASE_NAME=calcomdb updateConfigVar CALCOM_DATABASE_NAME $CALCOM_DATABASE_NAME fi if [ -z "$CALCOM_DATABASE_USER" ]; then CALCOM_DATABASE_USER=calcom-user updateConfigVar CALCOM_DATABASE_USER $CALCOM_DATABASE_USER fi if [ -z "$CALCOM_DATABASE_USER_PASSWORD" ]; then CALCOM_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CALCOM_DATABASE_USER_PASSWORD $CALCOM_DATABASE_USER_PASSWORD fi if [ -z "$RALLLY_ADMIN_USERNAME" ]; then RALLLY_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rallly" updateConfigVar RALLLY_ADMIN_USERNAME $RALLLY_ADMIN_USERNAME fi if [ -z "$RALLLY_ADMIN_EMAIL_ADDRESS" ]; then RALLLY_ADMIN_EMAIL_ADDRESS=$RALLLY_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar RALLLY_ADMIN_EMAIL_ADDRESS $RALLLY_ADMIN_EMAIL_ADDRESS fi if [ -z "$RALLLY_ADMIN_PASSWORD" ]; then RALLLY_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RALLLY_ADMIN_PASSWORD $RALLLY_ADMIN_PASSWORD fi if [ -z "$RALLLY_DATABASE_NAME" ]; then RALLLY_DATABASE_NAME=ralllydb updateConfigVar RALLLY_DATABASE_NAME $RALLLY_DATABASE_NAME fi if [ -z "$RALLLY_DATABASE_USER" ]; then RALLLY_DATABASE_USER=rallly-user updateConfigVar RALLLY_DATABASE_USER $RALLLY_DATABASE_USER fi if [ -z "$RALLLY_DATABASE_USER_PASSWORD" ]; then RALLLY_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RALLLY_DATABASE_USER_PASSWORD $RALLLY_DATABASE_USER_PASSWORD fi if [ -z "$RALLLY_MINIO_KEY" ]; then RALLLY_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar RALLLY_MINIO_KEY $RALLLY_MINIO_KEY fi if [ -z "$RALLLY_MINIO_SECRET" ]; then RALLLY_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar RALLLY_MINIO_SECRET $RALLLY_MINIO_SECRET fi if [ -z "$EASYAPPOINTMENTS_ADMIN_USERNAME" ]; then EASYAPPOINTMENTS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_easyappointments" updateConfigVar EASYAPPOINTMENTS_ADMIN_USERNAME $EASYAPPOINTMENTS_ADMIN_USERNAME fi if [ -z "$EASYAPPOINTMENTS_ADMIN_PASSWORD" ]; then EASYAPPOINTMENTS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar EASYAPPOINTMENTS_ADMIN_PASSWORD $EASYAPPOINTMENTS_ADMIN_PASSWORD fi if [ -z "$EASYAPPOINTMENTS_ADMIN_EMAIL_ADDRESS" ]; then EASYAPPOINTMENTS_ADMIN_EMAIL_ADDRESS=$EASYAPPOINTMENTS_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar EASYAPPOINTMENTS_ADMIN_EMAIL_ADDRESS $EASYAPPOINTMENTS_ADMIN_EMAIL_ADDRESS fi if [ -z "$EASYAPPOINTMENTS_DATABASE_NAME" ]; then EASYAPPOINTMENTS_DATABASE_NAME=easyappointmentsdb updateConfigVar EASYAPPOINTMENTS_DATABASE_NAME $EASYAPPOINTMENTS_DATABASE_NAME fi if [ -z "$EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD" ]; then EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD $EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD fi if [ -z "$EASYAPPOINTMENTS_DATABASE_USER" ]; then EASYAPPOINTMENTS_DATABASE_USER=easyappointments-user updateConfigVar EASYAPPOINTMENTS_DATABASE_USER $EASYAPPOINTMENTS_DATABASE_USER fi if [ -z "$EASYAPPOINTMENTS_DATABASE_USER_PASSWORD" ]; then EASYAPPOINTMENTS_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar EASYAPPOINTMENTS_DATABASE_USER_PASSWORD $EASYAPPOINTMENTS_DATABASE_USER_PASSWORD fi if [ -z "$OPENPROJECT_ADMIN_USERNAME" ]; then OPENPROJECT_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_openproject" updateConfigVar OPENPROJECT_ADMIN_USERNAME $OPENPROJECT_ADMIN_USERNAME fi if [ -z "$OPENPROJECT_ADMIN_EMAIL_ADDRESS" ]; then OPENPROJECT_ADMIN_EMAIL_ADDRESS=$ADMIN_USERNAME_BASE"_openproject"@$HOMESERVER_DOMAIN updateConfigVar OPENPROJECT_ADMIN_EMAIL_ADDRESS $OPENPROJECT_ADMIN_EMAIL_ADDRESS fi if [ -z "$OPENPROJECT_ADMIN_PASSWORD" ]; then OPENPROJECT_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENPROJECT_ADMIN_PASSWORD $OPENPROJECT_ADMIN_PASSWORD fi if [ -z "$OPENPROJECT_DATABASE_NAME" ]; then OPENPROJECT_DATABASE_NAME=openprojectdb updateConfigVar OPENPROJECT_DATABASE_NAME $OPENPROJECT_DATABASE_NAME fi if [ -z "$OPENPROJECT_DATABASE_USER" ]; then OPENPROJECT_DATABASE_USER=openproject-user updateConfigVar OPENPROJECT_DATABASE_USER $OPENPROJECT_DATABASE_USER fi if [ -z "$OPENPROJECT_DATABASE_USER_PASSWORD" ]; then OPENPROJECT_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENPROJECT_DATABASE_USER_PASSWORD $OPENPROJECT_DATABASE_USER_PASSWORD fi if [ -z "$OPENPROJECT_MINIO_KEY" ]; then OPENPROJECT_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar OPENPROJECT_MINIO_KEY $OPENPROJECT_MINIO_KEY fi if [ -z "$OPENPROJECT_MINIO_SECRET" ]; then OPENPROJECT_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar OPENPROJECT_MINIO_SECRET $OPENPROJECT_MINIO_SECRET fi if [ -z "$OPENPROJECT_SECRET_KEY_BASE" ]; then OPENPROJECT_SECRET_KEY_BASE=$(openssl rand -hex 64) updateConfigVar OPENPROJECT_SECRET_KEY_BASE $OPENPROJECT_SECRET_KEY_BASE fi if [ -z "$ZAMMAD_ADMIN_USERNAME" ]; then ZAMMAD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_zammad" updateConfigVar ZAMMAD_ADMIN_USERNAME $ZAMMAD_ADMIN_USERNAME fi if [ -z "$ZAMMAD_ADMIN_EMAIL_ADDRESS" ]; then ZAMMAD_ADMIN_EMAIL_ADDRESS=$ZAMMAD_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar ZAMMAD_ADMIN_EMAIL_ADDRESS $ZAMMAD_ADMIN_EMAIL_ADDRESS fi if [ -z "$ZAMMAD_ADMIN_PASSWORD" ]; then ZAMMAD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_ADMIN_PASSWORD $ZAMMAD_ADMIN_PASSWORD fi if [ -z "$ZAMMAD_DATABASE_NAME" ]; then ZAMMAD_DATABASE_NAME=zammaddb updateConfigVar ZAMMAD_DATABASE_NAME $ZAMMAD_DATABASE_NAME fi if [ -z "$ZAMMAD_DATABASE_USER" ]; then ZAMMAD_DATABASE_USER=zammad-user updateConfigVar ZAMMAD_DATABASE_USER $ZAMMAD_DATABASE_USER fi if [ -z "$ZAMMAD_DATABASE_USER_PASSWORD" ]; then ZAMMAD_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_DATABASE_USER_PASSWORD $ZAMMAD_DATABASE_USER_PASSWORD fi if [ -z "$ZAMMAD_MINIO_KEY" ]; then ZAMMAD_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_MINIO_KEY $ZAMMAD_MINIO_KEY fi if [ -z "$ZAMMAD_MINIO_SECRET" ]; then ZAMMAD_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_MINIO_SECRET $ZAMMAD_MINIO_SECRET fi if [ -z "$ZAMMAD_REDIS_PASSWORD" ]; then ZAMMAD_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_REDIS_PASSWORD $ZAMMAD_REDIS_PASSWORD fi if [ -z "$ZAMMAD_ES_USERNAME" ]; then ZAMMAD_ES_USERNAME=zammad-es-user updateConfigVar ZAMMAD_ES_USERNAME $ZAMMAD_ES_USERNAME fi if [ -z "$ZAMMAD_ES_PASSWORD" ]; then ZAMMAD_ES_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_ES_PASSWORD $ZAMMAD_ES_PASSWORD fi if [ -z "$ZAMMAD_DEDICATED_EMAIL_USERNAME" ]; then ZAMMAD_DEDICATED_EMAIL_USERNAME="zammad_hshq" updateConfigVar ZAMMAD_DEDICATED_EMAIL_USERNAME $ZAMMAD_DEDICATED_EMAIL_USERNAME fi if [ -z "$ZAMMAD_DEDICATED_EMAIL_ADDRESS" ]; then ZAMMAD_DEDICATED_EMAIL_ADDRESS=$ZAMMAD_DEDICATED_EMAIL_USERNAME@$HOMESERVER_DOMAIN updateConfigVar ZAMMAD_DEDICATED_EMAIL_ADDRESS $ZAMMAD_DEDICATED_EMAIL_ADDRESS fi if [ -z "$ZAMMAD_DEDICATED_EMAIL_PASSWORD" ]; then ZAMMAD_DEDICATED_EMAIL_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_DEDICATED_EMAIL_PASSWORD $ZAMMAD_DEDICATED_EMAIL_PASSWORD fi if [ -z "$ZULIP_ADMIN_USERNAME" ]; then ZULIP_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_zulip" updateConfigVar ZULIP_ADMIN_USERNAME $ZULIP_ADMIN_USERNAME fi if [ -z "$ZULIP_ADMIN_EMAIL_ADDRESS" ]; then ZULIP_ADMIN_EMAIL_ADDRESS=$ZULIP_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar ZULIP_ADMIN_EMAIL_ADDRESS $ZULIP_ADMIN_EMAIL_ADDRESS fi if [ -z "$ZULIP_ADMIN_PASSWORD" ]; then ZULIP_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZULIP_ADMIN_PASSWORD $ZULIP_ADMIN_PASSWORD fi if [ -z "$ZULIP_DATABASE_NAME" ]; then ZULIP_DATABASE_NAME=zulip updateConfigVar ZULIP_DATABASE_NAME $ZULIP_DATABASE_NAME fi if [ -z "$ZULIP_DATABASE_USER" ]; then ZULIP_DATABASE_USER=zulip updateConfigVar ZULIP_DATABASE_USER $ZULIP_DATABASE_USER fi if [ -z "$ZULIP_DATABASE_USER_PASSWORD" ]; then ZULIP_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZULIP_DATABASE_USER_PASSWORD $ZULIP_DATABASE_USER_PASSWORD fi if [ -z "$ZULIP_REDIS_PASSWORD" ]; then ZULIP_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZULIP_REDIS_PASSWORD $ZULIP_REDIS_PASSWORD fi if [ -z "$ZULIP_RABBITMQ_USERNAME" ]; then ZULIP_RABBITMQ_USERNAME=zulip updateConfigVar ZULIP_RABBITMQ_USERNAME $ZULIP_RABBITMQ_USERNAME fi if [ -z "$ZULIP_RABBITMQ_PASSWORD" ]; then ZULIP_RABBITMQ_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZULIP_RABBITMQ_PASSWORD $ZULIP_RABBITMQ_PASSWORD fi if [ -z "$ZULIP_MEMCACHE_PASSWORD" ]; then ZULIP_MEMCACHE_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZULIP_MEMCACHE_PASSWORD $ZULIP_MEMCACHE_PASSWORD fi if [ -z "$KILLBILL_ADMIN_USERNAME" ]; then KILLBILL_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_killbill" updateConfigVar KILLBILL_ADMIN_USERNAME $KILLBILL_ADMIN_USERNAME fi if [ -z "$KILLBILL_ADMIN_EMAIL_ADDRESS" ]; then KILLBILL_ADMIN_EMAIL_ADDRESS=$KILLBILL_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar KILLBILL_ADMIN_EMAIL_ADDRESS $KILLBILL_ADMIN_EMAIL_ADDRESS fi if [ -z "$KILLBILL_ADMIN_PASSWORD" ]; then KILLBILL_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KILLBILL_ADMIN_PASSWORD $KILLBILL_ADMIN_PASSWORD fi if [ -z "$KILLBILL_DATABASE_USER" ]; then KILLBILL_DATABASE_USER=root updateConfigVar KILLBILL_DATABASE_USER $KILLBILL_DATABASE_USER fi if [ -z "$KILLBILL_DATABASE_USER_PASSWORD" ]; then KILLBILL_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KILLBILL_DATABASE_USER_PASSWORD $KILLBILL_DATABASE_USER_PASSWORD fi if [ -z "$KILLBILL_KB_DATABASE_NAME" ]; then KILLBILL_KB_DATABASE_NAME=killbill updateConfigVar KILLBILL_KB_DATABASE_NAME $KILLBILL_KB_DATABASE_NAME fi if [ -z "$KILLBILL_KAUI_DATABASE_NAME" ]; then KILLBILL_KAUI_DATABASE_NAME=kaui updateConfigVar KILLBILL_KAUI_DATABASE_NAME $KILLBILL_KAUI_DATABASE_NAME fi if [ -z "$KILLBILL_REDIS_PASSWORD" ]; then KILLBILL_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KILLBILL_REDIS_PASSWORD $KILLBILL_REDIS_PASSWORD fi if [ -z "$KILLBILL_API_KEY" ]; then KILLBILL_API_KEY=$(pwgen -c -n 32 1) updateConfigVar KILLBILL_API_KEY $KILLBILL_API_KEY fi if [ -z "$KILLBILL_API_SECRET" ]; then KILLBILL_API_SECRET=$(pwgen -c -n 64 1) updateConfigVar KILLBILL_API_SECRET $KILLBILL_API_SECRET fi if [ -z "$INVOICESHELF_ADMIN_USERNAME" ]; then INVOICESHELF_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_invoiceshelf" updateConfigVar INVOICESHELF_ADMIN_USERNAME $INVOICESHELF_ADMIN_USERNAME fi if [ -z "$INVOICESHELF_ADMIN_EMAIL_ADDRESS" ]; then INVOICESHELF_ADMIN_EMAIL_ADDRESS=$INVOICESHELF_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar INVOICESHELF_ADMIN_EMAIL_ADDRESS $INVOICESHELF_ADMIN_EMAIL_ADDRESS fi if [ -z "$INVOICESHELF_ADMIN_PASSWORD" ]; then INVOICESHELF_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICESHELF_ADMIN_PASSWORD $INVOICESHELF_ADMIN_PASSWORD fi if [ -z "$INVOICESHELF_DATABASE_NAME" ]; then INVOICESHELF_DATABASE_NAME=invoiceshelfdb updateConfigVar INVOICESHELF_DATABASE_NAME $INVOICESHELF_DATABASE_NAME fi if [ -z "$INVOICESHELF_DATABASE_USER" ]; then INVOICESHELF_DATABASE_USER=invoiceshelf-user updateConfigVar INVOICESHELF_DATABASE_USER $INVOICESHELF_DATABASE_USER fi if [ -z "$INVOICESHELF_DATABASE_USER_PASSWORD" ]; then INVOICESHELF_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICESHELF_DATABASE_USER_PASSWORD $INVOICESHELF_DATABASE_USER_PASSWORD fi if [ -z "$INVOICENINJA_ADMIN_USERNAME" ]; then INVOICENINJA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_invoiceninja" updateConfigVar INVOICENINJA_ADMIN_USERNAME $INVOICENINJA_ADMIN_USERNAME fi if [ -z "$INVOICENINJA_ADMIN_EMAIL_ADDRESS" ]; then INVOICENINJA_ADMIN_EMAIL_ADDRESS=$INVOICENINJA_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar INVOICENINJA_ADMIN_EMAIL_ADDRESS $INVOICENINJA_ADMIN_EMAIL_ADDRESS fi if [ -z "$INVOICENINJA_ADMIN_PASSWORD" ]; then INVOICENINJA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_ADMIN_PASSWORD $INVOICENINJA_ADMIN_PASSWORD fi if [ -z "$INVOICENINJA_DATABASE_NAME" ]; then INVOICENINJA_DATABASE_NAME=invoiceninjadb updateConfigVar INVOICENINJA_DATABASE_NAME $INVOICENINJA_DATABASE_NAME fi if [ -z "$INVOICENINJA_DATABASE_ROOT_PASSWORD" ]; then INVOICENINJA_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_DATABASE_ROOT_PASSWORD $INVOICENINJA_DATABASE_ROOT_PASSWORD fi if [ -z "$INVOICENINJA_DATABASE_USER" ]; then INVOICENINJA_DATABASE_USER=invoiceninja-user updateConfigVar INVOICENINJA_DATABASE_USER $INVOICENINJA_DATABASE_USER fi if [ -z "$INVOICENINJA_DATABASE_USER_PASSWORD" ]; then INVOICENINJA_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_DATABASE_USER_PASSWORD $INVOICENINJA_DATABASE_USER_PASSWORD fi if [ -z "$INVOICENINJA_REDIS_PASSWORD" ]; then INVOICENINJA_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_REDIS_PASSWORD $INVOICENINJA_REDIS_PASSWORD fi if [ -z "$INVOICENINJA_MINIO_KEY" ]; then INVOICENINJA_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_MINIO_KEY $INVOICENINJA_MINIO_KEY fi if [ -z "$INVOICENINJA_MINIO_SECRET" ]; then INVOICENINJA_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_MINIO_SECRET $INVOICENINJA_MINIO_SECRET fi if [ -z "$INVOICENINJA_API_SECRET" ]; then INVOICENINJA_API_SECRET=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_API_SECRET $INVOICENINJA_API_SECRET fi if [ -z "$INVOICENINJA_UPDATE_SECRET" ]; then INVOICENINJA_UPDATE_SECRET=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_UPDATE_SECRET $INVOICENINJA_UPDATE_SECRET fi if [ -z "$INVOICENINJA_WEBCRON_SECRET" ]; then INVOICENINJA_WEBCRON_SECRET=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_WEBCRON_SECRET $INVOICENINJA_WEBCRON_SECRET fi if [ -z "$DOLIBARR_ADMIN_USERNAME" ]; then DOLIBARR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_dolibarr" updateConfigVar DOLIBARR_ADMIN_USERNAME $DOLIBARR_ADMIN_USERNAME fi if [ -z "$DOLIBARR_ADMIN_EMAIL_ADDRESS" ]; then DOLIBARR_ADMIN_EMAIL_ADDRESS=$DOLIBARR_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar DOLIBARR_ADMIN_EMAIL_ADDRESS $DOLIBARR_ADMIN_EMAIL_ADDRESS fi if [ -z "$DOLIBARR_ADMIN_PASSWORD" ]; then DOLIBARR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_ADMIN_PASSWORD $DOLIBARR_ADMIN_PASSWORD fi if [ -z "$DOLIBARR_DATABASE_NAME" ]; then DOLIBARR_DATABASE_NAME=dolibarrdb updateConfigVar DOLIBARR_DATABASE_NAME $DOLIBARR_DATABASE_NAME fi if [ -z "$DOLIBARR_DATABASE_ROOT_PASSWORD" ]; then DOLIBARR_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_DATABASE_ROOT_PASSWORD $DOLIBARR_DATABASE_ROOT_PASSWORD fi if [ -z "$DOLIBARR_DATABASE_USER" ]; then DOLIBARR_DATABASE_USER=dolibarr-user updateConfigVar DOLIBARR_DATABASE_USER $DOLIBARR_DATABASE_USER fi if [ -z "$DOLIBARR_DATABASE_USER_PASSWORD" ]; then DOLIBARR_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_DATABASE_USER_PASSWORD $DOLIBARR_DATABASE_USER_PASSWORD fi if [ -z "$DOLIBARR_REDIS_PASSWORD" ]; then DOLIBARR_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_REDIS_PASSWORD $DOLIBARR_REDIS_PASSWORD fi if [ -z "$DOLIBARR_CRON_SECRET" ]; then DOLIBARR_CRON_SECRET=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_CRON_SECRET $DOLIBARR_CRON_SECRET fi if [ -z "$DOLIBARR_INSTANCE_UNIQUE_ID" ]; then DOLIBARR_INSTANCE_UNIQUE_ID=$(openssl rand -hex 32) updateConfigVar DOLIBARR_INSTANCE_UNIQUE_ID $DOLIBARR_INSTANCE_UNIQUE_ID fi if [ -z "$DOLIBARR_API_KEY" ]; then DOLIBARR_API_KEY=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_API_KEY $DOLIBARR_API_KEY fi if [ -z "$DOLIBARR_MCP_API_KEY" ]; then DOLIBARR_MCP_API_KEY=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_MCP_API_KEY $DOLIBARR_MCP_API_KEY fi if [ -z "$DOLIBARR_MCP_REDIS_PASSWORD" ]; then DOLIBARR_MCP_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_MCP_REDIS_PASSWORD $DOLIBARR_MCP_REDIS_PASSWORD fi if [ -z "$DOLIBARR_REDIS_PASSWORD" ]; then DOLIBARR_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_REDIS_PASSWORD $DOLIBARR_REDIS_PASSWORD fi if [ -z "$N8N_ADMIN_USERNAME" ]; then N8N_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_n8n" updateConfigVar N8N_ADMIN_USERNAME $N8N_ADMIN_USERNAME fi if [ -z "$N8N_ADMIN_EMAIL_ADDRESS" ]; then N8N_ADMIN_EMAIL_ADDRESS=$N8N_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar N8N_ADMIN_EMAIL_ADDRESS $N8N_ADMIN_EMAIL_ADDRESS fi if [ -z "$N8N_ADMIN_PASSWORD" ]; then N8N_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar N8N_ADMIN_PASSWORD $N8N_ADMIN_PASSWORD fi if [ -z "$N8N_DATABASE_NAME" ]; then N8N_DATABASE_NAME=n8ndb updateConfigVar N8N_DATABASE_NAME $N8N_DATABASE_NAME fi if [ -z "$N8N_DATABASE_USER" ]; then N8N_DATABASE_USER=n8n-user updateConfigVar N8N_DATABASE_USER $N8N_DATABASE_USER fi if [ -z "$N8N_DATABASE_USER_PASSWORD" ]; then N8N_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar N8N_DATABASE_USER_PASSWORD $N8N_DATABASE_USER_PASSWORD fi if [ -z "$N8N_REDIS_PASSWORD" ]; then N8N_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar N8N_REDIS_PASSWORD $N8N_REDIS_PASSWORD fi if [ -z "$N8N_ENCRYPTION_KEY" ]; then N8N_ENCRYPTION_KEY=$(pwgen -c -n 32 1) updateConfigVar N8N_ENCRYPTION_KEY $N8N_ENCRYPTION_KEY fi if [ -z "$AUTOMATISCH_ADMIN_USERNAME" ]; then AUTOMATISCH_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_automatisch" updateConfigVar AUTOMATISCH_ADMIN_USERNAME $AUTOMATISCH_ADMIN_USERNAME fi if [ -z "$AUTOMATISCH_ADMIN_EMAIL_ADDRESS" ]; then AUTOMATISCH_ADMIN_EMAIL_ADDRESS=$AUTOMATISCH_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AUTOMATISCH_ADMIN_EMAIL_ADDRESS $AUTOMATISCH_ADMIN_EMAIL_ADDRESS fi if [ -z "$AUTOMATISCH_ADMIN_PASSWORD" ]; then AUTOMATISCH_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOMATISCH_ADMIN_PASSWORD $AUTOMATISCH_ADMIN_PASSWORD fi if [ -z "$AUTOMATISCH_DATABASE_NAME" ]; then AUTOMATISCH_DATABASE_NAME=automatischdb updateConfigVar AUTOMATISCH_DATABASE_NAME $AUTOMATISCH_DATABASE_NAME fi if [ -z "$AUTOMATISCH_DATABASE_USER" ]; then AUTOMATISCH_DATABASE_USER=automatisch-user updateConfigVar AUTOMATISCH_DATABASE_USER $AUTOMATISCH_DATABASE_USER fi if [ -z "$AUTOMATISCH_DATABASE_USER_PASSWORD" ]; then AUTOMATISCH_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOMATISCH_DATABASE_USER_PASSWORD $AUTOMATISCH_DATABASE_USER_PASSWORD fi if [ -z "$AUTOMATISCH_REDIS_PASSWORD" ]; then AUTOMATISCH_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOMATISCH_REDIS_PASSWORD $AUTOMATISCH_REDIS_PASSWORD fi if [ -z "$AUTOMATISCH_ENCRYPTION_KEY" ]; then AUTOMATISCH_ENCRYPTION_KEY=$(pwgen -c -n 32 1) updateConfigVar AUTOMATISCH_ENCRYPTION_KEY $AUTOMATISCH_ENCRYPTION_KEY fi if [ -z "$AUTOMATISCH_WEBHOOK_SECRET_KEY" ]; then AUTOMATISCH_WEBHOOK_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar AUTOMATISCH_WEBHOOK_SECRET_KEY $AUTOMATISCH_WEBHOOK_SECRET_KEY fi if [ -z "$AUTOMATISCH_APP_SECRET_KEY" ]; then AUTOMATISCH_APP_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar AUTOMATISCH_APP_SECRET_KEY $AUTOMATISCH_APP_SECRET_KEY fi if [ -z "$ACTIVEPIECES_ADMIN_USERNAME" ]; then ACTIVEPIECES_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_activepieces" updateConfigVar ACTIVEPIECES_ADMIN_USERNAME $ACTIVEPIECES_ADMIN_USERNAME fi if [ -z "$ACTIVEPIECES_ADMIN_EMAIL_ADDRESS" ]; then ACTIVEPIECES_ADMIN_EMAIL_ADDRESS=$ACTIVEPIECES_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar ACTIVEPIECES_ADMIN_EMAIL_ADDRESS $ACTIVEPIECES_ADMIN_EMAIL_ADDRESS fi if [ -z "$ACTIVEPIECES_ADMIN_PASSWORD" ]; then ACTIVEPIECES_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar ACTIVEPIECES_ADMIN_PASSWORD $ACTIVEPIECES_ADMIN_PASSWORD fi if [ -z "$ACTIVEPIECES_DATABASE_NAME" ]; then ACTIVEPIECES_DATABASE_NAME=activepiecesdb updateConfigVar ACTIVEPIECES_DATABASE_NAME $ACTIVEPIECES_DATABASE_NAME fi if [ -z "$ACTIVEPIECES_DATABASE_USER" ]; then ACTIVEPIECES_DATABASE_USER=activepieces-user updateConfigVar ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_USER fi if [ -z "$ACTIVEPIECES_DATABASE_USER_PASSWORD" ]; then ACTIVEPIECES_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ACTIVEPIECES_DATABASE_USER_PASSWORD $ACTIVEPIECES_DATABASE_USER_PASSWORD fi if [ -z "$ACTIVEPIECES_REDIS_PASSWORD" ]; then ACTIVEPIECES_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ACTIVEPIECES_REDIS_PASSWORD $ACTIVEPIECES_REDIS_PASSWORD fi if [ -z "$ACTIVEPIECES_ENCRYPTION_KEY" ]; then ACTIVEPIECES_ENCRYPTION_KEY=$(pwgen -c -n 32 1) updateConfigVar ACTIVEPIECES_ENCRYPTION_KEY $ACTIVEPIECES_ENCRYPTION_KEY fi if [ -z "$ACTIVEPIECES_API_KEY" ]; then ACTIVEPIECES_API_KEY=$(pwgen -c -n 32 1) updateConfigVar ACTIVEPIECES_API_KEY $ACTIVEPIECES_API_KEY fi if [ -z "$ACTIVEPIECES_JWT_SECRET" ]; then ACTIVEPIECES_JWT_SECRET=$(pwgen -c -n 32 1) updateConfigVar ACTIVEPIECES_JWT_SECRET $ACTIVEPIECES_JWT_SECRET fi if [ -z "$BESZEL_ADMIN_USERNAME" ]; then BESZEL_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_beszel" updateConfigVar BESZEL_ADMIN_USERNAME $BESZEL_ADMIN_USERNAME fi if [ -z "$BESZEL_ADMIN_EMAIL_ADDRESS" ]; then BESZEL_ADMIN_EMAIL_ADDRESS=$BESZEL_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar BESZEL_ADMIN_EMAIL_ADDRESS $BESZEL_ADMIN_EMAIL_ADDRESS fi if [ -z "$BESZEL_ADMIN_PASSWORD" ]; then BESZEL_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BESZEL_ADMIN_PASSWORD $BESZEL_ADMIN_PASSWORD fi if [ -z "$TAIGA_ADMIN_USERNAME" ]; then TAIGA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_taiga" updateConfigVar TAIGA_ADMIN_USERNAME $TAIGA_ADMIN_USERNAME fi if [ -z "$TAIGA_ADMIN_EMAIL_ADDRESS" ]; then TAIGA_ADMIN_EMAIL_ADDRESS=$TAIGA_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar TAIGA_ADMIN_EMAIL_ADDRESS $TAIGA_ADMIN_EMAIL_ADDRESS fi if [ -z "$TAIGA_ADMIN_PASSWORD" ]; then TAIGA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TAIGA_ADMIN_PASSWORD $TAIGA_ADMIN_PASSWORD fi if [ -z "$TAIGA_DATABASE_NAME" ]; then TAIGA_DATABASE_NAME=taigadb updateConfigVar TAIGA_DATABASE_NAME $TAIGA_DATABASE_NAME fi if [ -z "$TAIGA_DATABASE_USER" ]; then TAIGA_DATABASE_USER=taiga-user updateConfigVar TAIGA_DATABASE_USER $TAIGA_DATABASE_USER fi if [ -z "$TAIGA_DATABASE_USER_PASSWORD" ]; then TAIGA_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TAIGA_DATABASE_USER_PASSWORD $TAIGA_DATABASE_USER_PASSWORD fi if [ -z "$TAIGA_SECRET_KEY" ]; then TAIGA_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar TAIGA_SECRET_KEY $TAIGA_SECRET_KEY fi if [ -z "$TAIGA_RABBITMQ_USERNAME" ]; then TAIGA_RABBITMQ_USERNAME=taiga updateConfigVar TAIGA_RABBITMQ_USERNAME $TAIGA_RABBITMQ_USERNAME fi if [ -z "$TAIGA_RABBITMQ_PASSWORD" ]; then TAIGA_RABBITMQ_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TAIGA_RABBITMQ_PASSWORD $TAIGA_RABBITMQ_PASSWORD fi if [ -z "$TAIGA_RABBITMQ_ERLANG_COOKIE" ]; then TAIGA_RABBITMQ_ERLANG_COOKIE=$(pwgen -c -n 32 1) updateConfigVar TAIGA_RABBITMQ_ERLANG_COOKIE $TAIGA_RABBITMQ_ERLANG_COOKIE fi if [ -z "$OPENSIGN_ADMIN_USERNAME" ]; then OPENSIGN_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_opensign" updateConfigVar OPENSIGN_ADMIN_USERNAME $OPENSIGN_ADMIN_USERNAME fi if [ -z "$OPENSIGN_ADMIN_EMAIL_ADDRESS" ]; then OPENSIGN_ADMIN_EMAIL_ADDRESS=$OPENSIGN_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar OPENSIGN_ADMIN_EMAIL_ADDRESS $OPENSIGN_ADMIN_EMAIL_ADDRESS fi if [ -z "$OPENSIGN_ADMIN_PASSWORD" ]; then OPENSIGN_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar OPENSIGN_ADMIN_PASSWORD $OPENSIGN_ADMIN_PASSWORD fi if [ -z "$OPENSIGN_DATABASE_NAME" ]; then OPENSIGN_DATABASE_NAME=opensigndb updateConfigVar OPENSIGN_DATABASE_NAME $OPENSIGN_DATABASE_NAME fi if [ -z "$OPENSIGN_DATABASE_USER" ]; then OPENSIGN_DATABASE_USER=opensign-user updateConfigVar OPENSIGN_DATABASE_USER $OPENSIGN_DATABASE_USER fi if [ -z "$OPENSIGN_DATABASE_USER_PASSWORD" ]; then OPENSIGN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENSIGN_DATABASE_USER_PASSWORD $OPENSIGN_DATABASE_USER_PASSWORD fi if [ -z "$OPENSIGN_MASTER_KEY" ]; then OPENSIGN_MASTER_KEY=$(pwgen -c -0 16 1) updateConfigVar OPENSIGN_MASTER_KEY $OPENSIGN_MASTER_KEY fi if [ -z "$OPENSIGN_CERTIFICATE_PASS_PHRASE" ]; then OPENSIGN_CERTIFICATE_PASS_PHRASE=$(pwgen -c -n 32 1) updateConfigVar OPENSIGN_CERTIFICATE_PASS_PHRASE $OPENSIGN_CERTIFICATE_PASS_PHRASE fi if [ -z "$DOCUSEAL_ADMIN_USERNAME" ]; then DOCUSEAL_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_docuseal" updateConfigVar DOCUSEAL_ADMIN_USERNAME $DOCUSEAL_ADMIN_USERNAME fi if [ -z "$DOCUSEAL_ADMIN_EMAIL_ADDRESS" ]; then DOCUSEAL_ADMIN_EMAIL_ADDRESS=$DOCUSEAL_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar DOCUSEAL_ADMIN_EMAIL_ADDRESS $DOCUSEAL_ADMIN_EMAIL_ADDRESS fi if [ -z "$DOCUSEAL_ADMIN_PASSWORD" ]; then DOCUSEAL_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCUSEAL_ADMIN_PASSWORD $DOCUSEAL_ADMIN_PASSWORD fi if [ -z "$DOCUSEAL_DATABASE_NAME" ]; then DOCUSEAL_DATABASE_NAME=docusealdb updateConfigVar DOCUSEAL_DATABASE_NAME $DOCUSEAL_DATABASE_NAME fi if [ -z "$DOCUSEAL_DATABASE_USER" ]; then DOCUSEAL_DATABASE_USER=docuseal-user updateConfigVar DOCUSEAL_DATABASE_USER $DOCUSEAL_DATABASE_USER fi if [ -z "$DOCUSEAL_DATABASE_USER_PASSWORD" ]; then DOCUSEAL_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCUSEAL_DATABASE_USER_PASSWORD $DOCUSEAL_DATABASE_USER_PASSWORD fi if [ -z "$CONTROLR_ADMIN_USERNAME" ]; then CONTROLR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_controlr" updateConfigVar CONTROLR_ADMIN_USERNAME $CONTROLR_ADMIN_USERNAME fi if [ -z "$CONTROLR_ADMIN_EMAIL_ADDRESS" ]; then CONTROLR_ADMIN_EMAIL_ADDRESS=$CONTROLR_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar CONTROLR_ADMIN_EMAIL_ADDRESS $CONTROLR_ADMIN_EMAIL_ADDRESS fi if [ -z "$CONTROLR_ADMIN_PASSWORD" ]; then CONTROLR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CONTROLR_ADMIN_PASSWORD $CONTROLR_ADMIN_PASSWORD fi if [ -z "$CONTROLR_DATABASE_NAME" ]; then CONTROLR_DATABASE_NAME=controlrdb updateConfigVar CONTROLR_DATABASE_NAME $CONTROLR_DATABASE_NAME fi if [ -z "$CONTROLR_DATABASE_USER" ]; then CONTROLR_DATABASE_USER=controlr-user updateConfigVar CONTROLR_DATABASE_USER $CONTROLR_DATABASE_USER fi if [ -z "$CONTROLR_DATABASE_USER_PASSWORD" ]; then CONTROLR_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CONTROLR_DATABASE_USER_PASSWORD $CONTROLR_DATABASE_USER_PASSWORD fi if [ -z "$CONTROLR_ASPIRE_TOKEN" ]; then CONTROLR_ASPIRE_TOKEN=$(pwgen -c -n 32 1) updateConfigVar CONTROLR_ASPIRE_TOKEN $CONTROLR_ASPIRE_TOKEN fi if [ -z "$CONTROLR_ASPIRE_APIKEY" ]; then CONTROLR_ASPIRE_APIKEY=$(pwgen -c -n 32 1) updateConfigVar CONTROLR_ASPIRE_APIKEY $CONTROLR_ASPIRE_APIKEY fi if [ -z "$AKAUNTING_ADMIN_USERNAME" ]; then AKAUNTING_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_akaunting" updateConfigVar AKAUNTING_ADMIN_USERNAME $AKAUNTING_ADMIN_USERNAME fi if [ -z "$AKAUNTING_ADMIN_EMAIL_ADDRESS" ]; then AKAUNTING_ADMIN_EMAIL_ADDRESS=$AKAUNTING_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AKAUNTING_ADMIN_EMAIL_ADDRESS $AKAUNTING_ADMIN_EMAIL_ADDRESS fi if [ -z "$AKAUNTING_ADMIN_PASSWORD" ]; then AKAUNTING_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AKAUNTING_ADMIN_PASSWORD $AKAUNTING_ADMIN_PASSWORD fi if [ -z "$AKAUNTING_DATABASE_NAME" ]; then AKAUNTING_DATABASE_NAME=akauntingdb updateConfigVar AKAUNTING_DATABASE_NAME $AKAUNTING_DATABASE_NAME fi if [ -z "$AKAUNTING_DATABASE_ROOT_PASSWORD" ]; then AKAUNTING_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AKAUNTING_DATABASE_ROOT_PASSWORD $AKAUNTING_DATABASE_ROOT_PASSWORD fi if [ -z "$AKAUNTING_DATABASE_USER" ]; then AKAUNTING_DATABASE_USER=akaunting-user updateConfigVar AKAUNTING_DATABASE_USER $AKAUNTING_DATABASE_USER fi if [ -z "$AKAUNTING_DATABASE_USER_PASSWORD" ]; then AKAUNTING_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AKAUNTING_DATABASE_USER_PASSWORD $AKAUNTING_DATABASE_USER_PASSWORD fi if [ -z "$AXELOR_ADMIN_USERNAME" ]; then AXELOR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_axelor" updateConfigVar AXELOR_ADMIN_USERNAME $AXELOR_ADMIN_USERNAME fi if [ -z "$AXELOR_ADMIN_EMAIL_ADDRESS" ]; then AXELOR_ADMIN_EMAIL_ADDRESS=$AXELOR_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AXELOR_ADMIN_EMAIL_ADDRESS $AXELOR_ADMIN_EMAIL_ADDRESS fi if [ -z "$AXELOR_ADMIN_PASSWORD" ]; then AXELOR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AXELOR_ADMIN_PASSWORD $AXELOR_ADMIN_PASSWORD fi if [ -z "$AXELOR_DATABASE_NAME" ]; then AXELOR_DATABASE_NAME=axelordb updateConfigVar AXELOR_DATABASE_NAME $AXELOR_DATABASE_NAME fi if [ -z "$AXELOR_DATABASE_USER" ]; then AXELOR_DATABASE_USER=axelor-user updateConfigVar AXELOR_DATABASE_USER $AXELOR_DATABASE_USER fi if [ -z "$AXELOR_DATABASE_USER_PASSWORD" ]; then AXELOR_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AXELOR_DATABASE_USER_PASSWORD $AXELOR_DATABASE_USER_PASSWORD fi if [ -z "$AXELOR_NEXTAUTH_SECRET" ]; then AXELOR_NEXTAUTH_SECRET=$(pwgen -c -n 32 1) updateConfigVar AXELOR_NEXTAUTH_SECRET $AXELOR_NEXTAUTH_SECRET fi if [ -z "$AXELOR_ADMIN_USERNAME" ]; then AXELOR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_axelor" updateConfigVar AXELOR_ADMIN_USERNAME $AXELOR_ADMIN_USERNAME fi if [ -z "$AXELOR_GOOVEE_ADMIN_USERNAME" ]; then AXELOR_GOOVEE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_goovee" updateConfigVar AXELOR_GOOVEE_ADMIN_USERNAME $AXELOR_GOOVEE_ADMIN_USERNAME fi if [ -z "$AXELOR_GOOVEE_ADMIN_PASSWORD" ]; then AXELOR_GOOVEE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AXELOR_GOOVEE_ADMIN_PASSWORD $AXELOR_GOOVEE_ADMIN_PASSWORD fi if [ -z "$CONVERTX_ADMIN_USERNAME" ]; then CONVERTX_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_convertx" updateConfigVar CONVERTX_ADMIN_USERNAME $CONVERTX_ADMIN_USERNAME fi if [ -z "$CONVERTX_ADMIN_EMAIL_ADDRESS" ]; then CONVERTX_ADMIN_EMAIL_ADDRESS=$CONVERTX_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar CONVERTX_ADMIN_EMAIL_ADDRESS $CONVERTX_ADMIN_EMAIL_ADDRESS fi if [ -z "$CONVERTX_ADMIN_PASSWORD" ]; then CONVERTX_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CONVERTX_ADMIN_PASSWORD $CONVERTX_ADMIN_PASSWORD fi if [ -z "$CONVERTX_JWT_SECRET" ]; then CONVERTX_JWT_SECRET=$(pwgen -c -n 64 1) updateConfigVar CONVERTX_JWT_SECRET $CONVERTX_JWT_SECRET fi if [ -z "$KOPIA_ADMIN_USERNAME" ]; then KOPIA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_kopia" updateConfigVar KOPIA_ADMIN_USERNAME $KOPIA_ADMIN_USERNAME fi if [ -z "$KOPIA_ADMIN_PASSWORD" ]; then KOPIA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KOPIA_ADMIN_PASSWORD $KOPIA_ADMIN_PASSWORD fi if [ -z "$OLLAMA_API_KEY" ]; then OLLAMA_API_KEY=$(pwgen -c -n 32 1) updateConfigVar OLLAMA_API_KEY $OLLAMA_API_KEY fi if [ -z "$COMFYUI_API_KEY" ]; then COMFYUI_API_KEY=$(pwgen -c -n 32 1) updateConfigVar COMFYUI_API_KEY $COMFYUI_API_KEY fi if [ -z "$LOCALAI_ADMIN_USERNAME" ]; then LOCALAI_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_localai" updateConfigVar LOCALAI_ADMIN_USERNAME $LOCALAI_ADMIN_USERNAME fi if [ -z "$LOCALAI_ADMIN_EMAIL_ADDRESS" ]; then LOCALAI_ADMIN_EMAIL_ADDRESS=$LOCALAI_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar LOCALAI_ADMIN_EMAIL_ADDRESS $LOCALAI_ADMIN_EMAIL_ADDRESS fi if [ -z "$LOCALAI_ADMIN_PASSWORD" ]; then LOCALAI_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LOCALAI_ADMIN_PASSWORD $LOCALAI_ADMIN_PASSWORD fi if [ -z "$LOCALAI_API_KEY" ]; then LOCALAI_API_KEY=$(pwgen -c -n 32 1) updateConfigVar LOCALAI_API_KEY $LOCALAI_API_KEY fi if [ -z "$LOCALAI_SSHBOX_PASSWORD" ]; then LOCALAI_SSHBOX_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LOCALAI_SSHBOX_PASSWORD $LOCALAI_SSHBOX_PASSWORD fi if [ -z "$LANGFLOW_ADMIN_USERNAME" ]; then LANGFLOW_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_langflow" updateConfigVar LANGFLOW_ADMIN_USERNAME $LANGFLOW_ADMIN_USERNAME fi if [ -z "$LANGFLOW_ADMIN_EMAIL_ADDRESS" ]; then LANGFLOW_ADMIN_EMAIL_ADDRESS=$LANGFLOW_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar LANGFLOW_ADMIN_EMAIL_ADDRESS $LANGFLOW_ADMIN_EMAIL_ADDRESS fi if [ -z "$LANGFLOW_ADMIN_PASSWORD" ]; then LANGFLOW_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFLOW_ADMIN_PASSWORD $LANGFLOW_ADMIN_PASSWORD fi if [ -z "$LANGFLOW_DATABASE_NAME" ]; then LANGFLOW_DATABASE_NAME=langflowdb updateConfigVar LANGFLOW_DATABASE_NAME $LANGFLOW_DATABASE_NAME fi if [ -z "$LANGFLOW_DATABASE_USER" ]; then LANGFLOW_DATABASE_USER=langflow-user updateConfigVar LANGFLOW_DATABASE_USER $LANGFLOW_DATABASE_USER fi if [ -z "$LANGFLOW_DATABASE_USER_PASSWORD" ]; then LANGFLOW_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFLOW_DATABASE_USER_PASSWORD $LANGFLOW_DATABASE_USER_PASSWORD fi if [ -z "$ANYTHINGLLM_ADMIN_USERNAME" ]; then ANYTHINGLLM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_anythingllm" updateConfigVar ANYTHINGLLM_ADMIN_USERNAME $ANYTHINGLLM_ADMIN_USERNAME fi if [ -z "$ANYTHINGLLM_ADMIN_EMAIL_ADDRESS" ]; then ANYTHINGLLM_ADMIN_EMAIL_ADDRESS=$ANYTHINGLLM_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar ANYTHINGLLM_ADMIN_EMAIL_ADDRESS $ANYTHINGLLM_ADMIN_EMAIL_ADDRESS fi if [ -z "$ANYTHINGLLM_ADMIN_PASSWORD" ]; then ANYTHINGLLM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ANYTHINGLLM_ADMIN_PASSWORD $ANYTHINGLLM_ADMIN_PASSWORD fi if [ -z "$ANYTHINGLLM_SIG_KEY" ]; then ANYTHINGLLM_SIG_KEY=$(pwgen -c -n 32 1) updateConfigVar ANYTHINGLLM_SIG_KEY $ANYTHINGLLM_SIG_KEY fi if [ -z "$ANYTHINGLLM_SIG_SALT" ]; then ANYTHINGLLM_SIG_SALT=$(pwgen -c -n 32 1) updateConfigVar ANYTHINGLLM_SIG_SALT $ANYTHINGLLM_SIG_SALT fi if [ -z "$ANYTHINGLLM_JWT_SECRET" ]; then ANYTHINGLLM_JWT_SECRET=$(pwgen -c -n 32 1) updateConfigVar ANYTHINGLLM_JWT_SECRET $ANYTHINGLLM_JWT_SECRET fi if [ -z "$FIRECRAWL_DATABASE_NAME" ]; then FIRECRAWL_DATABASE_NAME=firecrawldb updateConfigVar FIRECRAWL_DATABASE_NAME $FIRECRAWL_DATABASE_NAME fi if [ -z "$FIRECRAWL_DATABASE_USER" ]; then FIRECRAWL_DATABASE_USER=firecrawl-user updateConfigVar FIRECRAWL_DATABASE_USER $FIRECRAWL_DATABASE_USER fi if [ -z "$FIRECRAWL_DATABASE_USER_PASSWORD" ]; then FIRECRAWL_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FIRECRAWL_DATABASE_USER_PASSWORD $FIRECRAWL_DATABASE_USER_PASSWORD fi if [ -z "$FIRECRAWL_REDIS_PASSWORD" ]; then FIRECRAWL_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FIRECRAWL_REDIS_PASSWORD $FIRECRAWL_REDIS_PASSWORD fi if [ -z "$FIRECRAWL_BULL_AUTH_KEY" ]; then FIRECRAWL_BULL_AUTH_KEY=$(pwgen -c -n 32 1) updateConfigVar FIRECRAWL_BULL_AUTH_KEY $FIRECRAWL_BULL_AUTH_KEY fi if [ -z "$FIRECRAWL_API_KEY" ]; then FIRECRAWL_API_KEY=$(pwgen -c -n 32 1) updateConfigVar FIRECRAWL_API_KEY $FIRECRAWL_API_KEY fi if [ -z "$FIRECRAWL_RABBITMQ_USERNAME" ]; then FIRECRAWL_RABBITMQ_USERNAME=$ADMIN_USERNAME_BASE"_firecrawl_rmq" updateConfigVar FIRECRAWL_RABBITMQ_USERNAME $FIRECRAWL_RABBITMQ_USERNAME fi if [ -z "$FIRECRAWL_RABBITMQ_PASSWORD" ]; then FIRECRAWL_RABBITMQ_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FIRECRAWL_RABBITMQ_PASSWORD $FIRECRAWL_RABBITMQ_PASSWORD fi if [ -z "$LIBRECHAT_ADMIN_USERNAME" ]; then LIBRECHAT_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_librechat" updateConfigVar LIBRECHAT_ADMIN_USERNAME $LIBRECHAT_ADMIN_USERNAME fi if [ -z "$LIBRECHAT_ADMIN_EMAIL_ADDRESS" ]; then LIBRECHAT_ADMIN_EMAIL_ADDRESS=$LIBRECHAT_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar LIBRECHAT_ADMIN_EMAIL_ADDRESS $LIBRECHAT_ADMIN_EMAIL_ADDRESS fi if [ -z "$LIBRECHAT_ADMIN_PASSWORD" ]; then LIBRECHAT_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIBRECHAT_ADMIN_PASSWORD $LIBRECHAT_ADMIN_PASSWORD fi if [ -z "$LIBRECHAT_DATABASE_NAME" ]; then LIBRECHAT_DATABASE_NAME=librechatdb updateConfigVar LIBRECHAT_DATABASE_NAME $LIBRECHAT_DATABASE_NAME fi if [ -z "$LIBRECHAT_DATABASE_USER" ]; then LIBRECHAT_DATABASE_USER=librechat-user updateConfigVar LIBRECHAT_DATABASE_USER $LIBRECHAT_DATABASE_USER fi if [ -z "$LIBRECHAT_DATABASE_USER_PASSWORD" ]; then LIBRECHAT_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIBRECHAT_DATABASE_USER_PASSWORD $LIBRECHAT_DATABASE_USER_PASSWORD fi if [ -z "$LIBRECHAT_REDIS_PASSWORD" ]; then LIBRECHAT_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIBRECHAT_REDIS_PASSWORD $LIBRECHAT_REDIS_PASSWORD fi if [ -z "$LIBRECHAT_MONGODB_USER" ]; then LIBRECHAT_MONGODB_USER=librechat-user updateConfigVar LIBRECHAT_MONGODB_USER $LIBRECHAT_MONGODB_USER fi if [ -z "$LIBRECHAT_MONGODB_USER_PASSWORD" ]; then LIBRECHAT_MONGODB_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIBRECHAT_MONGODB_USER_PASSWORD $LIBRECHAT_MONGODB_USER_PASSWORD fi if [ -z "$LIBRECHAT_MONGODB_DATABASE" ]; then LIBRECHAT_MONGODB_DATABASE=librechatmongodb updateConfigVar LIBRECHAT_MONGODB_DATABASE $LIBRECHAT_MONGODB_DATABASE fi if [ -z "$LIBRECHAT_CREDS_KEY" ]; then LIBRECHAT_CREDS_KEY=$(openssl rand -hex 32) updateConfigVar LIBRECHAT_CREDS_KEY $LIBRECHAT_CREDS_KEY fi if [ -z "$LIBRECHAT_CREDS_IV" ]; then LIBRECHAT_CREDS_IV=$(openssl rand -hex 16) updateConfigVar LIBRECHAT_CREDS_IV $LIBRECHAT_CREDS_IV fi if [ -z "$LIBRECHAT_JWT_SECRET" ]; then LIBRECHAT_JWT_SECRET=$(openssl rand -hex 32) updateConfigVar LIBRECHAT_JWT_SECRET $LIBRECHAT_JWT_SECRET fi if [ -z "$LIBRECHAT_JWT_REFRESH_SECRET" ]; then LIBRECHAT_JWT_REFRESH_SECRET=$(openssl rand -hex 32) updateConfigVar LIBRECHAT_JWT_REFRESH_SECRET $LIBRECHAT_JWT_REFRESH_SECRET fi if [ -z "$LIBRECHAT_MEILI_MASTER_KEY" ]; then LIBRECHAT_MEILI_MASTER_KEY=$(pwgen -c -n 32 1) updateConfigVar LIBRECHAT_MEILI_MASTER_KEY $LIBRECHAT_MEILI_MASTER_KEY fi if [ -z "$LIBRECHAT_OIDC_CLIENT_SECRET" ]; then LIBRECHAT_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar LIBRECHAT_OIDC_CLIENT_SECRET $LIBRECHAT_OIDC_CLIENT_SECRET fi if [ -z "$OPENWEBUI_ADMIN_USERNAME" ]; then OPENWEBUI_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_openwebui" updateConfigVar OPENWEBUI_ADMIN_USERNAME $OPENWEBUI_ADMIN_USERNAME fi if [ -z "$OPENWEBUI_ADMIN_EMAIL_ADDRESS" ]; then OPENWEBUI_ADMIN_EMAIL_ADDRESS=$OPENWEBUI_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar OPENWEBUI_ADMIN_EMAIL_ADDRESS $OPENWEBUI_ADMIN_EMAIL_ADDRESS fi if [ -z "$OPENWEBUI_ADMIN_PASSWORD" ]; then OPENWEBUI_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENWEBUI_ADMIN_PASSWORD $OPENWEBUI_ADMIN_PASSWORD fi if [ -z "$OPENWEBUI_DATABASE_NAME" ]; then OPENWEBUI_DATABASE_NAME=openwebuidb updateConfigVar OPENWEBUI_DATABASE_NAME $OPENWEBUI_DATABASE_NAME fi if [ -z "$OPENWEBUI_DATABASE_USER" ]; then OPENWEBUI_DATABASE_USER=openwebui-user updateConfigVar OPENWEBUI_DATABASE_USER $OPENWEBUI_DATABASE_USER fi if [ -z "$OPENWEBUI_DATABASE_USER_PASSWORD" ]; then OPENWEBUI_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENWEBUI_DATABASE_USER_PASSWORD $OPENWEBUI_DATABASE_USER_PASSWORD fi if [ -z "$OPENWEBUI_REDIS_PASSWORD" ]; then OPENWEBUI_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENWEBUI_REDIS_PASSWORD $OPENWEBUI_REDIS_PASSWORD fi if [ -z "$OPENWEBUI_OIDC_CLIENT_ID" ]; then OPENWEBUI_OIDC_CLIENT_ID=openwebui updateConfigVar OPENWEBUI_OIDC_CLIENT_ID $OPENWEBUI_OIDC_CLIENT_ID fi if [ -z "$OPENWEBUI_OIDC_CLIENT_SECRET" ]; then OPENWEBUI_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar OPENWEBUI_OIDC_CLIENT_SECRET $OPENWEBUI_OIDC_CLIENT_SECRET fi if [ -z "$OPENWEBUI_SECRET_KEY" ]; then OPENWEBUI_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar OPENWEBUI_SECRET_KEY $OPENWEBUI_SECRET_KEY fi if [ -z "$OPENWEBUI_ADMIN_API_KEY" ]; then OPENWEBUI_ADMIN_API_KEY=sk-$(uuidgen | sed "s/-//g") updateConfigVar OPENWEBUI_ADMIN_API_KEY $OPENWEBUI_ADMIN_API_KEY fi if [ -z "$OPENWEBUI_QDRANT_API_KEY" ]; then OPENWEBUI_QDRANT_API_KEY=$(openssl rand -hex 32) updateConfigVar OPENWEBUI_QDRANT_API_KEY $OPENWEBUI_QDRANT_API_KEY fi if [ -z "$OPENWEBUI_OPENTERMINAL_API_KEY" ]; then OPENWEBUI_OPENTERMINAL_API_KEY=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32) updateConfigVar OPENWEBUI_OPENTERMINAL_API_KEY $OPENWEBUI_OPENTERMINAL_API_KEY fi if [ -z "$OPENWEBUI_MCPO_API_KEY" ]; then OPENWEBUI_MCPO_API_KEY=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9') updateConfigVar OPENWEBUI_MCPO_API_KEY $OPENWEBUI_MCPO_API_KEY fi if [ -z "$OPENWEBUI_PIPELINES_API_KEY" ]; then OPENWEBUI_PIPELINES_API_KEY=$(openssl rand -base64 12 | tr -d '/+=' | head -c 16) updateConfigVar OPENWEBUI_PIPELINES_API_KEY $OPENWEBUI_PIPELINES_API_KEY fi if [ -z "$OPENWEBUI_ADMIN_UUID" ]; then OPENWEBUI_ADMIN_UUID=$(uuidgen) updateConfigVar OPENWEBUI_ADMIN_UUID $OPENWEBUI_ADMIN_UUID fi if [ -z "$OPENWEBUI_PRIMARYUSERS_UUID" ]; then OPENWEBUI_PRIMARYUSERS_UUID=$(uuidgen) updateConfigVar OPENWEBUI_PRIMARYUSERS_UUID $OPENWEBUI_PRIMARYUSERS_UUID fi if [ -z "$KHOJ_ADMIN_USERNAME" ]; then KHOJ_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_khoj" updateConfigVar KHOJ_ADMIN_USERNAME $KHOJ_ADMIN_USERNAME fi if [ -z "$KHOJ_ADMIN_EMAIL_ADDRESS" ]; then KHOJ_ADMIN_EMAIL_ADDRESS=$KHOJ_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar KHOJ_ADMIN_EMAIL_ADDRESS $KHOJ_ADMIN_EMAIL_ADDRESS fi if [ -z "$KHOJ_ADMIN_PASSWORD" ]; then KHOJ_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KHOJ_ADMIN_PASSWORD $KHOJ_ADMIN_PASSWORD fi if [ -z "$KHOJ_DATABASE_NAME" ]; then KHOJ_DATABASE_NAME=khojdb updateConfigVar KHOJ_DATABASE_NAME $KHOJ_DATABASE_NAME fi if [ -z "$KHOJ_DATABASE_USER" ]; then KHOJ_DATABASE_USER=khoj-user updateConfigVar KHOJ_DATABASE_USER $KHOJ_DATABASE_USER fi if [ -z "$KHOJ_DATABASE_USER_PASSWORD" ]; then KHOJ_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KHOJ_DATABASE_USER_PASSWORD $KHOJ_DATABASE_USER_PASSWORD fi if [ -z "$KHOJ_DJANGO_SECRET_KEY" ]; then KHOJ_DJANGO_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar KHOJ_DJANGO_SECRET_KEY $KHOJ_DJANGO_SECRET_KEY fi if [ -z "$LOBECHAT_ADMIN_USERNAME" ]; then LOBECHAT_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_lobechat" updateConfigVar LOBECHAT_ADMIN_USERNAME $LOBECHAT_ADMIN_USERNAME fi if [ -z "$LOBECHAT_ADMIN_EMAIL_ADDRESS" ]; then LOBECHAT_ADMIN_EMAIL_ADDRESS=$LOBECHAT_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar LOBECHAT_ADMIN_EMAIL_ADDRESS $LOBECHAT_ADMIN_EMAIL_ADDRESS fi if [ -z "$LOBECHAT_ADMIN_PASSWORD" ]; then LOBECHAT_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_ADMIN_PASSWORD $LOBECHAT_ADMIN_PASSWORD fi if [ -z "$LOBECHAT_DATABASE_NAME" ]; then LOBECHAT_DATABASE_NAME=lobechatdb updateConfigVar LOBECHAT_DATABASE_NAME $LOBECHAT_DATABASE_NAME fi if [ -z "$LOBECHAT_DATABASE_USER" ]; then LOBECHAT_DATABASE_USER=lobechat-user updateConfigVar LOBECHAT_DATABASE_USER $LOBECHAT_DATABASE_USER fi if [ -z "$LOBECHAT_DATABASE_USER_PASSWORD" ]; then LOBECHAT_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_DATABASE_USER_PASSWORD $LOBECHAT_DATABASE_USER_PASSWORD fi if [ -z "$LOBECHAT_REDIS_PASSWORD" ]; then LOBECHAT_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_REDIS_PASSWORD $LOBECHAT_REDIS_PASSWORD fi if [ -z "$LOBECHAT_NEXTAUTH_SECRET" ]; then LOBECHAT_NEXTAUTH_SECRET=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_NEXTAUTH_SECRET $LOBECHAT_NEXTAUTH_SECRET fi if [ -z "$LOBECHAT_KEYVAULTS_SECRET" ]; then LOBECHAT_KEYVAULTS_SECRET=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_KEYVAULTS_SECRET $LOBECHAT_KEYVAULTS_SECRET fi if [ -z "$LOBECHAT_MINIO_KEY" ]; then LOBECHAT_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_MINIO_KEY $LOBECHAT_MINIO_KEY fi if [ -z "$LOBECHAT_MINIO_SECRET" ]; then LOBECHAT_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_MINIO_SECRET $LOBECHAT_MINIO_SECRET fi if [ -z "$LOBECHAT_OIDC_CLIENT_ID" ]; then LOBECHAT_OIDC_CLIENT_ID=lobechat updateConfigVar LOBECHAT_OIDC_CLIENT_ID $LOBECHAT_OIDC_CLIENT_ID fi if [ -z "$LOBECHAT_OIDC_CLIENT_SECRET" ]; then LOBECHAT_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar LOBECHAT_OIDC_CLIENT_SECRET $LOBECHAT_OIDC_CLIENT_SECRET fi if [ -z "$RAGFLOW_ADMIN_USERNAME" ]; then RAGFLOW_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_ragflow" updateConfigVar RAGFLOW_ADMIN_USERNAME $RAGFLOW_ADMIN_USERNAME fi if [ -z "$RAGFLOW_ADMIN_EMAIL_ADDRESS" ]; then RAGFLOW_ADMIN_EMAIL_ADDRESS=$RAGFLOW_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar RAGFLOW_ADMIN_EMAIL_ADDRESS $RAGFLOW_ADMIN_EMAIL_ADDRESS fi if [ -z "$RAGFLOW_ADMIN_PASSWORD" ]; then RAGFLOW_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_ADMIN_PASSWORD $RAGFLOW_ADMIN_PASSWORD fi if [ -z "$RAGFLOW_DATABASE_NAME" ]; then RAGFLOW_DATABASE_NAME=ragflowdb updateConfigVar RAGFLOW_DATABASE_NAME $RAGFLOW_DATABASE_NAME fi if [ -z "$RAGFLOW_DATABASE_USER" ]; then RAGFLOW_DATABASE_USER=ragflow-user updateConfigVar RAGFLOW_DATABASE_USER $RAGFLOW_DATABASE_USER fi if [ -z "$RAGFLOW_DATABASE_USER_PASSWORD" ]; then RAGFLOW_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_DATABASE_USER_PASSWORD $RAGFLOW_DATABASE_USER_PASSWORD fi if [ -z "$RAGFLOW_DATABASE_ROOT_PASSWORD" ]; then RAGFLOW_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_DATABASE_ROOT_PASSWORD $RAGFLOW_DATABASE_ROOT_PASSWORD fi if [ -z "$RAGFLOW_REDIS_PASSWORD" ]; then RAGFLOW_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_REDIS_PASSWORD $RAGFLOW_REDIS_PASSWORD fi if [ -z "$RAGFLOW_MINIO_KEY" ]; then RAGFLOW_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_MINIO_KEY $RAGFLOW_MINIO_KEY fi if [ -z "$RAGFLOW_MINIO_SECRET" ]; then RAGFLOW_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_MINIO_SECRET $RAGFLOW_MINIO_SECRET fi if [ -z "$RAGFLOW_SECRET_KEY" ]; then RAGFLOW_SECRET_KEY=$(openssl rand -hex 32) updateConfigVar RAGFLOW_SECRET_KEY $RAGFLOW_SECRET_KEY fi if [ -z "$RAGFLOW_OIDC_CLIENT_ID" ]; then RAGFLOW_OIDC_CLIENT_ID=ragflow updateConfigVar RAGFLOW_OIDC_CLIENT_ID $RAGFLOW_OIDC_CLIENT_ID fi if [ -z "$RAGFLOW_OIDC_CLIENT_SECRET" ]; then RAGFLOW_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar RAGFLOW_OIDC_CLIENT_SECRET $RAGFLOW_OIDC_CLIENT_SECRET fi if [ -z "$RAGFLOW_MCPSERVER_API_KEY" ]; then RAGFLOW_MCPSERVER_API_KEY=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_MCPSERVER_API_KEY $RAGFLOW_MCPSERVER_API_KEY fi if [ -z "$RAGFLOW_SANDBOX_EXECUTOR_MANAGER_API_TOKEN" ]; then RAGFLOW_SANDBOX_EXECUTOR_MANAGER_API_TOKEN=$(openssl rand -hex 24) updateConfigVar RAGFLOW_SANDBOX_EXECUTOR_MANAGER_API_TOKEN $RAGFLOW_SANDBOX_EXECUTOR_MANAGER_API_TOKEN fi if [ -z "$TABBYML_ADMIN_USERNAME" ]; then TABBYML_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_tabbyml" updateConfigVar TABBYML_ADMIN_USERNAME $TABBYML_ADMIN_USERNAME fi if [ -z "$TABBYML_ADMIN_EMAIL_ADDRESS" ]; then TABBYML_ADMIN_EMAIL_ADDRESS=$TABBYML_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar TABBYML_ADMIN_EMAIL_ADDRESS $TABBYML_ADMIN_EMAIL_ADDRESS fi if [ -z "$TABBYML_ADMIN_PASSWORD" ]; then TABBYML_ADMIN_PASSWORD=$(getPasswordWithSymbol 16) updateConfigVar TABBYML_ADMIN_PASSWORD $TABBYML_ADMIN_PASSWORD fi if [ -z "$TABBYML_JWT_SECRET" ]; then TABBYML_JWT_SECRET=$(uuidgen) updateConfigVar TABBYML_JWT_SECRET $TABBYML_JWT_SECRET fi if [ -z "$DEEPWIKI_OPEN_AUTH_CODE" ]; then DEEPWIKI_OPEN_AUTH_CODE=$(pwgen -c -n 32 1) updateConfigVar DEEPWIKI_OPEN_AUTH_CODE $DEEPWIKI_OPEN_AUTH_CODE fi if [ -z "$DOCLING_REDIS_PASSWORD" ]; then DOCLING_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCLING_REDIS_PASSWORD $DOCLING_REDIS_PASSWORD fi if [ -z "$DOCLING_API_KEY" ]; then DOCLING_API_KEY=$(pwgen -c -n 32 1) updateConfigVar DOCLING_API_KEY $DOCLING_API_KEY fi if [ -z "$DIFY_ADMIN_USERNAME" ]; then DIFY_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_dify" updateConfigVar DIFY_ADMIN_USERNAME $DIFY_ADMIN_USERNAME fi if [ -z "$DIFY_ADMIN_EMAIL_ADDRESS" ]; then DIFY_ADMIN_EMAIL_ADDRESS=$DIFY_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar DIFY_ADMIN_EMAIL_ADDRESS $DIFY_ADMIN_EMAIL_ADDRESS fi if [ -z "$DIFY_ADMIN_PASSWORD" ]; then DIFY_ADMIN_PASSWORD=$(pwgen -c -n 24 1) updateConfigVar DIFY_ADMIN_PASSWORD $DIFY_ADMIN_PASSWORD fi if [ -z "$DIFY_DATABASE_NAME" ]; then DIFY_DATABASE_NAME=difydb updateConfigVar DIFY_DATABASE_NAME $DIFY_DATABASE_NAME fi if [ -z "$DIFY_DATABASE_USER" ]; then DIFY_DATABASE_USER=dify-user updateConfigVar DIFY_DATABASE_USER $DIFY_DATABASE_USER fi if [ -z "$DIFY_DATABASE_USER_PASSWORD" ]; then DIFY_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DIFY_DATABASE_USER_PASSWORD $DIFY_DATABASE_USER_PASSWORD fi if [ -z "$DIFY_REDIS_PASSWORD" ]; then DIFY_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DIFY_REDIS_PASSWORD $DIFY_REDIS_PASSWORD fi if [ -z "$DIFY_SECRET_KEY" ]; then DIFY_SECRET_KEY=$(openssl rand -base64 42) updateConfigVar DIFY_SECRET_KEY $DIFY_SECRET_KEY fi if [ -z "$DIFY_MINIO_KEY" ]; then DIFY_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar DIFY_MINIO_KEY $DIFY_MINIO_KEY fi if [ -z "$DIFY_MINIO_SECRET" ]; then DIFY_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar DIFY_MINIO_SECRET $DIFY_MINIO_SECRET fi if [ -z "$DIFY_QDRANT_API_KEY" ]; then DIFY_QDRANT_API_KEY=$(pwgen -c -n 32 1) updateConfigVar DIFY_QDRANT_API_KEY $DIFY_QDRANT_API_KEY fi if [ -z "$DIFY_CODE_EXECUTION_API_KEY" ]; then DIFY_CODE_EXECUTION_API_KEY=$(pwgen -c -n 32 1) updateConfigVar DIFY_CODE_EXECUTION_API_KEY $DIFY_CODE_EXECUTION_API_KEY fi if [ -z "$DIFY_SANDBOX_API_KEY" ]; then DIFY_SANDBOX_API_KEY=$(pwgen -c -n 32 1) updateConfigVar DIFY_SANDBOX_API_KEY $DIFY_SANDBOX_API_KEY fi if [ -z "$DIFY_PLUGIN_DAEMON_KEY" ]; then DIFY_PLUGIN_DAEMON_KEY=$(dd if=/dev/urandom bs=42 count=1 2>/dev/null | base64 -w 0) updateConfigVar DIFY_PLUGIN_DAEMON_KEY $DIFY_PLUGIN_DAEMON_KEY fi if [ -z "$DIFY_PLUGIN_DIFY_INNER_API_KEY" ]; then DIFY_PLUGIN_DIFY_INNER_API_KEY=$(dd if=/dev/urandom bs=42 count=1 2>/dev/null | base64 -w 0) updateConfigVar DIFY_PLUGIN_DIFY_INNER_API_KEY $DIFY_PLUGIN_DIFY_INNER_API_KEY fi if [ -z "$ACTIVEPIECES_DATABASE_READONLYUSER" ]; then ACTIVEPIECES_DATABASE_READONLYUSER="activepieces-readonly" updateConfigVar ACTIVEPIECES_DATABASE_READONLYUSER $ACTIVEPIECES_DATABASE_READONLYUSER fi if [ -z "$ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD" ]; then ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD $ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$ADMINER_DATABASE_READONLYUSER" ]; then ADMINER_DATABASE_READONLYUSER="adminer-readonly" updateConfigVar ADMINER_DATABASE_READONLYUSER $ADMINER_DATABASE_READONLYUSER fi if [ -z "$ADMINER_DATABASE_READONLYUSER_PASSWORD" ]; then ADMINER_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ADMINER_DATABASE_READONLYUSER_PASSWORD $ADMINER_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$AUTOMATISCH_DATABASE_READONLYUSER" ]; then AUTOMATISCH_DATABASE_READONLYUSER="automatisch-readonly" updateConfigVar AUTOMATISCH_DATABASE_READONLYUSER $AUTOMATISCH_DATABASE_READONLYUSER fi if [ -z "$AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD" ]; then AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD $AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$BUDIBASE_DATABASE_READONLYUSER" ]; then BUDIBASE_DATABASE_READONLYUSER="budibase-readonly" updateConfigVar BUDIBASE_DATABASE_READONLYUSER $BUDIBASE_DATABASE_READONLYUSER fi if [ -z "$BUDIBASE_DATABASE_READONLYUSER_PASSWORD" ]; then BUDIBASE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BUDIBASE_DATABASE_READONLYUSER_PASSWORD $BUDIBASE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$CALCOM_DATABASE_READONLYUSER" ]; then CALCOM_DATABASE_READONLYUSER="calcom-readonly" updateConfigVar CALCOM_DATABASE_READONLYUSER $CALCOM_DATABASE_READONLYUSER fi if [ -z "$CALCOM_DATABASE_READONLYUSER_PASSWORD" ]; then CALCOM_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CALCOM_DATABASE_READONLYUSER_PASSWORD $CALCOM_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$DISCOURSE_DATABASE_READONLYUSER" ]; then DISCOURSE_DATABASE_READONLYUSER="discourse-readonly" updateConfigVar DISCOURSE_DATABASE_READONLYUSER $DISCOURSE_DATABASE_READONLYUSER fi if [ -z "$DISCOURSE_DATABASE_READONLYUSER_PASSWORD" ]; then DISCOURSE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DISCOURSE_DATABASE_READONLYUSER_PASSWORD $DISCOURSE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$DOLIBARR_DATABASE_READONLYUSER" ]; then DOLIBARR_DATABASE_READONLYUSER="dolibarr-readonly" updateConfigVar DOLIBARR_DATABASE_READONLYUSER $DOLIBARR_DATABASE_READONLYUSER fi if [ -z "$DOLIBARR_DATABASE_READONLYUSER_PASSWORD" ]; then DOLIBARR_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOLIBARR_DATABASE_READONLYUSER_PASSWORD $DOLIBARR_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$EASYAPPOINTMENTS_DATABASE_READONLYUSER" ]; then EASYAPPOINTMENTS_DATABASE_READONLYUSER="easyappointments-readonly" updateConfigVar EASYAPPOINTMENTS_DATABASE_READONLYUSER $EASYAPPOINTMENTS_DATABASE_READONLYUSER fi if [ -z "$EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD" ]; then EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD $EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$ESPOCRM_DATABASE_READONLYUSER" ]; then ESPOCRM_DATABASE_READONLYUSER="espocrm-readonly" updateConfigVar ESPOCRM_DATABASE_READONLYUSER $ESPOCRM_DATABASE_READONLYUSER fi if [ -z "$ESPOCRM_DATABASE_READONLYUSER_PASSWORD" ]; then ESPOCRM_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ESPOCRM_DATABASE_READONLYUSER_PASSWORD $ESPOCRM_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$FIREFLY_DATABASE_READONLYUSER" ]; then FIREFLY_DATABASE_READONLYUSER="firefly-readonly" updateConfigVar FIREFLY_DATABASE_READONLYUSER $FIREFLY_DATABASE_READONLYUSER fi if [ -z "$FIREFLY_DATABASE_READONLYUSER_PASSWORD" ]; then FIREFLY_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FIREFLY_DATABASE_READONLYUSER_PASSWORD $FIREFLY_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$FRAPPE_HR_DATABASE_READONLYUSER" ]; then FRAPPE_HR_DATABASE_READONLYUSER="frappe-hr-readonly" updateConfigVar FRAPPE_HR_DATABASE_READONLYUSER $FRAPPE_HR_DATABASE_READONLYUSER fi if [ -z "$FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD" ]; then FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD $FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$FRESHRSS_DATABASE_READONLYUSER" ]; then FRESHRSS_DATABASE_READONLYUSER="freshrss-readonly" updateConfigVar FRESHRSS_DATABASE_READONLYUSER $FRESHRSS_DATABASE_READONLYUSER fi if [ -z "$FRESHRSS_DATABASE_READONLYUSER_PASSWORD" ]; then FRESHRSS_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FRESHRSS_DATABASE_READONLYUSER_PASSWORD $FRESHRSS_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$GHOST_DATABASE_READONLYUSER" ]; then GHOST_DATABASE_READONLYUSER="ghost-readonly" updateConfigVar GHOST_DATABASE_READONLYUSER $GHOST_DATABASE_READONLYUSER fi if [ -z "$GHOST_DATABASE_READONLYUSER_PASSWORD" ]; then GHOST_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GHOST_DATABASE_READONLYUSER_PASSWORD $GHOST_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$GITEA_DATABASE_READONLYUSER" ]; then GITEA_DATABASE_READONLYUSER="gitea-readonly" updateConfigVar GITEA_DATABASE_READONLYUSER $GITEA_DATABASE_READONLYUSER fi if [ -z "$GITEA_DATABASE_READONLYUSER_PASSWORD" ]; then GITEA_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GITEA_DATABASE_READONLYUSER_PASSWORD $GITEA_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$GITLAB_DATABASE_READONLYUSER" ]; then GITLAB_DATABASE_READONLYUSER="gitlab-readonly" updateConfigVar GITLAB_DATABASE_READONLYUSER $GITLAB_DATABASE_READONLYUSER fi if [ -z "$GITLAB_DATABASE_READONLYUSER_PASSWORD" ]; then GITLAB_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GITLAB_DATABASE_READONLYUSER_PASSWORD $GITLAB_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$GUACAMOLE_DATABASE_READONLYUSER" ]; then GUACAMOLE_DATABASE_READONLYUSER="guacamole-readonly" updateConfigVar GUACAMOLE_DATABASE_READONLYUSER $GUACAMOLE_DATABASE_READONLYUSER fi if [ -z "$GUACAMOLE_DATABASE_READONLYUSER_PASSWORD" ]; then GUACAMOLE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GUACAMOLE_DATABASE_READONLYUSER_PASSWORD $GUACAMOLE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$HOMEASSISTANT_DATABASE_READONLYUSER" ]; then HOMEASSISTANT_DATABASE_READONLYUSER="homeassistant-readonly" updateConfigVar HOMEASSISTANT_DATABASE_READONLYUSER $HOMEASSISTANT_DATABASE_READONLYUSER fi if [ -z "$HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD" ]; then HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD $HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$HUGINN_DATABASE_READONLYUSER" ]; then HUGINN_DATABASE_READONLYUSER="huginn-readonly" updateConfigVar HUGINN_DATABASE_READONLYUSER $HUGINN_DATABASE_READONLYUSER fi if [ -z "$HUGINN_DATABASE_READONLYUSER_PASSWORD" ]; then HUGINN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HUGINN_DATABASE_READONLYUSER_PASSWORD $HUGINN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$IMMICH_DATABASE_READONLYUSER" ]; then IMMICH_DATABASE_READONLYUSER="immich-readonly" updateConfigVar IMMICH_DATABASE_READONLYUSER $IMMICH_DATABASE_READONLYUSER fi if [ -z "$IMMICH_DATABASE_READONLYUSER_PASSWORD" ]; then IMMICH_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar IMMICH_DATABASE_READONLYUSER_PASSWORD $IMMICH_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$INVIDIOUS_DATABASE_READONLYUSER" ]; then INVIDIOUS_DATABASE_READONLYUSER="invidious-readonly" updateConfigVar INVIDIOUS_DATABASE_READONLYUSER $INVIDIOUS_DATABASE_READONLYUSER fi if [ -z "$INVIDIOUS_DATABASE_READONLYUSER_PASSWORD" ]; then INVIDIOUS_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVIDIOUS_DATABASE_READONLYUSER_PASSWORD $INVIDIOUS_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$INVOICENINJA_DATABASE_READONLYUSER" ]; then INVOICENINJA_DATABASE_READONLYUSER="invoiceninja-readonly" updateConfigVar INVOICENINJA_DATABASE_READONLYUSER $INVOICENINJA_DATABASE_READONLYUSER fi if [ -z "$INVOICENINJA_DATABASE_READONLYUSER_PASSWORD" ]; then INVOICENINJA_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICENINJA_DATABASE_READONLYUSER_PASSWORD $INVOICENINJA_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$INVOICESHELF_DATABASE_READONLYUSER" ]; then INVOICESHELF_DATABASE_READONLYUSER="invoiceshelf-readonly" updateConfigVar INVOICESHELF_DATABASE_READONLYUSER $INVOICESHELF_DATABASE_READONLYUSER fi if [ -z "$INVOICESHELF_DATABASE_READONLYUSER_PASSWORD" ]; then INVOICESHELF_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar INVOICESHELF_DATABASE_READONLYUSER_PASSWORD $INVOICESHELF_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$KANBOARD_DATABASE_READONLYUSER" ]; then KANBOARD_DATABASE_READONLYUSER="kanboard-readonly" updateConfigVar KANBOARD_DATABASE_READONLYUSER $KANBOARD_DATABASE_READONLYUSER fi if [ -z "$KANBOARD_DATABASE_READONLYUSER_PASSWORD" ]; then KANBOARD_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KANBOARD_DATABASE_READONLYUSER_PASSWORD $KANBOARD_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$KEILA_DATABASE_READONLYUSER" ]; then KEILA_DATABASE_READONLYUSER="keila-readonly" updateConfigVar KEILA_DATABASE_READONLYUSER $KEILA_DATABASE_READONLYUSER fi if [ -z "$KEILA_DATABASE_READONLYUSER_PASSWORD" ]; then KEILA_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KEILA_DATABASE_READONLYUSER_PASSWORD $KEILA_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$KILLBILL_KB_DATABASE_READONLYUSER" ]; then KILLBILL_KB_DATABASE_READONLYUSER="killbill-readonly" updateConfigVar KILLBILL_KB_DATABASE_READONLYUSER $KILLBILL_KB_DATABASE_READONLYUSER fi if [ -z "$KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD" ]; then KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD $KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$KILLBILL_KAUI_DATABASE_READONLYUSER" ]; then KILLBILL_KAUI_DATABASE_READONLYUSER="killbill-readonly" updateConfigVar KILLBILL_KAUI_DATABASE_READONLYUSER $KILLBILL_KAUI_DATABASE_READONLYUSER fi if [ -z "$KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD" ]; then KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD $KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LINKWARDEN_DATABASE_READONLYUSER" ]; then LINKWARDEN_DATABASE_READONLYUSER="linkwarden-readonly" updateConfigVar LINKWARDEN_DATABASE_READONLYUSER $LINKWARDEN_DATABASE_READONLYUSER fi if [ -z "$LINKWARDEN_DATABASE_READONLYUSER_PASSWORD" ]; then LINKWARDEN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LINKWARDEN_DATABASE_READONLYUSER_PASSWORD $LINKWARDEN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MASTODON_DATABASE_READONLYUSER" ]; then MASTODON_DATABASE_READONLYUSER="mastodon-readonly" updateConfigVar MASTODON_DATABASE_READONLYUSER $MASTODON_DATABASE_READONLYUSER fi if [ -z "$MASTODON_DATABASE_READONLYUSER_PASSWORD" ]; then MASTODON_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MASTODON_DATABASE_READONLYUSER_PASSWORD $MASTODON_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MATOMO_DATABASE_READONLYUSER" ]; then MATOMO_DATABASE_READONLYUSER="matomo-readonly" updateConfigVar MATOMO_DATABASE_READONLYUSER $MATOMO_DATABASE_READONLYUSER fi if [ -z "$MATOMO_DATABASE_READONLYUSER_PASSWORD" ]; then MATOMO_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MATOMO_DATABASE_READONLYUSER_PASSWORD $MATOMO_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MATRIX_DATABASE_READONLYUSER" ]; then MATRIX_DATABASE_READONLYUSER="matrix-readonly" updateConfigVar MATRIX_DATABASE_READONLYUSER $MATRIX_DATABASE_READONLYUSER fi if [ -z "$MATRIX_DATABASE_READONLYUSER_PASSWORD" ]; then MATRIX_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MATRIX_DATABASE_READONLYUSER_PASSWORD $MATRIX_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MEALIE_DATABASE_READONLYUSER" ]; then MEALIE_DATABASE_READONLYUSER="mealie-readonly" updateConfigVar MEALIE_DATABASE_READONLYUSER $MEALIE_DATABASE_READONLYUSER fi if [ -z "$MEALIE_DATABASE_READONLYUSER_PASSWORD" ]; then MEALIE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MEALIE_DATABASE_READONLYUSER_PASSWORD $MEALIE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MESHCENTRAL_DATABASE_READONLYUSER" ]; then MESHCENTRAL_DATABASE_READONLYUSER="meshcentral-readonly" updateConfigVar MESHCENTRAL_DATABASE_READONLYUSER $MESHCENTRAL_DATABASE_READONLYUSER fi if [ -z "$MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD" ]; then MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD $MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$METABASE_DATABASE_READONLYUSER" ]; then METABASE_DATABASE_READONLYUSER="metabase-readonly" updateConfigVar METABASE_DATABASE_READONLYUSER $METABASE_DATABASE_READONLYUSER fi if [ -z "$METABASE_DATABASE_READONLYUSER_PASSWORD" ]; then METABASE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar METABASE_DATABASE_READONLYUSER_PASSWORD $METABASE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MINTHCM_DATABASE_READONLYUSER" ]; then MINTHCM_DATABASE_READONLYUSER="minthcm-readonly" updateConfigVar MINTHCM_DATABASE_READONLYUSER $MINTHCM_DATABASE_READONLYUSER fi if [ -z "$MINTHCM_DATABASE_READONLYUSER_PASSWORD" ]; then MINTHCM_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINTHCM_DATABASE_READONLYUSER_PASSWORD $MINTHCM_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$N8N_DATABASE_READONLYUSER" ]; then N8N_DATABASE_READONLYUSER="n8n-readonly" updateConfigVar N8N_DATABASE_READONLYUSER $N8N_DATABASE_READONLYUSER fi if [ -z "$N8N_DATABASE_READONLYUSER_PASSWORD" ]; then N8N_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar N8N_DATABASE_READONLYUSER_PASSWORD $N8N_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$NEXTCLOUD_DATABASE_READONLYUSER" ]; then NEXTCLOUD_DATABASE_READONLYUSER="nextcloud-readonly" updateConfigVar NEXTCLOUD_DATABASE_READONLYUSER $NEXTCLOUD_DATABASE_READONLYUSER fi if [ -z "$NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD" ]; then NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD $NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$ODOO_DATABASE_READONLYUSER" ]; then ODOO_DATABASE_READONLYUSER="odoo-readonly" updateConfigVar ODOO_DATABASE_READONLYUSER $ODOO_DATABASE_READONLYUSER fi if [ -z "$ODOO_DATABASE_READONLYUSER_PASSWORD" ]; then ODOO_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ODOO_DATABASE_READONLYUSER_PASSWORD $ODOO_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$OMBI_DATABASE_READONLYUSER" ]; then OMBI_DATABASE_READONLYUSER="ombi-readonly" updateConfigVar OMBI_DATABASE_READONLYUSER $OMBI_DATABASE_READONLYUSER fi if [ -z "$OMBI_DATABASE_READONLYUSER_PASSWORD" ]; then OMBI_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OMBI_DATABASE_READONLYUSER_PASSWORD $OMBI_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$OPENPROJECT_DATABASE_READONLYUSER" ]; then OPENPROJECT_DATABASE_READONLYUSER="openproject-readonly" updateConfigVar OPENPROJECT_DATABASE_READONLYUSER $OPENPROJECT_DATABASE_READONLYUSER fi if [ -z "$OPENPROJECT_DATABASE_READONLYUSER_PASSWORD" ]; then OPENPROJECT_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENPROJECT_DATABASE_READONLYUSER_PASSWORD $OPENPROJECT_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$PAPERLESS_DATABASE_READONLYUSER" ]; then PAPERLESS_DATABASE_READONLYUSER="paperless-readonly" updateConfigVar PAPERLESS_DATABASE_READONLYUSER $PAPERLESS_DATABASE_READONLYUSER fi if [ -z "$PAPERLESS_DATABASE_READONLYUSER_PASSWORD" ]; then PAPERLESS_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PAPERLESS_DATABASE_READONLYUSER_PASSWORD $PAPERLESS_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$PASTEFY_DATABASE_READONLYUSER" ]; then PASTEFY_DATABASE_READONLYUSER="pastefy-readonly" updateConfigVar PASTEFY_DATABASE_READONLYUSER $PASTEFY_DATABASE_READONLYUSER fi if [ -z "$PASTEFY_DATABASE_READONLYUSER_PASSWORD" ]; then PASTEFY_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PASTEFY_DATABASE_READONLYUSER_PASSWORD $PASTEFY_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$PEERTUBE_DATABASE_READONLYUSER" ]; then PEERTUBE_DATABASE_READONLYUSER="peertube-readonly" updateConfigVar PEERTUBE_DATABASE_READONLYUSER $PEERTUBE_DATABASE_READONLYUSER fi if [ -z "$PEERTUBE_DATABASE_READONLYUSER_PASSWORD" ]; then PEERTUBE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PEERTUBE_DATABASE_READONLYUSER_PASSWORD $PEERTUBE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$PENPOT_DATABASE_READONLYUSER" ]; then PENPOT_DATABASE_READONLYUSER="penpot-readonly" updateConfigVar PENPOT_DATABASE_READONLYUSER $PENPOT_DATABASE_READONLYUSER fi if [ -z "$PENPOT_DATABASE_READONLYUSER_PASSWORD" ]; then PENPOT_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PENPOT_DATABASE_READONLYUSER_PASSWORD $PENPOT_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$PHOTOPRISM_DATABASE_READONLYUSER" ]; then PHOTOPRISM_DATABASE_READONLYUSER="photoprism-readonly" updateConfigVar PHOTOPRISM_DATABASE_READONLYUSER $PHOTOPRISM_DATABASE_READONLYUSER fi if [ -z "$PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD" ]; then PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD $PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$PIPED_DATABASE_READONLYUSER" ]; then PIPED_DATABASE_READONLYUSER="piped-readonly" updateConfigVar PIPED_DATABASE_READONLYUSER $PIPED_DATABASE_READONLYUSER fi if [ -z "$PIPED_DATABASE_READONLYUSER_PASSWORD" ]; then PIPED_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PIPED_DATABASE_READONLYUSER_PASSWORD $PIPED_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$PIXELFED_DATABASE_READONLYUSER" ]; then PIXELFED_DATABASE_READONLYUSER="pixelfed-readonly" updateConfigVar PIXELFED_DATABASE_READONLYUSER $PIXELFED_DATABASE_READONLYUSER fi if [ -z "$PIXELFED_DATABASE_READONLYUSER_PASSWORD" ]; then PIXELFED_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PIXELFED_DATABASE_READONLYUSER_PASSWORD $PIXELFED_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$RALLLY_DATABASE_READONLYUSER" ]; then RALLLY_DATABASE_READONLYUSER="rallly-readonly" updateConfigVar RALLLY_DATABASE_READONLYUSER $RALLLY_DATABASE_READONLYUSER fi if [ -z "$RALLLY_DATABASE_READONLYUSER_PASSWORD" ]; then RALLLY_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RALLLY_DATABASE_READONLYUSER_PASSWORD $RALLLY_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$SHLINK_DATABASE_READONLYUSER" ]; then SHLINK_DATABASE_READONLYUSER="shlink-readonly" updateConfigVar SHLINK_DATABASE_READONLYUSER $SHLINK_DATABASE_READONLYUSER fi if [ -z "$SHLINK_DATABASE_READONLYUSER_PASSWORD" ]; then SHLINK_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SHLINK_DATABASE_READONLYUSER_PASSWORD $SHLINK_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$STANDARDNOTES_DATABASE_READONLYUSER" ]; then STANDARDNOTES_DATABASE_READONLYUSER="standardnotes-readonly" updateConfigVar STANDARDNOTES_DATABASE_READONLYUSER $STANDARDNOTES_DATABASE_READONLYUSER fi if [ -z "$STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD" ]; then STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD $STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER" ]; then SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER="speedtest-tracker-local-readonly" updateConfigVar SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER fi if [ -z "$SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD" ]; then SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER" ]; then SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER="speedtest-tracker-vpn-readonly" updateConfigVar SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER fi if [ -z "$SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD" ]; then SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$TWENTY_DATABASE_READONLYUSER" ]; then TWENTY_DATABASE_READONLYUSER="twenty-readonly" updateConfigVar TWENTY_DATABASE_READONLYUSER $TWENTY_DATABASE_READONLYUSER fi if [ -z "$TWENTY_DATABASE_READONLYUSER_PASSWORD" ]; then TWENTY_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TWENTY_DATABASE_READONLYUSER_PASSWORD $TWENTY_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$VAULTWARDEN_DATABASE_READONLYUSER" ]; then VAULTWARDEN_DATABASE_READONLYUSER="vaultwarden-readonly" updateConfigVar VAULTWARDEN_DATABASE_READONLYUSER $VAULTWARDEN_DATABASE_READONLYUSER fi if [ -z "$VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD" ]; then VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD $VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$WALLABAG_DATABASE_READONLYUSER" ]; then WALLABAG_DATABASE_READONLYUSER="wallabag-readonly" updateConfigVar WALLABAG_DATABASE_READONLYUSER $WALLABAG_DATABASE_READONLYUSER fi if [ -z "$WALLABAG_DATABASE_READONLYUSER_PASSWORD" ]; then WALLABAG_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WALLABAG_DATABASE_READONLYUSER_PASSWORD $WALLABAG_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$WIKIJS_DATABASE_READONLYUSER" ]; then WIKIJS_DATABASE_READONLYUSER="wikijs-readonly" updateConfigVar WIKIJS_DATABASE_READONLYUSER $WIKIJS_DATABASE_READONLYUSER fi if [ -z "$WIKIJS_DATABASE_READONLYUSER_PASSWORD" ]; then WIKIJS_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WIKIJS_DATABASE_READONLYUSER_PASSWORD $WIKIJS_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$WORDPRESS_DATABASE_READONLYUSER" ]; then WORDPRESS_DATABASE_READONLYUSER="wordpress-readonly" updateConfigVar WORDPRESS_DATABASE_READONLYUSER $WORDPRESS_DATABASE_READONLYUSER fi if [ -z "$WORDPRESS_DATABASE_READONLYUSER_PASSWORD" ]; then WORDPRESS_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WORDPRESS_DATABASE_READONLYUSER_PASSWORD $WORDPRESS_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$YAMTRACK_DATABASE_READONLYUSER" ]; then YAMTRACK_DATABASE_READONLYUSER="yamtrack-readonly" updateConfigVar YAMTRACK_DATABASE_READONLYUSER $YAMTRACK_DATABASE_READONLYUSER fi if [ -z "$YAMTRACK_DATABASE_READONLYUSER_PASSWORD" ]; then YAMTRACK_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar YAMTRACK_DATABASE_READONLYUSER_PASSWORD $YAMTRACK_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$ZAMMAD_DATABASE_READONLYUSER" ]; then ZAMMAD_DATABASE_READONLYUSER="zammad-readonly" updateConfigVar ZAMMAD_DATABASE_READONLYUSER $ZAMMAD_DATABASE_READONLYUSER fi if [ -z "$ZAMMAD_DATABASE_READONLYUSER_PASSWORD" ]; then ZAMMAD_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZAMMAD_DATABASE_READONLYUSER_PASSWORD $ZAMMAD_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$ZULIP_DATABASE_READONLYUSER" ]; then ZULIP_DATABASE_READONLYUSER="zulip-readonly" updateConfigVar ZULIP_DATABASE_READONLYUSER $ZULIP_DATABASE_READONLYUSER fi if [ -z "$ZULIP_DATABASE_READONLYUSER_PASSWORD" ]; then ZULIP_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ZULIP_DATABASE_READONLYUSER_PASSWORD $ZULIP_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$TAIGA_DATABASE_READONLYUSER" ]; then TAIGA_DATABASE_READONLYUSER="taiga-readonly" updateConfigVar TAIGA_DATABASE_READONLYUSER $TAIGA_DATABASE_READONLYUSER fi if [ -z "$TAIGA_DATABASE_READONLYUSER_PASSWORD" ]; then TAIGA_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TAIGA_DATABASE_READONLYUSER_PASSWORD $TAIGA_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$DOCUSEAL_DATABASE_READONLYUSER" ]; then DOCUSEAL_DATABASE_READONLYUSER="docuseal-readonly" updateConfigVar DOCUSEAL_DATABASE_READONLYUSER $DOCUSEAL_DATABASE_READONLYUSER fi if [ -z "$DOCUSEAL_DATABASE_READONLYUSER_PASSWORD" ]; then DOCUSEAL_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCUSEAL_DATABASE_READONLYUSER_PASSWORD $DOCUSEAL_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$CONTROLR_DATABASE_READONLYUSER" ]; then CONTROLR_DATABASE_READONLYUSER="controlr-readonly" updateConfigVar CONTROLR_DATABASE_READONLYUSER $CONTROLR_DATABASE_READONLYUSER fi if [ -z "$CONTROLR_DATABASE_READONLYUSER_PASSWORD" ]; then CONTROLR_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CONTROLR_DATABASE_READONLYUSER_PASSWORD $CONTROLR_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$AKAUNTING_DATABASE_READONLYUSER" ]; then AKAUNTING_DATABASE_READONLYUSER="akaunting-readonly" updateConfigVar AKAUNTING_DATABASE_READONLYUSER $AKAUNTING_DATABASE_READONLYUSER fi if [ -z "$AKAUNTING_DATABASE_READONLYUSER_PASSWORD" ]; then AKAUNTING_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AKAUNTING_DATABASE_READONLYUSER_PASSWORD $AKAUNTING_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$AXELOR_DATABASE_READONLYUSER" ]; then AXELOR_DATABASE_READONLYUSER="axelor-readonly" updateConfigVar AXELOR_DATABASE_READONLYUSER $AXELOR_DATABASE_READONLYUSER fi if [ -z "$AXELOR_DATABASE_READONLYUSER_PASSWORD" ]; then AXELOR_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AXELOR_DATABASE_READONLYUSER_PASSWORD $AXELOR_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LANGFLOW_DATABASE_READONLYUSER" ]; then LANGFLOW_DATABASE_READONLYUSER="langflow-readonly" updateConfigVar LANGFLOW_DATABASE_READONLYUSER $LANGFLOW_DATABASE_READONLYUSER fi if [ -z "$LANGFLOW_DATABASE_READONLYUSER_PASSWORD" ]; then LANGFLOW_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFLOW_DATABASE_READONLYUSER_PASSWORD $LANGFLOW_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LANGFLOW_API_KEY" ]; then LANGFLOW_API_KEY=sk_$(openssl rand -hex 32) updateConfigVar LANGFLOW_API_KEY $LANGFLOW_API_KEY fi if [ -z "$LANGFLOW_SECRET_KEY" ]; then LANGFLOW_SECRET_KEY=$(openssl rand -base64 32) updateConfigVar LANGFLOW_SECRET_KEY $LANGFLOW_SECRET_KEY fi if [ -z "$LANGFLOW_LANGFUSE_PUBLIC_KEY" ]; then LANGFLOW_LANGFUSE_PUBLIC_KEY=pk-lf-$(uuidgen) updateConfigVar LANGFLOW_LANGFUSE_PUBLIC_KEY $LANGFLOW_LANGFUSE_PUBLIC_KEY fi if [ -z "$LANGFLOW_LANGFUSE_SECRET_KEY" ]; then LANGFLOW_LANGFUSE_SECRET_KEY=sk-lf-$(uuidgen) updateConfigVar LANGFLOW_LANGFUSE_SECRET_KEY $LANGFLOW_LANGFUSE_SECRET_KEY fi if [ -z "$LIBRECHAT_DATABASE_READONLYUSER" ]; then LIBRECHAT_DATABASE_READONLYUSER="librechat-readonly" updateConfigVar LIBRECHAT_DATABASE_READONLYUSER $LIBRECHAT_DATABASE_READONLYUSER fi if [ -z "$LIBRECHAT_DATABASE_READONLYUSER_PASSWORD" ]; then LIBRECHAT_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIBRECHAT_DATABASE_READONLYUSER_PASSWORD $LIBRECHAT_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$OPENWEBUI_DATABASE_READONLYUSER" ]; then OPENWEBUI_DATABASE_READONLYUSER="openwebui-readonly" updateConfigVar OPENWEBUI_DATABASE_READONLYUSER $OPENWEBUI_DATABASE_READONLYUSER fi if [ -z "$OPENWEBUI_DATABASE_READONLYUSER_PASSWORD" ]; then OPENWEBUI_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENWEBUI_DATABASE_READONLYUSER_PASSWORD $OPENWEBUI_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$KHOJ_DATABASE_READONLYUSER" ]; then KHOJ_DATABASE_READONLYUSER="khoj-readonly" updateConfigVar KHOJ_DATABASE_READONLYUSER $KHOJ_DATABASE_READONLYUSER fi if [ -z "$KHOJ_DATABASE_READONLYUSER_PASSWORD" ]; then KHOJ_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar KHOJ_DATABASE_READONLYUSER_PASSWORD $KHOJ_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LOBECHAT_DATABASE_READONLYUSER" ]; then LOBECHAT_DATABASE_READONLYUSER="lobechat-readonly" updateConfigVar LOBECHAT_DATABASE_READONLYUSER $LOBECHAT_DATABASE_READONLYUSER fi if [ -z "$LOBECHAT_DATABASE_READONLYUSER_PASSWORD" ]; then LOBECHAT_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LOBECHAT_DATABASE_READONLYUSER_PASSWORD $LOBECHAT_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$RAGFLOW_DATABASE_READONLYUSER" ]; then RAGFLOW_DATABASE_READONLYUSER="ragflow-readonly" updateConfigVar RAGFLOW_DATABASE_READONLYUSER $RAGFLOW_DATABASE_READONLYUSER fi if [ -z "$RAGFLOW_DATABASE_READONLYUSER_PASSWORD" ]; then RAGFLOW_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RAGFLOW_DATABASE_READONLYUSER_PASSWORD $RAGFLOW_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$DIFY_DATABASE_READONLYUSER" ]; then DIFY_DATABASE_READONLYUSER="dify-readonly" updateConfigVar DIFY_DATABASE_READONLYUSER $DIFY_DATABASE_READONLYUSER fi if [ -z "$DIFY_DATABASE_READONLYUSER_PASSWORD" ]; then DIFY_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DIFY_DATABASE_READONLYUSER_PASSWORD $DIFY_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$WEKAN_DATABASE_READONLYUSER" ]; then WEKAN_DATABASE_READONLYUSER="wekan-readonly" updateConfigVar WEKAN_DATABASE_READONLYUSER $WEKAN_DATABASE_READONLYUSER fi if [ -z "$WEKAN_DATABASE_READONLYUSER_PASSWORD" ]; then WEKAN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WEKAN_DATABASE_READONLYUSER_PASSWORD $WEKAN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$REVOLT_DATABASE_READONLYUSER" ]; then REVOLT_DATABASE_READONLYUSER="revolt-readonly" updateConfigVar REVOLT_DATABASE_READONLYUSER $REVOLT_DATABASE_READONLYUSER fi if [ -z "$REVOLT_DATABASE_READONLYUSER_PASSWORD" ]; then REVOLT_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar REVOLT_DATABASE_READONLYUSER_PASSWORD $REVOLT_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$OPENSIGN_DATABASE_READONLYUSER" ]; then OPENSIGN_DATABASE_READONLYUSER="opensign-readonly" updateConfigVar OPENSIGN_DATABASE_READONLYUSER $OPENSIGN_DATABASE_READONLYUSER fi if [ -z "$OPENSIGN_DATABASE_READONLYUSER_PASSWORD" ]; then OPENSIGN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENSIGN_DATABASE_READONLYUSER_PASSWORD $OPENSIGN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LIBRECHAT_MONGODB_READONLYUSER" ]; then LIBRECHAT_MONGODB_READONLYUSER="librechat-readonly" updateConfigVar LIBRECHAT_MONGODB_READONLYUSER $LIBRECHAT_MONGODB_READONLYUSER fi if [ -z "$LIBRECHAT_MONGODB_READONLYUSER_PASSWORD" ]; then LIBRECHAT_MONGODB_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIBRECHAT_MONGODB_READONLYUSER_PASSWORD $LIBRECHAT_MONGODB_READONLYUSER_PASSWORD fi if [ -z "$MINDSDB_ADMIN_USERNAME" ]; then MINDSDB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_mindsdb" updateConfigVar MINDSDB_ADMIN_USERNAME $MINDSDB_ADMIN_USERNAME fi if [ -z "$MINDSDB_ADMIN_EMAIL_ADDRESS" ]; then MINDSDB_ADMIN_EMAIL_ADDRESS=$MINDSDB_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MINDSDB_ADMIN_EMAIL_ADDRESS $MINDSDB_ADMIN_EMAIL_ADDRESS fi if [ -z "$MINDSDB_ADMIN_PASSWORD" ]; then MINDSDB_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINDSDB_ADMIN_PASSWORD $MINDSDB_ADMIN_PASSWORD fi if [ -z "$MINDSDB_DATABASE_NAME" ]; then MINDSDB_DATABASE_NAME=mindsdbdb updateConfigVar MINDSDB_DATABASE_NAME $MINDSDB_DATABASE_NAME fi if [ -z "$MINDSDB_DATABASE_USER" ]; then MINDSDB_DATABASE_USER=mindsdb-user updateConfigVar MINDSDB_DATABASE_USER $MINDSDB_DATABASE_USER fi if [ -z "$MINDSDB_DATABASE_USER_PASSWORD" ]; then MINDSDB_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINDSDB_DATABASE_USER_PASSWORD $MINDSDB_DATABASE_USER_PASSWORD fi if [ -z "$MINDSDB_DATABASE_READONLYUSER" ]; then MINDSDB_DATABASE_READONLYUSER=mindsdb-readonly updateConfigVar MINDSDB_DATABASE_READONLYUSER $MINDSDB_DATABASE_READONLYUSER fi if [ -z "$MINDSDB_DATABASE_READONLYUSER_PASSWORD" ]; then MINDSDB_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINDSDB_DATABASE_READONLYUSER_PASSWORD $MINDSDB_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MINDSDB_REDIS_PASSWORD" ]; then MINDSDB_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MINDSDB_REDIS_PASSWORD $MINDSDB_REDIS_PASSWORD fi if [ -z "$WATERCRAWL_ADMIN_USERNAME" ]; then WATERCRAWL_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_watercrawl" updateConfigVar WATERCRAWL_ADMIN_USERNAME $WATERCRAWL_ADMIN_USERNAME fi if [ -z "$WATERCRAWL_ADMIN_EMAIL_ADDRESS" ]; then WATERCRAWL_ADMIN_EMAIL_ADDRESS=$WATERCRAWL_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar WATERCRAWL_ADMIN_EMAIL_ADDRESS $WATERCRAWL_ADMIN_EMAIL_ADDRESS fi if [ -z "$WATERCRAWL_ADMIN_PASSWORD" ]; then WATERCRAWL_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar WATERCRAWL_ADMIN_PASSWORD $WATERCRAWL_ADMIN_PASSWORD fi if [ -z "$WATERCRAWL_DATABASE_NAME" ]; then WATERCRAWL_DATABASE_NAME=watercrawldb updateConfigVar WATERCRAWL_DATABASE_NAME $WATERCRAWL_DATABASE_NAME fi if [ -z "$WATERCRAWL_DATABASE_USER" ]; then WATERCRAWL_DATABASE_USER=watercrawl-user updateConfigVar WATERCRAWL_DATABASE_USER $WATERCRAWL_DATABASE_USER fi if [ -z "$WATERCRAWL_DATABASE_USER_PASSWORD" ]; then WATERCRAWL_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WATERCRAWL_DATABASE_USER_PASSWORD $WATERCRAWL_DATABASE_USER_PASSWORD fi if [ -z "$WATERCRAWL_DATABASE_READONLYUSER" ]; then WATERCRAWL_DATABASE_READONLYUSER=watercrawl-readonly updateConfigVar WATERCRAWL_DATABASE_READONLYUSER $WATERCRAWL_DATABASE_READONLYUSER fi if [ -z "$WATERCRAWL_DATABASE_READONLYUSER_PASSWORD" ]; then WATERCRAWL_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WATERCRAWL_DATABASE_READONLYUSER_PASSWORD $WATERCRAWL_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$WATERCRAWL_REDIS_PASSWORD" ]; then WATERCRAWL_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WATERCRAWL_REDIS_PASSWORD $WATERCRAWL_REDIS_PASSWORD fi if [ -z "$WATERCRAWL_SECRET_KEY" ]; then WATERCRAWL_SECRET_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") updateConfigVar WATERCRAWL_SECRET_KEY $WATERCRAWL_SECRET_KEY fi if [ -z "$WATERCRAWL_API_ENCRYPTION_KEY" ]; then WATERCRAWL_API_ENCRYPTION_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") updateConfigVar WATERCRAWL_API_ENCRYPTION_KEY $WATERCRAWL_API_ENCRYPTION_KEY fi if [ -z "$WATERCRAWL_PLAYWRIGHT_API_KEY" ]; then WATERCRAWL_PLAYWRIGHT_API_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") updateConfigVar WATERCRAWL_PLAYWRIGHT_API_KEY $WATERCRAWL_PLAYWRIGHT_API_KEY fi if [ -z "$WATERCRAWL_MINIO_KEY" ]; then WATERCRAWL_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar WATERCRAWL_MINIO_KEY $WATERCRAWL_MINIO_KEY fi if [ -z "$WATERCRAWL_MINIO_SECRET" ]; then WATERCRAWL_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar WATERCRAWL_MINIO_SECRET $WATERCRAWL_MINIO_SECRET fi if [ -z "$FLOWISE_ADMIN_USERNAME" ]; then FLOWISE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_flowise" updateConfigVar FLOWISE_ADMIN_USERNAME $FLOWISE_ADMIN_USERNAME fi if [ -z "$FLOWISE_ADMIN_EMAIL_ADDRESS" ]; then FLOWISE_ADMIN_EMAIL_ADDRESS=$FLOWISE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar FLOWISE_ADMIN_EMAIL_ADDRESS $FLOWISE_ADMIN_EMAIL_ADDRESS fi if [ -z "$FLOWISE_ADMIN_PASSWORD" ]; then FLOWISE_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar FLOWISE_ADMIN_PASSWORD $FLOWISE_ADMIN_PASSWORD fi if [ -z "$FLOWISE_DATABASE_NAME" ]; then FLOWISE_DATABASE_NAME=flowisedb updateConfigVar FLOWISE_DATABASE_NAME $FLOWISE_DATABASE_NAME fi if [ -z "$FLOWISE_DATABASE_USER" ]; then FLOWISE_DATABASE_USER=flowise-user updateConfigVar FLOWISE_DATABASE_USER $FLOWISE_DATABASE_USER fi if [ -z "$FLOWISE_DATABASE_USER_PASSWORD" ]; then FLOWISE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FLOWISE_DATABASE_USER_PASSWORD $FLOWISE_DATABASE_USER_PASSWORD fi if [ -z "$FLOWISE_DATABASE_READONLYUSER" ]; then FLOWISE_DATABASE_READONLYUSER=flowise-readonly updateConfigVar FLOWISE_DATABASE_READONLYUSER $FLOWISE_DATABASE_READONLYUSER fi if [ -z "$FLOWISE_DATABASE_READONLYUSER_PASSWORD" ]; then FLOWISE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FLOWISE_DATABASE_READONLYUSER_PASSWORD $FLOWISE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$FLOWISE_REDIS_PASSWORD" ]; then FLOWISE_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FLOWISE_REDIS_PASSWORD $FLOWISE_REDIS_PASSWORD fi if [ -z "$FLOWISE_JWT_SECRET" ]; then FLOWISE_JWT_SECRET=$(openssl rand -hex 32) updateConfigVar FLOWISE_JWT_SECRET $FLOWISE_JWT_SECRET fi if [ -z "$FLOWISE_JWT_REFRESH_SECRET" ]; then FLOWISE_JWT_REFRESH_SECRET=$(openssl rand -hex 32) updateConfigVar FLOWISE_JWT_REFRESH_SECRET $FLOWISE_JWT_REFRESH_SECRET fi if [ -z "$FLOWISE_EXPRESS_SESSION_SECRET" ]; then FLOWISE_EXPRESS_SESSION_SECRET=$(openssl rand -hex 32) updateConfigVar FLOWISE_EXPRESS_SESSION_SECRET $FLOWISE_EXPRESS_SESSION_SECRET fi if [ -z "$FLOWISE_TOKEN_HASH_SECRET" ]; then FLOWISE_TOKEN_HASH_SECRET=$(openssl rand -hex 32) updateConfigVar FLOWISE_TOKEN_HASH_SECRET $FLOWISE_TOKEN_HASH_SECRET fi if [ -z "$FLOWISE_MINIO_KEY" ]; then FLOWISE_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar FLOWISE_MINIO_KEY $FLOWISE_MINIO_KEY fi if [ -z "$FLOWISE_MINIO_SECRET" ]; then FLOWISE_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar FLOWISE_MINIO_SECRET $FLOWISE_MINIO_SECRET fi if [ -z "$SURFSENSE_ADMIN_USERNAME" ]; then SURFSENSE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_surfsense" updateConfigVar SURFSENSE_ADMIN_USERNAME $SURFSENSE_ADMIN_USERNAME fi if [ -z "$SURFSENSE_ADMIN_EMAIL_ADDRESS" ]; then SURFSENSE_ADMIN_EMAIL_ADDRESS=$SURFSENSE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar SURFSENSE_ADMIN_EMAIL_ADDRESS $SURFSENSE_ADMIN_EMAIL_ADDRESS fi if [ -z "$SURFSENSE_ADMIN_PASSWORD" ]; then SURFSENSE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SURFSENSE_ADMIN_PASSWORD $SURFSENSE_ADMIN_PASSWORD fi if [ -z "$SURFSENSE_SECRET_KEY" ]; then SURFSENSE_SECRET_KEY=$(pwgen -c -n 32 1) updateConfigVar SURFSENSE_SECRET_KEY $SURFSENSE_SECRET_KEY fi if [ -z "$NOCODB_ADMIN_USERNAME" ]; then NOCODB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_nocodb" updateConfigVar NOCODB_ADMIN_USERNAME $NOCODB_ADMIN_USERNAME fi if [ -z "$NOCODB_ADMIN_EMAIL_ADDRESS" ]; then NOCODB_ADMIN_EMAIL_ADDRESS=$NOCODB_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar NOCODB_ADMIN_EMAIL_ADDRESS $NOCODB_ADMIN_EMAIL_ADDRESS fi if [ -z "$NOCODB_ADMIN_PASSWORD" ]; then NOCODB_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NOCODB_ADMIN_PASSWORD $NOCODB_ADMIN_PASSWORD fi if [ -z "$NOCODB_DATABASE_NAME" ]; then NOCODB_DATABASE_NAME=nocodbdb updateConfigVar NOCODB_DATABASE_NAME $NOCODB_DATABASE_NAME fi if [ -z "$NOCODB_DATABASE_USER" ]; then NOCODB_DATABASE_USER=nocodb-user updateConfigVar NOCODB_DATABASE_USER $NOCODB_DATABASE_USER fi if [ -z "$NOCODB_DATABASE_USER_PASSWORD" ]; then NOCODB_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NOCODB_DATABASE_USER_PASSWORD $NOCODB_DATABASE_USER_PASSWORD fi if [ -z "$NOCODB_DATABASE_READONLYUSER" ]; then NOCODB_DATABASE_READONLYUSER=nocodb-readonly updateConfigVar NOCODB_DATABASE_READONLYUSER $NOCODB_DATABASE_READONLYUSER fi if [ -z "$NOCODB_DATABASE_READONLYUSER_PASSWORD" ]; then NOCODB_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NOCODB_DATABASE_READONLYUSER_PASSWORD $NOCODB_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$NOCODB_REDIS_PASSWORD" ]; then NOCODB_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NOCODB_REDIS_PASSWORD $NOCODB_REDIS_PASSWORD fi if [ -z "$NOCODB_AUTH_JWT_SECRET" ]; then NOCODB_AUTH_JWT_SECRET=$(pwgen -c -n 32 1) updateConfigVar NOCODB_AUTH_JWT_SECRET $NOCODB_AUTH_JWT_SECRET fi if [ -z "$NOCODB_MINIO_KEY" ]; then NOCODB_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar NOCODB_MINIO_KEY $NOCODB_MINIO_KEY fi if [ -z "$NOCODB_MINIO_SECRET" ]; then NOCODB_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar NOCODB_MINIO_SECRET $NOCODB_MINIO_SECRET fi if [ -z "$ENTE_ADMIN_USERNAME" ]; then ENTE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_ente" updateConfigVar ENTE_ADMIN_USERNAME $ENTE_ADMIN_USERNAME fi if [ -z "$ENTE_ADMIN_EMAIL_ADDRESS" ]; then ENTE_ADMIN_EMAIL_ADDRESS=$ENTE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar ENTE_ADMIN_EMAIL_ADDRESS $ENTE_ADMIN_EMAIL_ADDRESS fi if [ -z "$ENTE_ADMIN_PASSWORD" ]; then ENTE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ENTE_ADMIN_PASSWORD $ENTE_ADMIN_PASSWORD fi if [ -z "$ENTE_DATABASE_NAME" ]; then ENTE_DATABASE_NAME=entedb updateConfigVar ENTE_DATABASE_NAME $ENTE_DATABASE_NAME fi if [ -z "$ENTE_DATABASE_USER" ]; then ENTE_DATABASE_USER=ente-user updateConfigVar ENTE_DATABASE_USER $ENTE_DATABASE_USER fi if [ -z "$ENTE_DATABASE_USER_PASSWORD" ]; then ENTE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ENTE_DATABASE_USER_PASSWORD $ENTE_DATABASE_USER_PASSWORD fi if [ -z "$ENTE_DATABASE_READONLYUSER" ]; then ENTE_DATABASE_READONLYUSER=ente-readonly updateConfigVar ENTE_DATABASE_READONLYUSER $ENTE_DATABASE_READONLYUSER fi if [ -z "$ENTE_DATABASE_READONLYUSER_PASSWORD" ]; then ENTE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar ENTE_DATABASE_READONLYUSER_PASSWORD $ENTE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$ENTE_MINIO_KEY" ]; then ENTE_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar ENTE_MINIO_KEY $ENTE_MINIO_KEY fi if [ -z "$ENTE_MINIO_SECRET" ]; then ENTE_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar ENTE_MINIO_SECRET $ENTE_MINIO_SECRET fi if [ -z "$ENTE_MUSEUM_KEY" ]; then ENTE_MUSEUM_KEY=$(head -c 32 /dev/urandom | base64 | tr -d '\n') updateConfigVar ENTE_MUSEUM_KEY $ENTE_MUSEUM_KEY fi if [ -z "$ENTE_MUSEUM_HASH" ]; then ENTE_MUSEUM_HASH=$(head -c 64 /dev/urandom | base64 | tr -d '\n') updateConfigVar ENTE_MUSEUM_HASH $ENTE_MUSEUM_HASH fi if [ -z "$ENTE_MUSEUM_JWT_SECRET" ]; then ENTE_MUSEUM_JWT_SECRET=$(head -c 32 /dev/urandom | base64 | tr -d '\n' | tr '+/' '-_') updateConfigVar ENTE_MUSEUM_JWT_SECRET $ENTE_MUSEUM_JWT_SECRET fi if [ -z "$MORPHIC_ADMIN_USERNAME" ]; then MORPHIC_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_morphic" updateConfigVar MORPHIC_ADMIN_USERNAME $MORPHIC_ADMIN_USERNAME fi if [ -z "$MORPHIC_ADMIN_EMAIL_ADDRESS" ]; then MORPHIC_ADMIN_EMAIL_ADDRESS=$MORPHIC_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MORPHIC_ADMIN_EMAIL_ADDRESS $MORPHIC_ADMIN_EMAIL_ADDRESS fi if [ -z "$MORPHIC_ADMIN_PASSWORD" ]; then MORPHIC_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MORPHIC_ADMIN_PASSWORD $MORPHIC_ADMIN_PASSWORD fi if [ -z "$MORPHIC_DATABASE_NAME" ]; then MORPHIC_DATABASE_NAME=postgres updateConfigVar MORPHIC_DATABASE_NAME $MORPHIC_DATABASE_NAME fi if [ -z "$MORPHIC_DATABASE_USER" ]; then MORPHIC_DATABASE_USER=supabase_admin updateConfigVar MORPHIC_DATABASE_USER $MORPHIC_DATABASE_USER fi if [ -z "$MORPHIC_DATABASE_USER_PASSWORD" ]; then MORPHIC_DATABASE_USER_PASSWORD=$(openssl rand -hex 16) updateConfigVar MORPHIC_DATABASE_USER_PASSWORD $MORPHIC_DATABASE_USER_PASSWORD fi if [ -z "$MORPHIC_DATABASE_READONLYUSER" ]; then MORPHIC_DATABASE_READONLYUSER=morphic-readonly updateConfigVar MORPHIC_DATABASE_READONLYUSER $MORPHIC_DATABASE_READONLYUSER fi if [ -z "$MORPHIC_DATABASE_READONLYUSER_PASSWORD" ]; then MORPHIC_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MORPHIC_DATABASE_READONLYUSER_PASSWORD $MORPHIC_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MORPHIC_REDIS_PASSWORD" ]; then MORPHIC_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MORPHIC_REDIS_PASSWORD $MORPHIC_REDIS_PASSWORD fi if [ -z "$MORPHIC_SUPABASE_DASHBOARD_USERNAME" ]; then MORPHIC_SUPABASE_DASHBOARD_USERNAME=$ADMIN_USERNAME_BASE"_morphic_supabase" updateConfigVar MORPHIC_SUPABASE_DASHBOARD_USERNAME $MORPHIC_SUPABASE_DASHBOARD_USERNAME fi if [ -z "$MORPHIC_SUPABASE_DASHBOARD_PASSWORD" ]; then MORPHIC_SUPABASE_DASHBOARD_PASSWORD=$(openssl rand -hex 16) updateConfigVar MORPHIC_SUPABASE_DASHBOARD_PASSWORD $MORPHIC_SUPABASE_DASHBOARD_PASSWORD fi if [ -z "$MORPHIC_SUPABASE_JWT_SECRET" ]; then MORPHIC_SUPABASE_JWT_SECRET=$(openssl rand -base64 30) updateConfigVar MORPHIC_SUPABASE_JWT_SECRET $MORPHIC_SUPABASE_JWT_SECRET fi if [ -z "$MORPHIC_SUPABASE_ANON_KEY" ]; then MORPHIC_SUPABASE_ANON_KEY=$(generateFullJWTToken "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" "{\"role\":\"anon\",\"iss\":\"supabase\",\"iat\":$(date +%s),\"exp\":$(($(date +%s) + 20 * 3600 * 24 * 365))}" "$MORPHIC_SUPABASE_JWT_SECRET") updateConfigVar MORPHIC_SUPABASE_ANON_KEY $MORPHIC_SUPABASE_ANON_KEY fi if [ -z "$MORPHIC_SUPABASE_DB_ENC_KEY" ]; then MORPHIC_SUPABASE_DB_ENC_KEY=$(pwgen -c -n 16 1) updateConfigVar MORPHIC_SUPABASE_DB_ENC_KEY $MORPHIC_SUPABASE_DB_ENC_KEY fi if [ -z "$MORPHIC_SUPABASE_PG_META_CRYPTO_KEY" ]; then MORPHIC_SUPABASE_PG_META_CRYPTO_KEY=$(openssl rand -base64 24) updateConfigVar MORPHIC_SUPABASE_PG_META_CRYPTO_KEY $MORPHIC_SUPABASE_PG_META_CRYPTO_KEY fi if [ -z "$MORPHIC_SUPABASE_SECRET_KEY_BASE" ]; then MORPHIC_SUPABASE_SECRET_KEY_BASE=$(openssl rand -base64 48) updateConfigVar MORPHIC_SUPABASE_SECRET_KEY_BASE $MORPHIC_SUPABASE_SECRET_KEY_BASE fi if [ -z "$MORPHIC_SUPABASE_SERVICE_ROLE_KEY" ]; then MORPHIC_SUPABASE_SERVICE_ROLE_KEY=$(generateFullJWTToken "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" "{\"role\":\"service_role\",\"iss\":\"supabase\",\"iat\":$(date +%s),\"exp\":$(($(date +%s) + 20 * 3600 * 24 * 365))}" "$MORPHIC_SUPABASE_JWT_SECRET") updateConfigVar MORPHIC_SUPABASE_SERVICE_ROLE_KEY $MORPHIC_SUPABASE_SERVICE_ROLE_KEY fi if [ -z "$MORPHIC_SUPABASE_VAULT_ENC_KEY" ]; then MORPHIC_SUPABASE_VAULT_ENC_KEY=$(openssl rand -hex 16) updateConfigVar MORPHIC_SUPABASE_VAULT_ENC_KEY $MORPHIC_SUPABASE_VAULT_ENC_KEY fi if [ -z "$MORPHIC_SUPABASE_LOGFLARE_PRIVATE_ACCESS_TOKEN" ]; then MORPHIC_SUPABASE_LOGFLARE_PRIVATE_ACCESS_TOKEN=$(openssl rand -base64 24) updateConfigVar MORPHIC_SUPABASE_LOGFLARE_PRIVATE_ACCESS_TOKEN $MORPHIC_SUPABASE_LOGFLARE_PRIVATE_ACCESS_TOKEN fi if [ -z "$MORPHIC_SUPABASE_LOGFLARE_PUBLIC_ACCESS_TOKEN" ]; then MORPHIC_SUPABASE_LOGFLARE_PUBLIC_ACCESS_TOKEN=$(openssl rand -base64 24) updateConfigVar MORPHIC_SUPABASE_LOGFLARE_PUBLIC_ACCESS_TOKEN $MORPHIC_SUPABASE_LOGFLARE_PUBLIC_ACCESS_TOKEN fi if [ -z "$OPENNOTEBOOK_ADMIN_USERNAME" ]; then OPENNOTEBOOK_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_opennotebook" updateConfigVar OPENNOTEBOOK_ADMIN_USERNAME $OPENNOTEBOOK_ADMIN_USERNAME fi if [ -z "$OPENNOTEBOOK_ADMIN_EMAIL_ADDRESS" ]; then OPENNOTEBOOK_ADMIN_EMAIL_ADDRESS=$OPENNOTEBOOK_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar OPENNOTEBOOK_ADMIN_EMAIL_ADDRESS $OPENNOTEBOOK_ADMIN_EMAIL_ADDRESS fi if [ -z "$OPENNOTEBOOK_ADMIN_PASSWORD" ]; then OPENNOTEBOOK_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENNOTEBOOK_ADMIN_PASSWORD $OPENNOTEBOOK_ADMIN_PASSWORD fi if [ -z "$OPENNOTEBOOK_SURREALDB_NAME" ]; then OPENNOTEBOOK_SURREALDB_NAME=opennotebookdb updateConfigVar OPENNOTEBOOK_SURREALDB_NAME $OPENNOTEBOOK_SURREALDB_NAME fi if [ -z "$OPENNOTEBOOK_SURREALDB_USER" ]; then OPENNOTEBOOK_SURREALDB_USER=opennotebook-user updateConfigVar OPENNOTEBOOK_SURREALDB_USER $OPENNOTEBOOK_SURREALDB_USER fi if [ -z "$OPENNOTEBOOK_SURREALDB_PASSWORD" ]; then OPENNOTEBOOK_SURREALDB_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENNOTEBOOK_SURREALDB_PASSWORD $OPENNOTEBOOK_SURREALDB_PASSWORD fi if [ -z "$OPENNOTEBOOK_ENCRYPTION_KEY" ]; then OPENNOTEBOOK_ENCRYPTION_KEY=$(openssl rand -base64 32) updateConfigVar OPENNOTEBOOK_ENCRYPTION_KEY $OPENNOTEBOOK_ENCRYPTION_KEY fi if [ -z "$APPSMITH_ADMIN_USERNAME" ]; then APPSMITH_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_appsmith" updateConfigVar APPSMITH_ADMIN_USERNAME $APPSMITH_ADMIN_USERNAME fi if [ -z "$APPSMITH_ADMIN_EMAIL_ADDRESS" ]; then APPSMITH_ADMIN_EMAIL_ADDRESS=$APPSMITH_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar APPSMITH_ADMIN_EMAIL_ADDRESS $APPSMITH_ADMIN_EMAIL_ADDRESS fi if [ -z "$APPSMITH_ADMIN_PASSWORD" ]; then APPSMITH_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar APPSMITH_ADMIN_PASSWORD $APPSMITH_ADMIN_PASSWORD fi if [ -z "$APPSMITH_SUPERVISOR_USER" ]; then APPSMITH_SUPERVISOR_USER=appsmith_supervisor updateConfigVar APPSMITH_SUPERVISOR_USER $APPSMITH_SUPERVISOR_USER fi if [ -z "$APPSMITH_SUPERVISOR_PASSWORD" ]; then APPSMITH_SUPERVISOR_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar APPSMITH_SUPERVISOR_PASSWORD $APPSMITH_SUPERVISOR_PASSWORD fi if [ -z "$TRILIUM_ADMIN_USERNAME" ]; then TRILIUM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_trilium" updateConfigVar TRILIUM_ADMIN_USERNAME $TRILIUM_ADMIN_USERNAME fi if [ -z "$TRILIUM_ADMIN_PASSWORD" ]; then TRILIUM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar TRILIUM_ADMIN_PASSWORD $TRILIUM_ADMIN_PASSWORD fi if [ -z "$TRILIUM_OIDC_CLIENT_ID" ]; then TRILIUM_OIDC_CLIENT_ID=trilium updateConfigVar TRILIUM_OIDC_CLIENT_ID $TRILIUM_OIDC_CLIENT_ID fi if [ -z "$TRILIUM_OIDC_CLIENT_SECRET" ]; then TRILIUM_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar TRILIUM_OIDC_CLIENT_SECRET $TRILIUM_OIDC_CLIENT_SECRET fi if [ -z "$DOCSGPT_ADMIN_USERNAME" ]; then DOCSGPT_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_docsgpt" updateConfigVar DOCSGPT_ADMIN_USERNAME $DOCSGPT_ADMIN_USERNAME fi if [ -z "$DOCSGPT_ADMIN_EMAIL_ADDRESS" ]; then DOCSGPT_ADMIN_EMAIL_ADDRESS=$DOCSGPT_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar DOCSGPT_ADMIN_EMAIL_ADDRESS $DOCSGPT_ADMIN_EMAIL_ADDRESS fi if [ -z "$DOCSGPT_ADMIN_PASSWORD" ]; then DOCSGPT_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCSGPT_ADMIN_PASSWORD $DOCSGPT_ADMIN_PASSWORD fi if [ -z "$DOCSGPT_DATABASE_NAME" ]; then DOCSGPT_DATABASE_NAME=docsgptdb updateConfigVar DOCSGPT_DATABASE_NAME $DOCSGPT_DATABASE_NAME fi if [ -z "$DOCSGPT_DATABASE_USER" ]; then DOCSGPT_DATABASE_USER=docsgpt-user updateConfigVar DOCSGPT_DATABASE_USER $DOCSGPT_DATABASE_USER fi if [ -z "$DOCSGPT_DATABASE_USER_PASSWORD" ]; then DOCSGPT_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCSGPT_DATABASE_USER_PASSWORD $DOCSGPT_DATABASE_USER_PASSWORD fi if [ -z "$DOCSGPT_DATABASE_READONLYUSER" ]; then DOCSGPT_DATABASE_READONLYUSER=docsgpt-readonly updateConfigVar DOCSGPT_DATABASE_READONLYUSER $DOCSGPT_DATABASE_READONLYUSER fi if [ -z "$DOCSGPT_DATABASE_READONLYUSER_PASSWORD" ]; then DOCSGPT_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCSGPT_DATABASE_READONLYUSER_PASSWORD $DOCSGPT_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$DOCSGPT_REDIS_PASSWORD" ]; then DOCSGPT_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DOCSGPT_REDIS_PASSWORD $DOCSGPT_REDIS_PASSWORD fi if [ -z "$DOCSGPT_API_KEY" ]; then DOCSGPT_API_KEY=$(pwgen -c -n 32 1) updateConfigVar DOCSGPT_API_KEY $DOCSGPT_API_KEY fi if [ -z "$DOCSGPT_JWT_SECRET" ]; then DOCSGPT_JWT_SECRET=$(pwgen -c -n 32 1) updateConfigVar DOCSGPT_JWT_SECRET $DOCSGPT_JWT_SECRET fi if [ -z "$MEMOS_ADMIN_USERNAME" ]; then MEMOS_ADMIN_USERNAME="memosadmin" updateConfigVar MEMOS_ADMIN_USERNAME $MEMOS_ADMIN_USERNAME fi if [ -z "$MEMOS_ADMIN_EMAIL_ADDRESS" ]; then MEMOS_ADMIN_EMAIL_ADDRESS=$ADMIN_USERNAME_BASE"_memos"@$HOMESERVER_DOMAIN updateConfigVar MEMOS_ADMIN_EMAIL_ADDRESS $MEMOS_ADMIN_EMAIL_ADDRESS fi if [ -z "$MEMOS_ADMIN_PASSWORD" ]; then MEMOS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MEMOS_ADMIN_PASSWORD $MEMOS_ADMIN_PASSWORD fi if [ -z "$MEMOS_DATABASE_NAME" ]; then MEMOS_DATABASE_NAME=memosdb updateConfigVar MEMOS_DATABASE_NAME $MEMOS_DATABASE_NAME fi if [ -z "$MEMOS_DATABASE_USER" ]; then MEMOS_DATABASE_USER=memos-user updateConfigVar MEMOS_DATABASE_USER $MEMOS_DATABASE_USER fi if [ -z "$MEMOS_DATABASE_USER_PASSWORD" ]; then MEMOS_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MEMOS_DATABASE_USER_PASSWORD $MEMOS_DATABASE_USER_PASSWORD fi if [ -z "$MEMOS_DATABASE_READONLYUSER" ]; then MEMOS_DATABASE_READONLYUSER=memos-readonly updateConfigVar MEMOS_DATABASE_READONLYUSER $MEMOS_DATABASE_READONLYUSER fi if [ -z "$MEMOS_DATABASE_READONLYUSER_PASSWORD" ]; then MEMOS_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MEMOS_DATABASE_READONLYUSER_PASSWORD $MEMOS_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MEMOS_OIDC_CLIENT_ID" ]; then MEMOS_OIDC_CLIENT_ID=memos updateConfigVar MEMOS_OIDC_CLIENT_ID $MEMOS_OIDC_CLIENT_ID fi if [ -z "$MEMOS_OIDC_CLIENT_SECRET" ]; then MEMOS_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar MEMOS_OIDC_CLIENT_SECRET $MEMOS_OIDC_CLIENT_SECRET fi if [ -z "$LEMONADE_API_KEY" ]; then LEMONADE_API_KEY=$(pwgen -c -n 32 1) updateConfigVar LEMONADE_API_KEY $LEMONADE_API_KEY fi if [ -z "$SPEAKR_ADMIN_USERNAME" ]; then SPEAKR_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_speakr" updateConfigVar SPEAKR_ADMIN_USERNAME $SPEAKR_ADMIN_USERNAME fi if [ -z "$SPEAKR_ADMIN_EMAIL_ADDRESS" ]; then SPEAKR_ADMIN_EMAIL_ADDRESS=$SPEAKR_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar SPEAKR_ADMIN_EMAIL_ADDRESS $SPEAKR_ADMIN_EMAIL_ADDRESS fi if [ -z "$SPEAKR_ADMIN_PASSWORD" ]; then SPEAKR_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEAKR_ADMIN_PASSWORD $SPEAKR_ADMIN_PASSWORD fi if [ -z "$SPEAKR_DATABASE_NAME" ]; then SPEAKR_DATABASE_NAME=speakrdb updateConfigVar SPEAKR_DATABASE_NAME $SPEAKR_DATABASE_NAME fi if [ -z "$SPEAKR_DATABASE_USER" ]; then SPEAKR_DATABASE_USER=speakr-user updateConfigVar SPEAKR_DATABASE_USER $SPEAKR_DATABASE_USER fi if [ -z "$SPEAKR_DATABASE_USER_PASSWORD" ]; then SPEAKR_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEAKR_DATABASE_USER_PASSWORD $SPEAKR_DATABASE_USER_PASSWORD fi if [ -z "$SPEAKR_DATABASE_READONLYUSER" ]; then SPEAKR_DATABASE_READONLYUSER=speakr-readonly updateConfigVar SPEAKR_DATABASE_READONLYUSER $SPEAKR_DATABASE_READONLYUSER fi if [ -z "$SPEAKR_DATABASE_READONLYUSER_PASSWORD" ]; then SPEAKR_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SPEAKR_DATABASE_READONLYUSER_PASSWORD $SPEAKR_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$SPEAKR_OIDC_CLIENT_ID" ]; then SPEAKR_OIDC_CLIENT_ID=speakr updateConfigVar SPEAKR_OIDC_CLIENT_ID $SPEAKR_OIDC_CLIENT_ID fi if [ -z "$SPEAKR_OIDC_CLIENT_SECRET" ]; then SPEAKR_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar SPEAKR_OIDC_CLIENT_SECRET $SPEAKR_OIDC_CLIENT_SECRET fi if [ -z "$INSANELYFASTWHISPER_API_KEY" ]; then INSANELYFASTWHISPER_API_KEY=$(pwgen -c -n 32 1) updateConfigVar INSANELYFASTWHISPER_API_KEY $INSANELYFASTWHISPER_API_KEY fi if [ -z "$MONICA_ADMIN_USERNAME" ]; then MONICA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_monica" updateConfigVar MONICA_ADMIN_USERNAME $MONICA_ADMIN_USERNAME fi if [ -z "$MONICA_ADMIN_EMAIL_ADDRESS" ]; then MONICA_ADMIN_EMAIL_ADDRESS=$MONICA_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MONICA_ADMIN_EMAIL_ADDRESS $MONICA_ADMIN_EMAIL_ADDRESS fi if [ -z "$MONICA_ADMIN_PASSWORD" ]; then MONICA_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar MONICA_ADMIN_PASSWORD $MONICA_ADMIN_PASSWORD fi if [ -z "$MONICA_DATABASE_NAME" ]; then MONICA_DATABASE_NAME=monicadb updateConfigVar MONICA_DATABASE_NAME $MONICA_DATABASE_NAME fi if [ -z "$MONICA_DATABASE_ROOT_PASSWORD" ]; then MONICA_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MONICA_DATABASE_ROOT_PASSWORD $MONICA_DATABASE_ROOT_PASSWORD fi if [ -z "$MONICA_DATABASE_USER" ]; then MONICA_DATABASE_USER=monica-user updateConfigVar MONICA_DATABASE_USER $MONICA_DATABASE_USER fi if [ -z "$MONICA_DATABASE_USER_PASSWORD" ]; then MONICA_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MONICA_DATABASE_USER_PASSWORD $MONICA_DATABASE_USER_PASSWORD fi if [ -z "$MONICA_DATABASE_READONLYUSER" ]; then MONICA_DATABASE_READONLYUSER=monica-readonly updateConfigVar MONICA_DATABASE_READONLYUSER $MONICA_DATABASE_READONLYUSER fi if [ -z "$MONICA_DATABASE_READONLYUSER_PASSWORD" ]; then MONICA_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MONICA_DATABASE_READONLYUSER_PASSWORD $MONICA_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MONICA_REDIS_PASSWORD" ]; then MONICA_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MONICA_REDIS_PASSWORD $MONICA_REDIS_PASSWORD fi if [ -z "$MONICA_APP_KEY" ]; then MONICA_APP_KEY=$(echo -n 'base64:'; openssl rand -base64 32) updateConfigVar MONICA_APP_KEY $MONICA_APP_KEY fi if [ -z "$MONICA_MEILISEARCH_KEY" ]; then MONICA_MEILISEARCH_KEY=$(pwgen -c -n 64 1) updateConfigVar MONICA_MEILISEARCH_KEY $MONICA_MEILISEARCH_KEY fi if [ -z "$MONICA_MEMCACHE_PASSWORD" ]; then MONICA_MEMCACHE_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MONICA_MEMCACHE_PASSWORD $MONICA_MEMCACHE_PASSWORD fi if [ -z "$AFFINE_ADMIN_USERNAME" ]; then AFFINE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_affine" updateConfigVar AFFINE_ADMIN_USERNAME $AFFINE_ADMIN_USERNAME fi if [ -z "$AFFINE_ADMIN_EMAIL_ADDRESS" ]; then AFFINE_ADMIN_EMAIL_ADDRESS=$AFFINE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar AFFINE_ADMIN_EMAIL_ADDRESS $AFFINE_ADMIN_EMAIL_ADDRESS fi if [ -z "$AFFINE_ADMIN_PASSWORD" ]; then AFFINE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AFFINE_ADMIN_PASSWORD $AFFINE_ADMIN_PASSWORD fi if [ -z "$AFFINE_DATABASE_NAME" ]; then AFFINE_DATABASE_NAME=affinedb updateConfigVar AFFINE_DATABASE_NAME $AFFINE_DATABASE_NAME fi if [ -z "$AFFINE_DATABASE_USER" ]; then AFFINE_DATABASE_USER=affine-user updateConfigVar AFFINE_DATABASE_USER $AFFINE_DATABASE_USER fi if [ -z "$AFFINE_DATABASE_USER_PASSWORD" ]; then AFFINE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AFFINE_DATABASE_USER_PASSWORD $AFFINE_DATABASE_USER_PASSWORD fi if [ -z "$AFFINE_DATABASE_READONLYUSER" ]; then AFFINE_DATABASE_READONLYUSER=affine-readonly updateConfigVar AFFINE_DATABASE_READONLYUSER $AFFINE_DATABASE_READONLYUSER fi if [ -z "$AFFINE_DATABASE_READONLYUSER_PASSWORD" ]; then AFFINE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AFFINE_DATABASE_READONLYUSER_PASSWORD $AFFINE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$AFFINE_REDIS_PASSWORD" ]; then AFFINE_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AFFINE_REDIS_PASSWORD $AFFINE_REDIS_PASSWORD fi if [ -z "$AFFINE_OIDC_CLIENT_ID" ]; then AFFINE_OIDC_CLIENT_ID=affine updateConfigVar AFFINE_OIDC_CLIENT_ID $AFFINE_OIDC_CLIENT_ID fi if [ -z "$AFFINE_OIDC_CLIENT_SECRET" ]; then AFFINE_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar AFFINE_OIDC_CLIENT_SECRET $AFFINE_OIDC_CLIENT_SECRET fi if [ -z "$JOPLIN_ADMIN_USERNAME" ]; then JOPLIN_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_joplin" updateConfigVar JOPLIN_ADMIN_USERNAME $JOPLIN_ADMIN_USERNAME fi if [ -z "$JOPLIN_ADMIN_EMAIL_ADDRESS" ]; then JOPLIN_ADMIN_EMAIL_ADDRESS=$JOPLIN_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar JOPLIN_ADMIN_EMAIL_ADDRESS $JOPLIN_ADMIN_EMAIL_ADDRESS fi if [ -z "$JOPLIN_ADMIN_PASSWORD" ]; then JOPLIN_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar JOPLIN_ADMIN_PASSWORD $JOPLIN_ADMIN_PASSWORD fi if [ -z "$JOPLIN_DATABASE_NAME" ]; then JOPLIN_DATABASE_NAME=joplindb updateConfigVar JOPLIN_DATABASE_NAME $JOPLIN_DATABASE_NAME fi if [ -z "$JOPLIN_DATABASE_USER" ]; then JOPLIN_DATABASE_USER=joplin-user updateConfigVar JOPLIN_DATABASE_USER $JOPLIN_DATABASE_USER fi if [ -z "$JOPLIN_DATABASE_USER_PASSWORD" ]; then JOPLIN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar JOPLIN_DATABASE_USER_PASSWORD $JOPLIN_DATABASE_USER_PASSWORD fi if [ -z "$JOPLIN_DATABASE_READONLYUSER" ]; then JOPLIN_DATABASE_READONLYUSER=joplin-readonly updateConfigVar JOPLIN_DATABASE_READONLYUSER $JOPLIN_DATABASE_READONLYUSER fi if [ -z "$JOPLIN_DATABASE_READONLYUSER_PASSWORD" ]; then JOPLIN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar JOPLIN_DATABASE_READONLYUSER_PASSWORD $JOPLIN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$JOPLIN_TRANSCRIBE_DATABASE_NAME" ]; then JOPLIN_TRANSCRIBE_DATABASE_NAME=joplintranscribedb updateConfigVar JOPLIN_TRANSCRIBE_DATABASE_NAME $JOPLIN_TRANSCRIBE_DATABASE_NAME fi if [ -z "$JOPLIN_TRANSCRIBE_DATABASE_USER" ]; then JOPLIN_TRANSCRIBE_DATABASE_USER=joplintranscribe-user updateConfigVar JOPLIN_TRANSCRIBE_DATABASE_USER $JOPLIN_TRANSCRIBE_DATABASE_USER fi if [ -z "$JOPLIN_TRANSCRIBE_DATABASE_USER_PASSWORD" ]; then JOPLIN_TRANSCRIBE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar JOPLIN_TRANSCRIBE_DATABASE_USER_PASSWORD $JOPLIN_TRANSCRIBE_DATABASE_USER_PASSWORD fi if [ -z "$JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER" ]; then JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER=joplin-transcribe-readonly updateConfigVar JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER $JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER fi if [ -z "$JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER_PASSWORD" ]; then JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER_PASSWORD $JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$JOPLIN_TRANSCRIBE_API_KEY" ]; then JOPLIN_TRANSCRIBE_API_KEY=$(pwgen -c -n 32 1) updateConfigVar JOPLIN_TRANSCRIBE_API_KEY $JOPLIN_TRANSCRIBE_API_KEY fi if [ -z "$SUPERSET_ADMIN_USERNAME" ]; then SUPERSET_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_superset" updateConfigVar SUPERSET_ADMIN_USERNAME $SUPERSET_ADMIN_USERNAME fi if [ -z "$SUPERSET_ADMIN_EMAIL_ADDRESS" ]; then SUPERSET_ADMIN_EMAIL_ADDRESS=$SUPERSET_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar SUPERSET_ADMIN_EMAIL_ADDRESS $SUPERSET_ADMIN_EMAIL_ADDRESS fi if [ -z "$SUPERSET_ADMIN_PASSWORD" ]; then SUPERSET_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUPERSET_ADMIN_PASSWORD $SUPERSET_ADMIN_PASSWORD fi if [ -z "$SUPERSET_DATABASE_NAME" ]; then SUPERSET_DATABASE_NAME=supersetdb updateConfigVar SUPERSET_DATABASE_NAME $SUPERSET_DATABASE_NAME fi if [ -z "$SUPERSET_DATABASE_USER" ]; then SUPERSET_DATABASE_USER=superset-user updateConfigVar SUPERSET_DATABASE_USER $SUPERSET_DATABASE_USER fi if [ -z "$SUPERSET_DATABASE_USER_PASSWORD" ]; then SUPERSET_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUPERSET_DATABASE_USER_PASSWORD $SUPERSET_DATABASE_USER_PASSWORD fi if [ -z "$SUPERSET_DATABASE_READONLYUSER" ]; then SUPERSET_DATABASE_READONLYUSER=superset-readonly updateConfigVar SUPERSET_DATABASE_READONLYUSER $SUPERSET_DATABASE_READONLYUSER fi if [ -z "$SUPERSET_DATABASE_READONLYUSER_PASSWORD" ]; then SUPERSET_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUPERSET_DATABASE_READONLYUSER_PASSWORD $SUPERSET_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$SUPERSET_SECRET_KEY" ]; then SUPERSET_SECRET_KEY=$(openssl rand -base64 42) updateConfigVar SUPERSET_SECRET_KEY $SUPERSET_SECRET_KEY fi if [ -z "$SUPERSET_JWT_SECRET" ]; then SUPERSET_JWT_SECRET=$(pwgen -c -n 64 1) updateConfigVar SUPERSET_JWT_SECRET $SUPERSET_JWT_SECRET fi if [ -z "$KOKORO_API_KEY" ]; then KOKORO_API_KEY=$(pwgen -c -n 32 1) updateConfigVar KOKORO_API_KEY $KOKORO_API_KEY fi if [ -z "$CHATTERBOX_API_KEY" ]; then CHATTERBOX_API_KEY=$(pwgen -c -n 32 1) updateConfigVar CHATTERBOX_API_KEY $CHATTERBOX_API_KEY fi if [ -z "$LITELLM_ADMIN_USERNAME" ]; then LITELLM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_litellm" updateConfigVar LITELLM_ADMIN_USERNAME $LITELLM_ADMIN_USERNAME fi if [ -z "$LITELLM_ADMIN_EMAIL_ADDRESS" ]; then LITELLM_ADMIN_EMAIL_ADDRESS=$LITELLM_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar LITELLM_ADMIN_EMAIL_ADDRESS $LITELLM_ADMIN_EMAIL_ADDRESS fi if [ -z "$LITELLM_ADMIN_PASSWORD" ]; then LITELLM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LITELLM_ADMIN_PASSWORD $LITELLM_ADMIN_PASSWORD fi if [ -z "$LITELLM_DATABASE_NAME" ]; then LITELLM_DATABASE_NAME=litellmdb updateConfigVar LITELLM_DATABASE_NAME $LITELLM_DATABASE_NAME fi if [ -z "$LITELLM_DATABASE_USER" ]; then LITELLM_DATABASE_USER=litellm-user updateConfigVar LITELLM_DATABASE_USER $LITELLM_DATABASE_USER fi if [ -z "$LITELLM_DATABASE_USER_PASSWORD" ]; then LITELLM_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LITELLM_DATABASE_USER_PASSWORD $LITELLM_DATABASE_USER_PASSWORD fi if [ -z "$LITELLM_DATABASE_READONLYUSER" ]; then LITELLM_DATABASE_READONLYUSER=litellm-readonly updateConfigVar LITELLM_DATABASE_READONLYUSER $LITELLM_DATABASE_READONLYUSER fi if [ -z "$LITELLM_DATABASE_READONLYUSER_PASSWORD" ]; then LITELLM_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LITELLM_DATABASE_READONLYUSER_PASSWORD $LITELLM_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LITELLM_REDIS_PASSWORD" ]; then LITELLM_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LITELLM_REDIS_PASSWORD $LITELLM_REDIS_PASSWORD fi if [ -z "$LITELLM_MASTER_KEY" ]; then LITELLM_MASTER_KEY=sk-$(pwgen -c -n 32 1) updateConfigVar LITELLM_MASTER_KEY $LITELLM_MASTER_KEY fi if [ -z "$LITELLM_OIDC_CLIENT_ID" ]; then LITELLM_OIDC_CLIENT_ID=litellm updateConfigVar LITELLM_OIDC_CLIENT_ID $LITELLM_OIDC_CLIENT_ID fi if [ -z "$LITELLM_OIDC_CLIENT_SECRET" ]; then LITELLM_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar LITELLM_OIDC_CLIENT_SECRET $LITELLM_OIDC_CLIENT_SECRET fi if [ -z "$LITELLM_SALT_KEY" ]; then LITELLM_SALT_KEY=sk-$(pwgen -c -n 32 1) updateConfigVar LITELLM_SALT_KEY $LITELLM_SALT_KEY fi if [ -z "$LANGFUSE_ADMIN_USERNAME" ]; then LANGFUSE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_langfuse" updateConfigVar LANGFUSE_ADMIN_USERNAME $LANGFUSE_ADMIN_USERNAME fi if [ -z "$LANGFUSE_ADMIN_EMAIL_ADDRESS" ]; then LANGFUSE_ADMIN_EMAIL_ADDRESS=$LANGFUSE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar LANGFUSE_ADMIN_EMAIL_ADDRESS $LANGFUSE_ADMIN_EMAIL_ADDRESS fi if [ -z "$LANGFUSE_ADMIN_PASSWORD" ]; then LANGFUSE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFUSE_ADMIN_PASSWORD $LANGFUSE_ADMIN_PASSWORD fi if [ -z "$LANGFUSE_DATABASE_NAME" ]; then LANGFUSE_DATABASE_NAME=langfusedb updateConfigVar LANGFUSE_DATABASE_NAME $LANGFUSE_DATABASE_NAME fi if [ -z "$LANGFUSE_DATABASE_USER" ]; then LANGFUSE_DATABASE_USER=langfuse-user updateConfigVar LANGFUSE_DATABASE_USER $LANGFUSE_DATABASE_USER fi if [ -z "$LANGFUSE_DATABASE_USER_PASSWORD" ]; then LANGFUSE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFUSE_DATABASE_USER_PASSWORD $LANGFUSE_DATABASE_USER_PASSWORD fi if [ -z "$LANGFUSE_DATABASE_READONLYUSER" ]; then LANGFUSE_DATABASE_READONLYUSER=langfuse-readonly updateConfigVar LANGFUSE_DATABASE_READONLYUSER $LANGFUSE_DATABASE_READONLYUSER fi if [ -z "$LANGFUSE_DATABASE_READONLYUSER_PASSWORD" ]; then LANGFUSE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFUSE_DATABASE_READONLYUSER_PASSWORD $LANGFUSE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LANGFUSE_REDIS_PASSWORD" ]; then LANGFUSE_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFUSE_REDIS_PASSWORD $LANGFUSE_REDIS_PASSWORD fi if [ -z "$LANGFUSE_PUBLIC_KEY" ]; then LANGFUSE_PUBLIC_KEY=pk-lf-$(uuidgen) updateConfigVar LANGFUSE_PUBLIC_KEY $LANGFUSE_PUBLIC_KEY fi if [ -z "$LANGFUSE_SECRET_KEY" ]; then LANGFUSE_SECRET_KEY=sk-lf-$(uuidgen) updateConfigVar LANGFUSE_SECRET_KEY $LANGFUSE_SECRET_KEY fi if [ -z "$LANGFUSE_INIT_PROJECT_ID" ]; then LANGFUSE_INIT_PROJECT_ID=$(uuidgen) updateConfigVar LANGFUSE_INIT_PROJECT_ID $LANGFUSE_INIT_PROJECT_ID fi if [ -z "$LANGFUSE_MINIO_KEY" ]; then LANGFUSE_MINIO_KEY=$(pwgen -c -n 32 1) updateConfigVar LANGFUSE_MINIO_KEY $LANGFUSE_MINIO_KEY fi if [ -z "$LANGFUSE_MINIO_SECRET" ]; then LANGFUSE_MINIO_SECRET=$(pwgen -c -n 32 1) updateConfigVar LANGFUSE_MINIO_SECRET $LANGFUSE_MINIO_SECRET fi if [ -z "$LANGFUSE_OIDC_CLIENT_ID" ]; then LANGFUSE_OIDC_CLIENT_ID=langfuse updateConfigVar LANGFUSE_OIDC_CLIENT_ID $LANGFUSE_OIDC_CLIENT_ID fi if [ -z "$LANGFUSE_OIDC_CLIENT_SECRET" ]; then LANGFUSE_OIDC_CLIENT_SECRET=$(pwgen -c -n 64 1) updateConfigVar LANGFUSE_OIDC_CLIENT_SECRET $LANGFUSE_OIDC_CLIENT_SECRET fi if [ -z "$LANGFUSE_SALT" ]; then LANGFUSE_SALT=$(openssl rand -base64 32) updateConfigVar LANGFUSE_SALT $LANGFUSE_SALT fi if [ -z "$LANGFUSE_ENCRYPTION_KEY" ]; then LANGFUSE_ENCRYPTION_KEY=$(openssl rand -hex 32) updateConfigVar LANGFUSE_ENCRYPTION_KEY $LANGFUSE_ENCRYPTION_KEY fi if [ -z "$LANGFUSE_NEXTAUTH_SECRET" ]; then LANGFUSE_NEXTAUTH_SECRET=$(openssl rand -base64 32) updateConfigVar LANGFUSE_NEXTAUTH_SECRET $LANGFUSE_NEXTAUTH_SECRET fi if [ -z "$LANGFUSE_CLICKHOUSE_DB" ]; then LANGFUSE_CLICKHOUSE_DB=default updateConfigVar LANGFUSE_CLICKHOUSE_DB $LANGFUSE_CLICKHOUSE_DB fi if [ -z "$LANGFUSE_CLICKHOUSE_USER" ]; then LANGFUSE_CLICKHOUSE_USER=default updateConfigVar LANGFUSE_CLICKHOUSE_USER $LANGFUSE_CLICKHOUSE_USER fi if [ -z "$LANGFUSE_CLICKHOUSE_PASSWORD" ]; then LANGFUSE_CLICKHOUSE_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LANGFUSE_CLICKHOUSE_PASSWORD $LANGFUSE_CLICKHOUSE_PASSWORD fi if [ -z "$SKYVERN_ADMIN_USERNAME" ]; then SKYVERN_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_skyvern" updateConfigVar SKYVERN_ADMIN_USERNAME $SKYVERN_ADMIN_USERNAME fi if [ -z "$SKYVERN_ADMIN_EMAIL_ADDRESS" ]; then SKYVERN_ADMIN_EMAIL_ADDRESS=$SKYVERN_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar SKYVERN_ADMIN_EMAIL_ADDRESS $SKYVERN_ADMIN_EMAIL_ADDRESS fi if [ -z "$SKYVERN_ADMIN_PASSWORD" ]; then SKYVERN_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SKYVERN_ADMIN_PASSWORD $SKYVERN_ADMIN_PASSWORD fi if [ -z "$SKYVERN_DATABASE_NAME" ]; then SKYVERN_DATABASE_NAME=skyverndb updateConfigVar SKYVERN_DATABASE_NAME $SKYVERN_DATABASE_NAME fi if [ -z "$SKYVERN_DATABASE_USER" ]; then SKYVERN_DATABASE_USER=skyvern-user updateConfigVar SKYVERN_DATABASE_USER $SKYVERN_DATABASE_USER fi if [ -z "$SKYVERN_DATABASE_USER_PASSWORD" ]; then SKYVERN_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SKYVERN_DATABASE_USER_PASSWORD $SKYVERN_DATABASE_USER_PASSWORD fi if [ -z "$SKYVERN_DATABASE_READONLYUSER" ]; then SKYVERN_DATABASE_READONLYUSER=skyvern-readonly updateConfigVar SKYVERN_DATABASE_READONLYUSER $SKYVERN_DATABASE_READONLYUSER fi if [ -z "$SKYVERN_DATABASE_READONLYUSER_PASSWORD" ]; then SKYVERN_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SKYVERN_DATABASE_READONLYUSER_PASSWORD $SKYVERN_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$SKYVERN_API_KEY" ]; then SKYVERN_API_KEY=$(pwgen -c -n 32 1) updateConfigVar SKYVERN_API_KEY $SKYVERN_API_KEY fi if [ -z "$WGER_ADMIN_USERNAME" ]; then WGER_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_wger" updateConfigVar WGER_ADMIN_USERNAME $WGER_ADMIN_USERNAME fi if [ -z "$WGER_ADMIN_EMAIL_ADDRESS" ]; then WGER_ADMIN_EMAIL_ADDRESS=$WGER_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar WGER_ADMIN_EMAIL_ADDRESS $WGER_ADMIN_EMAIL_ADDRESS fi if [ -z "$WGER_ADMIN_PASSWORD" ]; then WGER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WGER_ADMIN_PASSWORD $WGER_ADMIN_PASSWORD fi if [ -z "$WGER_DATABASE_NAME" ]; then WGER_DATABASE_NAME=wgerdb updateConfigVar WGER_DATABASE_NAME $WGER_DATABASE_NAME fi if [ -z "$WGER_DATABASE_USER" ]; then WGER_DATABASE_USER=wger-user updateConfigVar WGER_DATABASE_USER $WGER_DATABASE_USER fi if [ -z "$WGER_DATABASE_USER_PASSWORD" ]; then WGER_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WGER_DATABASE_USER_PASSWORD $WGER_DATABASE_USER_PASSWORD fi if [ -z "$WGER_DATABASE_READONLYUSER" ]; then WGER_DATABASE_READONLYUSER=wger-readonly updateConfigVar WGER_DATABASE_READONLYUSER $WGER_DATABASE_READONLYUSER fi if [ -z "$WGER_DATABASE_READONLYUSER_PASSWORD" ]; then WGER_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WGER_DATABASE_READONLYUSER_PASSWORD $WGER_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$WGER_REDIS_PASSWORD" ]; then WGER_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WGER_REDIS_PASSWORD $WGER_REDIS_PASSWORD fi if [ -z "$WGER_SECRET_KEY" ]; then WGER_SECRET_KEY=$(pwgen -c -n 50 1) updateConfigVar WGER_SECRET_KEY $WGER_SECRET_KEY fi if [ -z "$WGER_SIGNING_KEY" ]; then WGER_SIGNING_KEY=$(pwgen -c -n 50 1) updateConfigVar WGER_SIGNING_KEY $WGER_SIGNING_KEY fi if [ -z "$WGER_CELERY_FLOWER_PASSWORD" ]; then WGER_CELERY_FLOWER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WGER_CELERY_FLOWER_PASSWORD $WGER_CELERY_FLOWER_PASSWORD fi if [ -z "$WORKOUTCOOL_ADMIN_USERNAME" ]; then WORKOUTCOOL_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_workoutcool" updateConfigVar WORKOUTCOOL_ADMIN_USERNAME $WORKOUTCOOL_ADMIN_USERNAME fi if [ -z "$WORKOUTCOOL_ADMIN_EMAIL_ADDRESS" ]; then WORKOUTCOOL_ADMIN_EMAIL_ADDRESS=$WORKOUTCOOL_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar WORKOUTCOOL_ADMIN_EMAIL_ADDRESS $WORKOUTCOOL_ADMIN_EMAIL_ADDRESS fi if [ -z "$WORKOUTCOOL_ADMIN_PASSWORD" ]; then WORKOUTCOOL_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WORKOUTCOOL_ADMIN_PASSWORD $WORKOUTCOOL_ADMIN_PASSWORD fi if [ -z "$WORKOUTCOOL_DATABASE_NAME" ]; then WORKOUTCOOL_DATABASE_NAME=workoutcooldb updateConfigVar WORKOUTCOOL_DATABASE_NAME $WORKOUTCOOL_DATABASE_NAME fi if [ -z "$WORKOUTCOOL_DATABASE_USER" ]; then WORKOUTCOOL_DATABASE_USER=workoutcool-user updateConfigVar WORKOUTCOOL_DATABASE_USER $WORKOUTCOOL_DATABASE_USER fi if [ -z "$WORKOUTCOOL_DATABASE_USER_PASSWORD" ]; then WORKOUTCOOL_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WORKOUTCOOL_DATABASE_USER_PASSWORD $WORKOUTCOOL_DATABASE_USER_PASSWORD fi if [ -z "$WORKOUTCOOL_DATABASE_READONLYUSER" ]; then WORKOUTCOOL_DATABASE_READONLYUSER=workoutcool-readonly updateConfigVar WORKOUTCOOL_DATABASE_READONLYUSER $WORKOUTCOOL_DATABASE_READONLYUSER fi if [ -z "$WORKOUTCOOL_DATABASE_READONLYUSER_PASSWORD" ]; then WORKOUTCOOL_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WORKOUTCOOL_DATABASE_READONLYUSER_PASSWORD $WORKOUTCOOL_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$WORKOUTCOOL_BETTER_AUTH_SECRET" ]; then WORKOUTCOOL_BETTER_AUTH_SECRET=$(openssl rand -base64 32) updateConfigVar WORKOUTCOOL_BETTER_AUTH_SECRET $WORKOUTCOOL_BETTER_AUTH_SECRET fi if [ -z "$OPENRAG_ADMIN_USERNAME" ]; then OPENRAG_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_openrag" updateConfigVar OPENRAG_ADMIN_USERNAME $OPENRAG_ADMIN_USERNAME fi if [ -z "$OPENRAG_ADMIN_EMAIL_ADDRESS" ]; then OPENRAG_ADMIN_EMAIL_ADDRESS=$OPENRAG_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar OPENRAG_ADMIN_EMAIL_ADDRESS $OPENRAG_ADMIN_EMAIL_ADDRESS fi if [ -z "$OPENRAG_ADMIN_PASSWORD" ]; then OPENRAG_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENRAG_ADMIN_PASSWORD $OPENRAG_ADMIN_PASSWORD fi if [ -z "$OPENRAG_OPENSEARCH_USERNAME" ]; then OPENRAG_OPENSEARCH_USERNAME="admin" updateConfigVar OPENRAG_OPENSEARCH_USERNAME $OPENRAG_OPENSEARCH_USERNAME fi if [ -z "$OPENRAG_OPENSEARCH_PASSWORD" ]; then OPENRAG_OPENSEARCH_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar OPENRAG_OPENSEARCH_PASSWORD $OPENRAG_OPENSEARCH_PASSWORD fi if [ -z "$OPENRAG_LANGFLOW_ADMIN_USERNAME" ]; then OPENRAG_LANGFLOW_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_openrag_langflow" updateConfigVar OPENRAG_LANGFLOW_ADMIN_USERNAME $OPENRAG_LANGFLOW_ADMIN_USERNAME fi if [ -z "$OPENRAG_LANGFLOW_ADMIN_PASSWORD" ]; then OPENRAG_LANGFLOW_ADMIN_PASSWORD=$(getPasswordWithSymbol 32) updateConfigVar OPENRAG_LANGFLOW_ADMIN_PASSWORD $OPENRAG_LANGFLOW_ADMIN_PASSWORD fi if [ -z "$OPENRAG_LANGFLOW_API_KEY" ]; then OPENRAG_LANGFLOW_API_KEY=sk_$(openssl rand -hex 32) updateConfigVar OPENRAG_LANGFLOW_API_KEY $OPENRAG_LANGFLOW_API_KEY fi if [ -z "$OPENRAG_LANGFLOW_SECRET_KEY" ]; then OPENRAG_LANGFLOW_SECRET_KEY=$(openssl rand -base64 32) updateConfigVar OPENRAG_LANGFLOW_SECRET_KEY $OPENRAG_LANGFLOW_SECRET_KEY fi if [ -z "$OPENRAG_LANGFUSE_SECRET_KEY" ]; then OPENRAG_LANGFUSE_SECRET_KEY=pk-lf-$(uuidgen) updateConfigVar OPENRAG_LANGFUSE_SECRET_KEY $OPENRAG_LANGFUSE_SECRET_KEY fi if [ -z "$OPENRAG_LANGFUSE_PUBLIC_KEY" ]; then OPENRAG_LANGFUSE_PUBLIC_KEY=sk-lf-$(uuidgen) updateConfigVar OPENRAG_LANGFUSE_PUBLIC_KEY $OPENRAG_LANGFUSE_PUBLIC_KEY fi if [ -z "$VOICEBOX_ADMIN_USERNAME" ]; then VOICEBOX_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_voicebox" updateConfigVar VOICEBOX_ADMIN_USERNAME $VOICEBOX_ADMIN_USERNAME fi if [ -z "$VOICEBOX_ADMIN_EMAIL_ADDRESS" ]; then VOICEBOX_ADMIN_EMAIL_ADDRESS=$VOICEBOX_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar VOICEBOX_ADMIN_EMAIL_ADDRESS $VOICEBOX_ADMIN_EMAIL_ADDRESS fi if [ -z "$VOICEBOX_ADMIN_PASSWORD" ]; then VOICEBOX_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar VOICEBOX_ADMIN_PASSWORD $VOICEBOX_ADMIN_PASSWORD fi if [ -z "$OPENCODE_ADMIN_USERNAME" ]; then OPENCODE_ADMIN_USERNAME="opencode" updateConfigVar OPENCODE_ADMIN_USERNAME $OPENCODE_ADMIN_USERNAME fi if [ -z "$OPENCODE_ADMIN_PASSWORD" ]; then OPENCODE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENCODE_ADMIN_PASSWORD $OPENCODE_ADMIN_PASSWORD fi if [ -z "$OPENCODE_CODESERVER_USERNAME" ]; then OPENCODE_CODESERVER_USERNAME=$ADMIN_USERNAME_BASE"_opencode_codeserver" updateConfigVar OPENCODE_CODESERVER_USERNAME $OPENCODE_CODESERVER_USERNAME fi if [ -z "$OPENCODE_CODESERVER_PASSWORD" ]; then OPENCODE_CODESERVER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar OPENCODE_CODESERVER_PASSWORD $OPENCODE_CODESERVER_PASSWORD fi if [ -z "$OPENSKILLS_API_KEY" ]; then OPENSKILLS_API_KEY=$(pwgen -c -n 32 1) updateConfigVar OPENSKILLS_API_KEY $OPENSKILLS_API_KEY fi if [ -z "$EMAILCLASSIFIERAI_ADMIN_USERNAME" ]; then EMAILCLASSIFIERAI_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_emailclassifierai" updateConfigVar EMAILCLASSIFIERAI_ADMIN_USERNAME $EMAILCLASSIFIERAI_ADMIN_USERNAME fi if [ -z "$EMAILCLASSIFIERAI_ADMIN_PASSWORD" ]; then EMAILCLASSIFIERAI_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar EMAILCLASSIFIERAI_ADMIN_PASSWORD $EMAILCLASSIFIERAI_ADMIN_PASSWORD fi if [ -z "$EMAILCLASSIFIERAI_REDIS_PASSWORD" ]; then EMAILCLASSIFIERAI_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar EMAILCLASSIFIERAI_REDIS_PASSWORD $EMAILCLASSIFIERAI_REDIS_PASSWORD fi if [ -z "$EMAILCLASSIFIERAI_FLASK_SECRET" ]; then EMAILCLASSIFIERAI_FLASK_SECRET=$(openssl rand -hex 32) updateConfigVar EMAILCLASSIFIERAI_FLASK_SECRET $EMAILCLASSIFIERAI_FLASK_SECRET fi if [ -z "$HERMES_AGENT_ADMIN_USERNAME" ]; then HERMES_AGENT_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_hermes-agent" updateConfigVar HERMES_AGENT_ADMIN_USERNAME $HERMES_AGENT_ADMIN_USERNAME fi if [ -z "$HERMES_AGENT_ADMIN_PASSWORD" ]; then HERMES_AGENT_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HERMES_AGENT_ADMIN_PASSWORD $HERMES_AGENT_ADMIN_PASSWORD fi if [ -z "$HERMES_AGENT_SUDO_PASSWORD" ]; then HERMES_AGENT_SUDO_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HERMES_AGENT_SUDO_PASSWORD $HERMES_AGENT_SUDO_PASSWORD fi if [ -z "$HERMES_AGENT_API_KEY" ]; then HERMES_AGENT_API_KEY=$(pwgen -c -n 32 1) updateConfigVar HERMES_AGENT_API_KEY $HERMES_AGENT_API_KEY fi if [ -z "$HERMES_AGENT_WEBUI_PASSWORD" ]; then HERMES_AGENT_WEBUI_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HERMES_AGENT_WEBUI_PASSWORD $HERMES_AGENT_WEBUI_PASSWORD fi if [ -z "$AUTOKB_ADMIN_USERNAME" ]; then AUTOKB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_autokb" updateConfigVar AUTOKB_ADMIN_USERNAME $AUTOKB_ADMIN_USERNAME fi if [ -z "$AUTOKB_ADMIN_PASSWORD" ]; then AUTOKB_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_ADMIN_PASSWORD $AUTOKB_ADMIN_PASSWORD fi if [ -z "$AUTOKB_DATABASE_NAME" ]; then AUTOKB_DATABASE_NAME=autokbdb updateConfigVar AUTOKB_DATABASE_NAME $AUTOKB_DATABASE_NAME fi if [ -z "$AUTOKB_DATABASE_USER" ]; then AUTOKB_DATABASE_USER=autokb-user updateConfigVar AUTOKB_DATABASE_USER $AUTOKB_DATABASE_USER fi if [ -z "$AUTOKB_DATABASE_USER_PASSWORD" ]; then AUTOKB_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_DATABASE_USER_PASSWORD $AUTOKB_DATABASE_USER_PASSWORD fi if [ -z "$AUTOKB_DATABASE_READONLYUSER" ]; then AUTOKB_DATABASE_READONLYUSER=autokb-readonly updateConfigVar AUTOKB_DATABASE_READONLYUSER $AUTOKB_DATABASE_READONLYUSER fi if [ -z "$AUTOKB_DATABASE_READONLYUSER_PASSWORD" ]; then AUTOKB_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_DATABASE_READONLYUSER_PASSWORD $AUTOKB_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$AUTOKB_REDIS_PASSWORD" ]; then AUTOKB_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_REDIS_PASSWORD $AUTOKB_REDIS_PASSWORD fi if [ -z "$AUTOKB_API_KEY" ]; then AUTOKB_API_KEY=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_API_KEY $AUTOKB_API_KEY fi if [ -z "$AUTOKB_BACKEND_API_KEY" ]; then AUTOKB_BACKEND_API_KEY=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_BACKEND_API_KEY $AUTOKB_BACKEND_API_KEY fi if [ -z "$AUTOKB_WEBHOOK_API_KEY" ]; then AUTOKB_WEBHOOK_API_KEY=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_WEBHOOK_API_KEY $AUTOKB_WEBHOOK_API_KEY fi if [ -z "$AUTOKB_ENCRYPTION_KEY" ]; then AUTOKB_ENCRYPTION_KEY=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_ENCRYPTION_KEY $AUTOKB_ENCRYPTION_KEY fi if [ -z "$AUTOKB_ENCRYPTION_SALT" ]; then AUTOKB_ENCRYPTION_SALT=$(pwgen -c -n 32 1) updateConfigVar AUTOKB_ENCRYPTION_SALT $AUTOKB_ENCRYPTION_SALT fi if [ -z "$SUITECRM_ADMIN_USERNAME" ]; then SUITECRM_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_suitecrm" updateConfigVar SUITECRM_ADMIN_USERNAME $SUITECRM_ADMIN_USERNAME fi if [ -z "$SUITECRM_ADMIN_EMAIL_ADDRESS" ]; then SUITECRM_ADMIN_EMAIL_ADDRESS=$SUITECRM_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar SUITECRM_ADMIN_EMAIL_ADDRESS $SUITECRM_ADMIN_EMAIL_ADDRESS fi if [ -z "$SUITECRM_ADMIN_PASSWORD" ]; then SUITECRM_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUITECRM_ADMIN_PASSWORD $SUITECRM_ADMIN_PASSWORD fi if [ -z "$SUITECRM_DATABASE_NAME" ]; then SUITECRM_DATABASE_NAME=suitecrmdb updateConfigVar SUITECRM_DATABASE_NAME $SUITECRM_DATABASE_NAME fi if [ -z "$SUITECRM_DATABASE_ROOT_PASSWORD" ]; then SUITECRM_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUITECRM_DATABASE_ROOT_PASSWORD $SUITECRM_DATABASE_ROOT_PASSWORD fi if [ -z "$SUITECRM_DATABASE_USER" ]; then SUITECRM_DATABASE_USER=suitecrm-user updateConfigVar SUITECRM_DATABASE_USER $SUITECRM_DATABASE_USER fi if [ -z "$SUITECRM_DATABASE_USER_PASSWORD" ]; then SUITECRM_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUITECRM_DATABASE_USER_PASSWORD $SUITECRM_DATABASE_USER_PASSWORD fi if [ -z "$SUITECRM_DATABASE_READONLYUSER" ]; then SUITECRM_DATABASE_READONLYUSER=suitecrm-readonly updateConfigVar SUITECRM_DATABASE_READONLYUSER $SUITECRM_DATABASE_READONLYUSER fi if [ -z "$SUITECRM_DATABASE_READONLYUSER_PASSWORD" ]; then SUITECRM_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUITECRM_DATABASE_READONLYUSER_PASSWORD $SUITECRM_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$SUITECRM_REDIS_PASSWORD" ]; then SUITECRM_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SUITECRM_REDIS_PASSWORD $SUITECRM_REDIS_PASSWORD fi if [ -z "$SUITECRM_APP_SECRET" ]; then SUITECRM_APP_SECRET=$(pwgen -c -n 32 1) updateConfigVar SUITECRM_APP_SECRET $SUITECRM_APP_SECRET fi if [ -z "$HEDGEDOC_ADMIN_USERNAME" ]; then HEDGEDOC_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_hedgedoc" updateConfigVar HEDGEDOC_ADMIN_USERNAME $HEDGEDOC_ADMIN_USERNAME fi if [ -z "$HEDGEDOC_ADMIN_EMAIL_ADDRESS" ]; then HEDGEDOC_ADMIN_EMAIL_ADDRESS=$HEDGEDOC_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar HEDGEDOC_ADMIN_EMAIL_ADDRESS $HEDGEDOC_ADMIN_EMAIL_ADDRESS fi if [ -z "$HEDGEDOC_ADMIN_PASSWORD" ]; then HEDGEDOC_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HEDGEDOC_ADMIN_PASSWORD $HEDGEDOC_ADMIN_PASSWORD fi if [ -z "$HEDGEDOC_DATABASE_NAME" ]; then HEDGEDOC_DATABASE_NAME=hedgedocdb updateConfigVar HEDGEDOC_DATABASE_NAME $HEDGEDOC_DATABASE_NAME fi if [ -z "$HEDGEDOC_DATABASE_USER" ]; then HEDGEDOC_DATABASE_USER=hedgedoc-user updateConfigVar HEDGEDOC_DATABASE_USER $HEDGEDOC_DATABASE_USER fi if [ -z "$HEDGEDOC_DATABASE_USER_PASSWORD" ]; then HEDGEDOC_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HEDGEDOC_DATABASE_USER_PASSWORD $HEDGEDOC_DATABASE_USER_PASSWORD fi if [ -z "$HEDGEDOC_DATABASE_READONLYUSER" ]; then HEDGEDOC_DATABASE_READONLYUSER=hedgedoc-readonly updateConfigVar HEDGEDOC_DATABASE_READONLYUSER $HEDGEDOC_DATABASE_READONLYUSER fi if [ -z "$HEDGEDOC_DATABASE_READONLYUSER_PASSWORD" ]; then HEDGEDOC_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar HEDGEDOC_DATABASE_READONLYUSER_PASSWORD $HEDGEDOC_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$HEDGEDOC_SESSION_SECRET" ]; then HEDGEDOC_SESSION_SECRET=$(openssl rand -hex 32) updateConfigVar HEDGEDOC_SESSION_SECRET $HEDGEDOC_SESSION_SECRET fi if [ -z "$PRESENTON_ADMIN_USERNAME" ]; then PRESENTON_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_presenton" updateConfigVar PRESENTON_ADMIN_USERNAME $PRESENTON_ADMIN_USERNAME fi if [ -z "$PRESENTON_ADMIN_EMAIL_ADDRESS" ]; then PRESENTON_ADMIN_EMAIL_ADDRESS=$PRESENTON_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar PRESENTON_ADMIN_EMAIL_ADDRESS $PRESENTON_ADMIN_EMAIL_ADDRESS fi if [ -z "$PRESENTON_ADMIN_PASSWORD" ]; then PRESENTON_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PRESENTON_ADMIN_PASSWORD $PRESENTON_ADMIN_PASSWORD fi if [ -z "$BASICMEMORY_ADMIN_USERNAME" ]; then BASICMEMORY_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_basicmemory" updateConfigVar BASICMEMORY_ADMIN_USERNAME $BASICMEMORY_ADMIN_USERNAME fi if [ -z "$BASICMEMORY_ADMIN_EMAIL_ADDRESS" ]; then BASICMEMORY_ADMIN_EMAIL_ADDRESS=$BASICMEMORY_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar BASICMEMORY_ADMIN_EMAIL_ADDRESS $BASICMEMORY_ADMIN_EMAIL_ADDRESS fi if [ -z "$BASICMEMORY_ADMIN_PASSWORD" ]; then BASICMEMORY_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BASICMEMORY_ADMIN_PASSWORD $BASICMEMORY_ADMIN_PASSWORD fi if [ -z "$BASICMEMORY_DATABASE_NAME" ]; then BASICMEMORY_DATABASE_NAME=basicmemorydb updateConfigVar BASICMEMORY_DATABASE_NAME $BASICMEMORY_DATABASE_NAME fi if [ -z "$BASICMEMORY_DATABASE_USER" ]; then BASICMEMORY_DATABASE_USER=basicmemory-user updateConfigVar BASICMEMORY_DATABASE_USER $BASICMEMORY_DATABASE_USER fi if [ -z "$BASICMEMORY_DATABASE_USER_PASSWORD" ]; then BASICMEMORY_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BASICMEMORY_DATABASE_USER_PASSWORD $BASICMEMORY_DATABASE_USER_PASSWORD fi if [ -z "$BASICMEMORY_DATABASE_READONLYUSER" ]; then BASICMEMORY_DATABASE_READONLYUSER=basicmemory-readonly updateConfigVar BASICMEMORY_DATABASE_READONLYUSER $BASICMEMORY_DATABASE_READONLYUSER fi if [ -z "$BASICMEMORY_DATABASE_READONLYUSER_PASSWORD" ]; then BASICMEMORY_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar BASICMEMORY_DATABASE_READONLYUSER_PASSWORD $BASICMEMORY_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$COGNEE_ADMIN_USERNAME" ]; then COGNEE_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_cognee" updateConfigVar COGNEE_ADMIN_USERNAME $COGNEE_ADMIN_USERNAME fi if [ -z "$COGNEE_ADMIN_EMAIL_ADDRESS" ]; then COGNEE_ADMIN_EMAIL_ADDRESS=$COGNEE_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar COGNEE_ADMIN_EMAIL_ADDRESS $COGNEE_ADMIN_EMAIL_ADDRESS fi if [ -z "$COGNEE_ADMIN_PASSWORD" ]; then COGNEE_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar COGNEE_ADMIN_PASSWORD $COGNEE_ADMIN_PASSWORD fi if [ -z "$COGNEE_DATABASE_NAME" ]; then COGNEE_DATABASE_NAME=cogneedb updateConfigVar COGNEE_DATABASE_NAME $COGNEE_DATABASE_NAME fi if [ -z "$COGNEE_DATABASE_USER" ]; then COGNEE_DATABASE_USER=cognee-user updateConfigVar COGNEE_DATABASE_USER $COGNEE_DATABASE_USER fi if [ -z "$COGNEE_DATABASE_USER_PASSWORD" ]; then COGNEE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar COGNEE_DATABASE_USER_PASSWORD $COGNEE_DATABASE_USER_PASSWORD fi if [ -z "$COGNEE_DATABASE_READONLYUSER" ]; then COGNEE_DATABASE_READONLYUSER=cognee-readonly updateConfigVar COGNEE_DATABASE_READONLYUSER $COGNEE_DATABASE_READONLYUSER fi if [ -z "$COGNEE_DATABASE_READONLYUSER_PASSWORD" ]; then COGNEE_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar COGNEE_DATABASE_READONLYUSER_PASSWORD $COGNEE_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$COGNEE_REDIS_PASSWORD" ]; then COGNEE_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar COGNEE_REDIS_PASSWORD $COGNEE_REDIS_PASSWORD fi if [ -z "$COGNEE_JWT_SECRET" ]; then COGNEE_JWT_SECRET=$(pwgen -c -n 32 1) updateConfigVar COGNEE_JWT_SECRET $COGNEE_JWT_SECRET fi if [ -z "$COGNEE_VERIFICATION_TOKEN_SECRET" ]; then COGNEE_VERIFICATION_TOKEN_SECRET=$(pwgen -c -n 32 1) updateConfigVar COGNEE_VERIFICATION_TOKEN_SECRET $COGNEE_VERIFICATION_TOKEN_SECRET fi if [ -z "$COGNEE_RESET_PASSWORD_TOKEN_SECRET" ]; then COGNEE_RESET_PASSWORD_TOKEN_SECRET=$(pwgen -c -n 32 1) updateConfigVar COGNEE_RESET_PASSWORD_TOKEN_SECRET $COGNEE_RESET_PASSWORD_TOKEN_SECRET fi if [ -z "$LIGHTRAG_ADMIN_USERNAME" ]; then LIGHTRAG_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_lightrag" updateConfigVar LIGHTRAG_ADMIN_USERNAME $LIGHTRAG_ADMIN_USERNAME fi if [ -z "$LIGHTRAG_ADMIN_EMAIL_ADDRESS" ]; then LIGHTRAG_ADMIN_EMAIL_ADDRESS=$LIGHTRAG_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar LIGHTRAG_ADMIN_EMAIL_ADDRESS $LIGHTRAG_ADMIN_EMAIL_ADDRESS fi if [ -z "$LIGHTRAG_ADMIN_PASSWORD" ]; then LIGHTRAG_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIGHTRAG_ADMIN_PASSWORD $LIGHTRAG_ADMIN_PASSWORD fi if [ -z "$LIGHTRAG_DATABASE_NAME" ]; then LIGHTRAG_DATABASE_NAME=lightragdb updateConfigVar LIGHTRAG_DATABASE_NAME $LIGHTRAG_DATABASE_NAME fi if [ -z "$LIGHTRAG_DATABASE_USER" ]; then LIGHTRAG_DATABASE_USER=lightrag-user updateConfigVar LIGHTRAG_DATABASE_USER $LIGHTRAG_DATABASE_USER fi if [ -z "$LIGHTRAG_DATABASE_USER_PASSWORD" ]; then LIGHTRAG_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIGHTRAG_DATABASE_USER_PASSWORD $LIGHTRAG_DATABASE_USER_PASSWORD fi if [ -z "$LIGHTRAG_DATABASE_READONLYUSER" ]; then LIGHTRAG_DATABASE_READONLYUSER=lightrag-readonly updateConfigVar LIGHTRAG_DATABASE_READONLYUSER $LIGHTRAG_DATABASE_READONLYUSER fi if [ -z "$LIGHTRAG_DATABASE_READONLYUSER_PASSWORD" ]; then LIGHTRAG_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIGHTRAG_DATABASE_READONLYUSER_PASSWORD $LIGHTRAG_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$LIGHTRAG_TOKEN_SECRET" ]; then LIGHTRAG_TOKEN_SECRET=$(pwgen -c -n 32 1) updateConfigVar LIGHTRAG_TOKEN_SECRET $LIGHTRAG_TOKEN_SECRET fi if [ -z "$LIGHTRAG_API_KEY" ]; then LIGHTRAG_API_KEY=$(pwgen -c -n 32 1) updateConfigVar LIGHTRAG_API_KEY $LIGHTRAG_API_KEY fi if [ -z "$LIGHTRAG_QDRANT_API_KEY" ]; then LIGHTRAG_QDRANT_API_KEY=$(pwgen -c -n 32 1) updateConfigVar LIGHTRAG_QDRANT_API_KEY $LIGHTRAG_QDRANT_API_KEY fi if [ -z "$LIGHTRAG_NEO4J_DATABASE" ]; then LIGHTRAG_NEO4J_DATABASE=neo4j updateConfigVar LIGHTRAG_NEO4J_DATABASE $LIGHTRAG_NEO4J_DATABASE fi if [ -z "$LIGHTRAG_NEO4J_USER" ]; then LIGHTRAG_NEO4J_USER=neo4j updateConfigVar LIGHTRAG_NEO4J_USER $LIGHTRAG_NEO4J_USER fi if [ -z "$LIGHTRAG_NEO4J_PASSWORD" ]; then LIGHTRAG_NEO4J_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LIGHTRAG_NEO4J_PASSWORD $LIGHTRAG_NEO4J_PASSWORD fi if [ -z "$SURICATA_ADMIN_USERNAME" ]; then SURICATA_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_suricata" updateConfigVar SURICATA_ADMIN_USERNAME $SURICATA_ADMIN_USERNAME fi if [ -z "$SURICATA_ADMIN_PASSWORD" ]; then SURICATA_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SURICATA_ADMIN_PASSWORD $SURICATA_ADMIN_PASSWORD fi if [ -z "$CROWDSEC_ADMIN_USERNAME" ]; then CROWDSEC_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_crowdsec" updateConfigVar CROWDSEC_ADMIN_USERNAME $CROWDSEC_ADMIN_USERNAME fi if [ -z "$CROWDSEC_ADMIN_PASSWORD" ]; then CROWDSEC_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar CROWDSEC_ADMIN_PASSWORD $CROWDSEC_ADMIN_PASSWORD fi if [ -z "$CROWDSEC_LAPI_USERNAME" ]; then CROWDSEC_LAPI_USERNAME=$ADMIN_USERNAME_BASE"_crowdsec_lapi" updateConfigVar CROWDSEC_LAPI_USERNAME $CROWDSEC_LAPI_USERNAME fi if [ -z "$CROWDSEC_LAPI_PASSWORD" ]; then CROWDSEC_LAPI_PASSWORD=$(openssl rand -hex 16) updateConfigVar CROWDSEC_LAPI_PASSWORD $CROWDSEC_LAPI_PASSWORD fi if [ -z "$CROWDSEC_API_KEY" ]; then CROWDSEC_API_KEY=$(openssl rand -hex 16) updateConfigVar CROWDSEC_API_KEY $CROWDSEC_API_KEY fi if [ -z "$FASTCRW_API_KEY" ]; then FASTCRW_API_KEY=$(pwgen -c -n 32 1) updateConfigVar FASTCRW_API_KEY $FASTCRW_API_KEY fi if [ -z "$WHITEBOARDRT_REDIS_PASSWORD" ]; then WHITEBOARDRT_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WHITEBOARDRT_REDIS_PASSWORD $WHITEBOARDRT_REDIS_PASSWORD fi if [ -z "$MDCOLLAB_ADMIN_USERNAME" ]; then MDCOLLAB_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_mdcollab" updateConfigVar MDCOLLAB_ADMIN_USERNAME $MDCOLLAB_ADMIN_USERNAME fi if [ -z "$MDCOLLAB_ADMIN_EMAIL_ADDRESS" ]; then MDCOLLAB_ADMIN_EMAIL_ADDRESS=$MDCOLLAB_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar MDCOLLAB_ADMIN_EMAIL_ADDRESS $MDCOLLAB_ADMIN_EMAIL_ADDRESS fi if [ -z "$MDCOLLAB_ADMIN_PASSWORD" ]; then MDCOLLAB_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MDCOLLAB_ADMIN_PASSWORD $MDCOLLAB_ADMIN_PASSWORD fi if [ -z "$MDCOLLAB_DATABASE_NAME" ]; then MDCOLLAB_DATABASE_NAME=mdcollabdb updateConfigVar MDCOLLAB_DATABASE_NAME $MDCOLLAB_DATABASE_NAME fi if [ -z "$MDCOLLAB_DATABASE_USER" ]; then MDCOLLAB_DATABASE_USER=mdcollab-user updateConfigVar MDCOLLAB_DATABASE_USER $MDCOLLAB_DATABASE_USER fi if [ -z "$MDCOLLAB_DATABASE_USER_PASSWORD" ]; then MDCOLLAB_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MDCOLLAB_DATABASE_USER_PASSWORD $MDCOLLAB_DATABASE_USER_PASSWORD fi if [ -z "$MDCOLLAB_DATABASE_READONLYUSER" ]; then MDCOLLAB_DATABASE_READONLYUSER=mdcollab-readonly updateConfigVar MDCOLLAB_DATABASE_READONLYUSER $MDCOLLAB_DATABASE_READONLYUSER fi if [ -z "$MDCOLLAB_DATABASE_READONLYUSER_PASSWORD" ]; then MDCOLLAB_DATABASE_READONLYUSER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MDCOLLAB_DATABASE_READONLYUSER_PASSWORD $MDCOLLAB_DATABASE_READONLYUSER_PASSWORD fi if [ -z "$MDCOLLAB_SESSION_SECRET" ]; then MDCOLLAB_SESSION_SECRET=$(openssl rand -hex 32) updateConfigVar MDCOLLAB_SESSION_SECRET $MDCOLLAB_SESSION_SECRET fi if [ -z "$HALOSPEAK_API_KEY" ]; then HALOSPEAK_API_KEY=$(pwgen -c -n 32 1) updateConfigVar HALOSPEAK_API_KEY $HALOSPEAK_API_KEY fi #ADD_NEW_SVC_CREDENTIALS_HERE # RelayServer credentials if [ -z "$CLIENTDNS_USER1_ADMIN_USERNAME" ]; then CLIENTDNS_USER1_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_clientdns" updateConfigVar CLIENTDNS_USER1_ADMIN_USERNAME $CLIENTDNS_USER1_ADMIN_USERNAME fi if [ -z "$CLIENTDNS_USER1_ADMIN_PASSWORD" ]; then CLIENTDNS_USER1_ADMIN_PASSWORD="$(pwgen -c -n 32 1)" updateConfigVar CLIENTDNS_USER1_ADMIN_PASSWORD $CLIENTDNS_USER1_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_PORTAINER_ADMIN_USERNAME" ]; then RELAYSERVER_PORTAINER_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_portainer" updateConfigVar RELAYSERVER_PORTAINER_ADMIN_USERNAME $RELAYSERVER_PORTAINER_ADMIN_USERNAME fi if [ -z "$RELAYSERVER_PORTAINER_ADMIN_PASSWORD" ]; then RELAYSERVER_PORTAINER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_PORTAINER_ADMIN_PASSWORD $RELAYSERVER_PORTAINER_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_ADGUARD_ADMIN_USERNAME" ]; then RELAYSERVER_ADGUARD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_adguard" updateConfigVar RELAYSERVER_ADGUARD_ADMIN_USERNAME $RELAYSERVER_ADGUARD_ADMIN_USERNAME fi if [ -z "$RELAYSERVER_ADGUARD_ADMIN_PASSWORD" ]; then RELAYSERVER_ADGUARD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_ADGUARD_ADMIN_PASSWORD $RELAYSERVER_ADGUARD_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_CADDYDNS_ADMIN_USERNAME" ]; then RELAYSERVER_CADDYDNS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_caddydns" updateConfigVar RELAYSERVER_CADDYDNS_ADMIN_USERNAME $RELAYSERVER_CADDYDNS_ADMIN_USERNAME fi if [ -z "$RELAYSERVER_CADDYDNS_ADMIN_PASSWORD" ]; then RELAYSERVER_CADDYDNS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_CADDYDNS_ADMIN_PASSWORD $RELAYSERVER_CADDYDNS_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_WGPORTAL_ADMIN_USERNAME" ]; then RELAYSERVER_WGPORTAL_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_wgportal" updateConfigVar RELAYSERVER_WGPORTAL_ADMIN_USERNAME $RELAYSERVER_WGPORTAL_ADMIN_USERNAME RELAYSERVER_WGPORTAL_ADMIN_EMAIL=$RELAYSERVER_WGPORTAL_ADMIN_USERNAME@$HOMESERVER_DOMAIN updateConfigVar RELAYSERVER_WGPORTAL_ADMIN_EMAIL $RELAYSERVER_WGPORTAL_ADMIN_EMAIL fi if [ -z "$RELAYSERVER_WGPORTAL_ADMIN_PASSWORD" ]; then RELAYSERVER_WGPORTAL_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_WGPORTAL_ADMIN_PASSWORD $RELAYSERVER_WGPORTAL_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_CLIENTDNS_ADMIN_USERNAME" ]; then RELAYSERVER_CLIENTDNS_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_clientdns" updateConfigVar RELAYSERVER_CLIENTDNS_ADMIN_USERNAME $RELAYSERVER_CLIENTDNS_ADMIN_USERNAME fi if [ -z "$RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD" ]; then RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD $RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_RSPAMD_ADMIN_USERNAME" ]; then RELAYSERVER_RSPAMD_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_rspamd" updateConfigVar RELAYSERVER_RSPAMD_ADMIN_USERNAME $RELAYSERVER_RSPAMD_ADMIN_USERNAME fi if [ -z "$RELAYSERVER_RSPAMD_ADMIN_PASSWORD" ]; then RELAYSERVER_RSPAMD_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_RSPAMD_ADMIN_PASSWORD $RELAYSERVER_RSPAMD_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_FILEBROWSER_ADMIN_USERNAME" ]; then RELAYSERVER_FILEBROWSER_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_filebrowser" updateConfigVar RELAYSERVER_FILEBROWSER_ADMIN_USERNAME $RELAYSERVER_FILEBROWSER_ADMIN_USERNAME fi if [ -z "$RELAYSERVER_FILEBROWSER_ADMIN_PASSWORD" ]; then RELAYSERVER_FILEBROWSER_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_FILEBROWSER_ADMIN_PASSWORD $RELAYSERVER_FILEBROWSER_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_SYNCTHING_ADMIN_USERNAME" ]; then RELAYSERVER_SYNCTHING_ADMIN_USERNAME=$ADMIN_USERNAME_BASE"_rs_syncthing" updateConfigVar RELAYSERVER_SYNCTHING_ADMIN_USERNAME $RELAYSERVER_SYNCTHING_ADMIN_USERNAME fi if [ -z "$RELAYSERVER_SYNCTHING_ADMIN_PASSWORD" ]; then RELAYSERVER_SYNCTHING_ADMIN_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_SYNCTHING_ADMIN_PASSWORD $RELAYSERVER_SYNCTHING_ADMIN_PASSWORD fi if [ -z "$RELAYSERVER_SYNCTHING_API_KEY" ]; then RELAYSERVER_SYNCTHING_API_KEY=$(pwgen -c -n 32 1) updateConfigVar RELAYSERVER_SYNCTHING_API_KEY $RELAYSERVER_SYNCTHING_API_KEY fi if [ -z "$RELAYSERVER_SYNCTHING_FOLDER_ID" ]; then RELAYSERVER_SYNCTHING_FOLDER_ID=$(pwgen -c -n 5 1)-$(pwgen -c -n 5 1) updateConfigVar RELAYSERVER_SYNCTHING_FOLDER_ID $RELAYSERVER_SYNCTHING_FOLDER_ID fi } function checkCreateNonbackupDirByStack() { nbStackName="$1" case "$nbStackName" in "adguard") mkdir -p $HSHQ_NONBACKUP_DIR/adguard/work ;; "sysutils") mkdir -p $HSHQ_NONBACKUP_DIR/sysutils/prometheus mkdir -p $HSHQ_NONBACKUP_DIR/sysutils/loki ;; "wazuh") mkdir -p $HSHQ_NONBACKUP_DIR/wazuh/volumes mkdir -p $HSHQ_NONBACKUP_DIR/wazuh/volumes/queue mkdir -p $HSHQ_NONBACKUP_DIR/wazuh/volumes/logs mkdir -p $HSHQ_NONBACKUP_DIR/wazuh/volumes/indexer-data ;; "duplicati") mkdir -p $HSHQ_NONBACKUP_DIR/duplicati/restore ;; "mastodon") mkdir -p $HSHQ_NONBACKUP_DIR/mastodon/static ;; "peertube") mkdir -p $HSHQ_NONBACKUP_DIR/peertube/assets ;; "kasm") mkdir -p $HSHQ_NONBACKUP_DIR/kasm/data ;; "netdata") mkdir -p $HSHQ_NONBACKUP_DIR/netdata/cache ;; "stirlingpdf") mkdir -p $HSHQ_NONBACKUP_DIR/stirlingpdf/logs mkdir -p $HSHQ_NONBACKUP_DIR/stirlingpdf/traindata ;; "piped") mkdir -p $HSHQ_NONBACKUP_DIR/piped/proxy ;; "immich") mkdir -p $HSHQ_NONBACKUP_DIR/immich/cache ;; "kopia") mkdir -p $HSHQ_NONBACKUP_DIR/kopia/snapshots ;; "localai") mkdir -p $HSHQ_AIMODELS_DIR/localai ;; "ollama") mkdir -p $HSHQ_AIMODELS_DIR/ollama ;; "comfyui") mkdir -p $HSHQ_AIMODELS_DIR/comfyui mkdir -p $HSHQ_NONBACKUP_DIR/comfyui/cache ;; "khoj") mkdir -p $HSHQ_AIMODELS_DIR/khoj ;; "invokeai") mkdir -p $HSHQ_AIMODELS_DIR/invokeai/models mkdir -p $HSHQ_AIMODELS_DIR/invokeai/cache/{download,convert} ;; "morphic") mkdir -p $HSHQ_NONBACKUP_DIR/morphic/redis ;; "ragflow") mkdir -p $HSHQ_NONBACKUP_DIR/ragflow/kvrocks ;; #ADD_NEW_NONBACKUP_DIRS_HERE *) ;; esac } function installBaseStacks() { initCaddyCommon installPortainer draw_progress_bar 57 installStackByName adguard draw_progress_bar 59 installStackByName openldap draw_progress_bar 61 installStackByName mailu draw_progress_bar 63 installStackByName duplicati draw_progress_bar 65 installStackByName authelia draw_progress_bar 67 installStackByName syncthing draw_progress_bar 69 installStackByName heimdall draw_progress_bar 71 installStackByName ofelia draw_progress_bar 73 installHostInterfaceCaddy $HOMESERVER_HOST_PRIMARY_INTERFACE_NAME $HOMESERVER_HOST_PRIMARY_INTERFACE_IP draw_progress_bar 75 installStackByName script-server draw_progress_bar 77 installStackByName uptimekuma draw_progress_bar 79 } function initServiceVars() { set +e checkAddSvc "SVCD_ACTIVEPIECES_APP=activepieces,activepieces,primary,admin,ActivePieces,activepieces,hshq" checkAddSvc "SVCD_ADGUARD=adguard,adguard,primary,admin,AdguardHome,adguard,hshq" checkAddSvc "SVCD_ADMINER=adminer,adminer,primary,admin,Adminer,adminer,hshq" checkAddSvc "SVCD_AUDIOBOOKSHELF=audiobookshelf,audiobookshelf,other,user,Audiobookshelf,audiobookshelf,le" checkAddSvc "SVCD_AUTHELIA=authelia,authelia,other,user,Authelia,authelia,hshq" checkAddSvc "SVCD_AUTOMATISCH_APP=automatisch,automatisch,primary,admin,Automatisch,automatisch,hshq" checkAddSvc "SVCD_BARASSISTANT=bar-assistant,bar-assistant,primary,user,Bar Assistant,bar-assistant,hshq" checkAddSvc "SVCD_BIND_IP=bind-ip,bind-ip,other,user,BindIP,bind-ip,hshq" checkAddSvc "SVCD_BUDIBASE=budibase,budibase,other,user,Budibase,budibase,hshq" checkAddSvc "SVCD_CADDY=caddy,caddy,primary,admin,Caddy,caddy,hshq" checkAddSvc "SVCD_CADDYDNS=caddy,caddy-dns,primary,admin,CaddyDNS,caddy-dns,hshq" checkAddSvc "SVCD_CALCOM_APP=calcom,calcom,primary,user,Calcom,calcom,hshq" checkAddSvc "SVCD_CALCOM_STUDIO=calcom,calcom-studio,primary,admin,Calcom Studio,calcom-studio,hshq" checkAddSvc "SVCD_CALIBRE_SERVER=calibre,calibre-server,primary,admin,Calibre-Server,calibre-server,hshq" checkAddSvc "SVCD_CALIBRE_WEB=calibre,calibre-web,other,user,Calibre-Web,calibre-web,le" checkAddSvc "SVCD_CHANGEDETECTION=changedetection,changedetection,primary,user,Change Detection,changedetection,hshq" checkAddSvc "SVCD_CLIENTDNS=clientdns,clientdns,primary,admin,ClientDNS,clientdns,hshq" checkAddSvc "SVCD_CLOUDBEAVER=cloudbeaver,cloudbeaver,primary,admin,CloudBeaver,cloudbeaver,hshq" checkAddSvc "SVCD_CODESERVER=codeserver,codeserver,primary,admin,CodeServer,codeserver,hshq" checkAddSvc "SVCD_COLLABORA=collabora,collabora,other,user,Collabora,collabora,hshq" checkAddSvc "SVCD_COTURN=coturn,coturn,other,user,Coturn,coturn,hshq" checkAddSvc "SVCD_DBGATE=dbgate,dbgate,primary,admin,DbGate,dbgate,hshq" checkAddSvc "SVCD_DISCOURSE=discourse,discourse,other,user,Discourse,discourse,hshq" checkAddSvc "SVCD_DOLIBARR_APP=dolibarr,dolibarr,primary,user,Dolibarr,dolibarr,hshq" checkAddSvc "SVCD_DOZZLE=dozzle,dozzle,primary,admin,Dozzle,dozzle,hshq" checkAddSvc "SVCD_DRAWIO_WEB=drawio,drawio,primary,user,Draw.io,drawio,hshq" checkAddSvc "SVCD_DRAWIO_NEXTAI=drawio,drawio-nextai,primary,user,NextAI Draw.io,drawio-nextai,hshq" checkAddSvc "SVCD_DUPLICATI=duplicati,duplicati,home,admin,Duplicati,duplicati,hshq" checkAddSvc "SVCD_EASYAPPOINTMENTS=easyappointments,easyappointments,primary,user,EasyAppointments,easyappointments,hshq" checkAddSvc "SVCD_ESPOCRM=espocrm,espocrm,primary,user,EspoCRM,espocrm,hshq" checkAddSvc "SVCD_EXCALIDRAW_WEB=excalidraw,excalidraw,primary,user,Excalidraw,excalidraw,hshq" checkAddSvc "SVCD_EXCALIDRAW_SERVER=excalidraw,excalidraw-server,primary,user,Excalidraw Server,excalidraw-server,hshq" checkAddSvc "SVCD_EXCALIDRAW_STORAGE=excalidraw,excalidraw-storage,primary,user,Excalidraw Storage,excalidraw-storage,hshq" checkAddSvc "SVCD_EXCALIDRAW_AI=excalidraw,excalidraw-ai,primary,user,Excalidraw AI,excalidraw-ai,hshq" checkAddSvc "SVCD_FILEBROWSER=filebrowser,filebrowser,other,user,FileBrowser,filebrowser,hshq" checkAddSvc "SVCD_FILEDROP=filedrop,filedrop,other,user,FileDrop,filedrop,hshq" checkAddSvc "SVCD_FILES=files,files,other,user,Files,files,hshq" sed -i "s/SVCD_FIREFLY=/SVCD_FIREFLY_APP=/" $CONFIG_FILE checkAddSvc "SVCD_FIREFLY_APP=firefly,fireflyiii,home,admin,Firefly III,fireflyiii,hshq" checkAddSvc "SVCD_FIREFLY_IMPORTER=firefly,fireflyiii-importer,home,admin,Firefly III-Importer,fireflyiii-importer,hshq" checkAddSvc "SVCD_FIREFLY_FINTSIMPORTER=firefly,fireflyiii-fints-importer,home,admin,Firefly III-FinTS-Importer,fireflyiii-fints-importer,hshq" checkAddSvc "SVCD_FRAPPE_HR=frappe-hr,frappe-hr,primary,user,Frappe HR,frappe-hr,hshq" checkAddSvc "SVCD_FRESHRSS=freshrss,freshrss,primary,user,FreshRSS,freshrss,le" checkAddSvc "SVCD_GHOST=ghost,ghost,other,user,Ghost,ghost,hshq" checkAddSvc "SVCD_GITEA=gitea,gitea,primary,user,Gitea,gitea,le" checkAddSvc "SVCD_GITLAB=gitlab,gitlab,primary,user,Gitlab,gitlab,hshq" checkAddSvc "SVCD_GRAFANA=sysutils,grafana,primary,admin,Grafana,grafana,hshq" checkAddSvc "SVCD_ALLOY=sysutils,alloy,primary,admin,Alloy,alloy,hshq" checkAddSvc "SVCD_GRAMPSWEB=grampsweb,grampsweb,primary,user,GrampsWeb,grampsweb,le" checkAddSvc "SVCD_GUACAMOLE=guacamole,guacamole,primary,admin,Guacamole,guacamole,hshq" checkAddSvc "SVCD_HEIMDALL=heimdall,heimdall,other,user,Heimdall,heimdall,hshq" checkAddSvc "SVCD_HOMARR=homarr,homarr,other,user,Homarr,homarr,hshq" checkAddSvc "SVCD_HOMEASSISTANT_APP=homeassistant,homeassistant,primary,admin,HomeAssistant,homeassistant,hshq" checkAddSvc "SVCD_HOMEASSISTANT_CONFIGURATOR=homeassistant,hass-configurator,primary,admin,HomeAssistant-Configurator,hass-configurator,hshq" checkAddSvc "SVCD_HOMEASSISTANT_NODERED=homeassistant,hass-nodered,primary,admin,HomeAssistant-NodeRed,hass-nodered,hshq" checkAddSvc "SVCD_HOMEASSISTANT_TASMOADMIN=homeassistant,hass-tasmoadmin,primary,admin,HomeAssistant-Tasmoadmin,hass-tasmoadmin,hshq" checkAddSvc "SVCD_HSHQHOME=home,home,other,user,Home,home,hshq" checkAddSvc "SVCD_HSHQSTATUS=hshqstatus,hshqstatus,other,user,HSHQStatus,hshqstatus,hshq" checkAddSvc "SVCD_SCRIPTSERVER=script-server,script-server,primary,admin,Script-server,script-server,hshq" checkAddSvc "SVCD_HUGINN=huginn,huginn,primary,user,Huginn,huginn,hshq" checkAddSvc "SVCD_IMAGES=images,images,other,user,Images,images,hshq" sed -i "s/SVCD_IMMICH=/SVCD_IMMICH_APP=/" $CONFIG_FILE checkAddSvc "SVCD_IMMICH_APP=immich,immich,other,user,Immich,immich,le" checkAddSvc "SVCD_INFLUXDB=sysutils,influxdb,primary,admin,InfluxDB,influxdb,hshq" checkAddSvc "SVCD_INVIDIOUS=invidious,invidious,primary,user,Invidious,invidious,hshq" checkAddSvc "SVCD_INVOICENINJA_WEB=invoiceninja,invoiceninja,primary,user,InvoiceNinja,invoiceninja,hshq" checkAddSvc "SVCD_INVOICESHELF_APP=invoiceshelf,invoiceshelf,primary,user,InvoiceShelf,invoiceshelf,hshq" checkAddSvc "SVCD_ITTOOLS=ittools,ittools,primary,admin,IT Tools,ittools,hshq" checkAddSvc "SVCD_JELLYFIN=jellyfin,jellyfin,primary,user,Jellyfin,jellyfin,le" checkAddSvc "SVCD_JITSI=jitsi,jitsi,other,user,Jitsi,jitsi,le" checkAddSvc "SVCD_JUPYTER=jupyter,jupyter,primary,admin,Jupyter,jupyter,hshq" checkAddSvc "SVCD_LINKWARDEN=linkwarden,linkwarden,primary,user,Linkwarden,linkwarden,hshq" checkAddSvc "SVCD_KANBOARD=kanboard,kanboard,primary,user,Kanboard,kanboard,hshq" checkAddSvc "SVCD_KASM=kasm,kasm,primary,user,Kasm,kasm,hshq" checkAddSvc "SVCD_KASM_WIZARD=kasm,kasm-wizard,home,admin,Kasm Wizard,kasm-wizard,hshq" checkAddSvc "SVCD_KEILA=keila,keila,primary,user,Keila,keila,hshq" checkAddSvc "SVCD_KILLBILL_API=killbill,kb-api,primary,user,KillBillAPI,kb-api,hshq" checkAddSvc "SVCD_KILLBILL_WEB=killbill,killbill,primary,user,KillBill,killbill,hshq" checkAddSvc "SVCD_MAILU=mailu,mailu,primary,user,Mailu,mailu,hshq" checkAddSvc "SVCD_MASTODON=mastodon,mastodon,other,user,Mastodon,mastodon,le" checkAddSvc "SVCD_MATRIX_ELEMENT_PRIVATE=matrix,element-private,other,user,Matrix Element (Private Jitsi),element-private,hshq" checkAddSvc "SVCD_MATRIX_ELEMENT_PUBLIC=matrix,element-public,other,user,Matrix Element (Public Jitsi),element-public,hshq" checkAddSvc "SVCD_MATRIX_SYNAPSE=matrix,synapse,other,user,Synapse,synapse,le" checkAddSvc "SVCD_MATOMO=matomo,matomo,primary,admin,Matomo,matomo,hshq" checkAddSvc "SVCD_MEALIE=mealie,mealie,other,user,Mealie,mealie,hshq" checkAddSvc "SVCD_MESHCENTRAL=meshcentral,meshcentral,primary,admin,MeshCentral,meshcentral,hshq" checkAddSvc "SVCD_METABASE=metabase,metabase,primary,user,Metabase,metabase,hshq" checkAddSvc "SVCD_MINTHCM=minthcm,minthcm,primary,user,MintHCM,minthcm,hshq" checkAddSvc "SVCD_N8N_APP=n8n,n8n,primary,admin,n8n,n8n,hshq" checkAddSvc "SVCD_NAVIDROME=navidrome,navidrome,other,user,Navidrome,navidrome,hshq" checkAddSvc "SVCD_NETDATA=netdata,netdata,primary,admin,Netdata,netdata,hshq" checkAddSvc "SVCD_NEXTCLOUD=nextcloud,nextcloud,other,user,Nextcloud,nextcloud,hshq" checkAddSvc "SVCD_NCTALKHPB=nextcloud,spreed,other,user,Nextcloud Talk HPB,spreed,hshq" checkAddSvc "SVCD_NCTALKRECORD=nextcloud,nctalk-record,other,user,Nextcloud Talk Recording,nctalk-record,hshq" checkAddSvc "SVCD_NTFY=ntfy,ntfy,primary,admin,NTFY,ntfy,hshq" checkAddSvc "SVCD_ODOO=odoo,odoo,primary,user,Odoo,odoo,hshq" checkAddSvc "SVCD_OMBI_APP=ombi,ombi,primary,user,Ombi,ombi,le" checkAddSvc "SVCD_OPENLDAP_MANAGER=openldap,usermanager,other,user,User Manager,usermanager,hshq" checkAddSvc "SVCD_OPENLDAP_PHP=openldap,ldapphp,primary,admin,LDAP PHP,ldapphp,hshq" checkAddSvc "SVCD_OPENPROJECT_APP=openproject,openproject,primary,user,OpenProject,openproject,hshq" sed -i "s/SVCD_PAPERLESS=/SVCD_PAPERLESS_APP=/" $CONFIG_FILE checkAddSvc "SVCD_PAPERLESS_APP=paperless,paperless,primary,user,Paperless-ngx,paperless,hshq" checkAddSvc "SVCD_PAPERLESS_AI_WEB=paperless,paperless-ai,primary,user,Zettelrobbe,paperless-ai,hshq" checkAddSvc "SVCD_PAPERLESS_GPT_WEB=paperless,paperless-gpt,primary,user,Paperless-GPT,paperless-gpt,hshq" checkAddSvc "SVCD_PASTEFY=pastefy,pastefy,primary,user,Pastefy,pastefy,hshq" checkAddSvc "SVCD_PEERTUBE=peertube,peertube,other,user,PeerTube,peertube,hshq" checkAddSvc "SVCD_PENPOT=penpot,penpot,other,user,Penpot,penpot,hshq" checkAddSvc "SVCD_PHOTOPRISM=photoprism,photoprism,other,user,PhotoPrism,photoprism,hshq" checkAddSvc "SVCD_PIPED_FRONTEND=piped,piped,primary,user,Piped,piped,hshq" checkAddSvc "SVCD_PIPED_PROXY=piped,piped-proxy,primary,user,Piped,piped-proxy,hshq" checkAddSvc "SVCD_PIPED_API=piped,piped-api,primary,user,Piped,piped-api,hshq" checkAddSvc "SVCD_PIXELFED=pixelfed,pixelfed,other,user,Pixelfed,pixelfed,hshq" checkAddSvc "SVCD_PORTAINER=portainer,portainer,primary,admin,Portainer,portainer,hshq" checkAddSvc "SVCD_POSTFIX=mail-relay,mail,primary,admin,Mail,mail,hshq" checkAddSvc "SVCD_PROMETHEUS=sysutils,prometheus,primary,admin,Prometheus,prometheus,hshq" checkAddSvc "SVCD_QBITTORRENT=qbittorrent,qbittorrent,primary,admin,qBittorrent,qbittorrent,hshq" checkAddSvc "SVCD_RALLLY=rallly,rallly,primary,user,Rallly,rallly,hshq" checkAddSvc "SVCD_REMOTELY=remotely,remotely,primary,admin,Remotely,remotely,hshq" checkAddSvc "SVCD_REVOLT=revolt,revolt,other,user,Revolt,revolt,hshq" checkAddSvc "SVCD_RSPAMD=mail-relay,rspamd,primary,admin,Rspamd,rspamd,hshq" checkAddSvc "SVCD_SABNZBD=sabnzbd,sabnzbd,primary,admin,SABnzbd,sabnzbd,hshq" checkAddSvc "SVCD_SEARXNG=searxng,searxng,primary,user,SearxNG,searxng,hshq" checkAddSvc "SVCD_SERVARR_SONARR=servarr,sonarr,primary,admin,Sonarr,sonarr,hshq" checkAddSvc "SVCD_SERVARR_RADARR=servarr,radarr,primary,admin,Radarr,radarr,hshq" checkAddSvc "SVCD_SERVARR_LIDARR=servarr,lidarr,primary,admin,Lidarr,lidarr,hshq" checkAddSvc "SVCD_SERVARR_READARR=servarr,readarr,primary,admin,Readarr,readarr,hshq" checkAddSvc "SVCD_SERVARR_BAZARR=servarr,bazarr,primary,admin,Bazarr,bazarr,hshq" checkAddSvc "SVCD_SERVARR_MYLAR3=servarr,mylar3,primary,admin,Mylar3,mylar3,hshq" checkAddSvc "SVCD_SERVARR_PROWLARR=servarr,prowlarr,primary,admin,Prowlarr,prowlarr,hshq" checkAddSvc "SVCD_SHLINK_APP=shlink,shlink-app,other,user,Shlink App,links,hshq" checkAddSvc "SVCD_SHLINK_WEB=shlink,shlink-web,primary,admin,Shlink,shlink,hshq" checkAddSvc "SVCD_SNIPPETBOX=snippetbox,snippetbox,primary,user,SnippetBox,snippetbox,hshq" checkAddSvc "SVCD_SPEEDTEST_TRACKER_LOCAL=speedtest-tracker-local,speedtest-tracker-local,primary,admin,Speedtest Tracker Local,speedtest-tracker-local,hshq" checkAddSvc "SVCD_SPEEDTEST_TRACKER_VPN=speedtest-tracker-vpn,speedtest-tracker-vpn,primary,admin,Speedtest Tracker VPN,speedtest-tracker-vpn,hshq" checkAddSvc "SVCD_SQLPAD=sqlpad,sqlpad,primary,admin,SQLPad,sqlpad,hshq" checkAddSvc "SVCD_STANDARDNOTES_SERVER=standardnotes,standardnotes,other,user,StandardNotes,standardnotes,le" checkAddSvc "SVCD_STANDARDNOTES_FILES=standardnotes,standardnotes-files,other,user,StandardNotes-Files,standardnotes-files,le" checkAddSvc "SVCD_STANDARDNOTES_WEB=standardnotes,standardnotes-web,other,user,StandardNotes-Web,standardnotes-web,hshq" checkAddSvc "SVCD_STIRLINGPDF=stirlingpdf,stirlingpdf,primary,user,Stirling PDF,stirlingpdf,hshq" checkAddSvc "SVCD_SYNCTHING=syncthing,syncthing,primary,admin,Syncthing,syncthing,hshq" checkAddSvc "SVCD_SYSUTILS=sysutils,sysutils,primary,admin,SysUtils,sysutils,hshq" checkAddSvc "SVCD_TWENTY=twenty,twenty,primary,user,Twenty,twenty,hshq" checkAddSvc "SVCD_UPTIMEKUMA=uptimekuma,uptimekuma,primary,admin,UptimeKuma,uptimekuma,hshq" checkAddSvc "SVCD_VAULTWARDEN=vaultwarden,vaultwarden,primary,user,Vaultwarden,vaultwarden,hshq" checkAddSvc "SVCD_WALLABAG=wallabag,wallabag,primary,user,Wallabag,wallabag,le" checkAddSvc "SVCD_WAZUH=wazuh,wazuh,primary,admin,Wazuh,wazuh,hshq" checkAddSvc "SVCD_WEKAN=wekan,wekan,primary,user,WeKan,wekan,hshq" checkAddSvc "SVCD_WGPORTAL=wgportal,wgportal,primary,admin,WG Portal,wgportal,hshq" checkAddSvc "SVCD_WIKIJS=wikijs,wikijs,other,user,Wiki.js,wikijs,hshq" checkAddSvc "SVCD_WORDPRESS=wordpress,wordpress,other,user,WordPress,wordpress,hshq" checkAddSvc "SVCD_YAMTRACK=yamtrack,yamtrack,other,user,Yamtrack,yamtrack,hshq" checkAddSvc "SVCD_ZAMMAD_APP=zammad,zammad,primary,user,Zammad,zammad,hshq" checkAddSvc "SVCD_ZULIP_APP=zulip,zulip,primary,user,Zulip,zulip,hshq" checkAddSvc "SVCD_BESZEL_APP=beszel,beszel,home,admin,Beszel,beszel,hshq" checkAddSvc "SVCD_TAIGA_APP=taiga,taiga,primary,user,Taiga,taiga,hshq" checkAddSvc "SVCD_OPENSIGN_APP=opensign,opensign,primary,user,OpenSign,opensign,hshq" checkAddSvc "SVCD_DOCUSEAL_APP=docuseal,docuseal,primary,user,DocuSeal,docuseal,hshq" checkAddSvc "SVCD_CONTROLR_APP=controlr,controlr,primary,admin,ControlR,controlr,hshq" checkAddSvc "SVCD_CONTROLR_ASPIRE=controlr,controlr-aspire,primary,admin,ControlR-Aspire,controlr-aspire,hshq" checkAddSvc "SVCD_AKAUNTING_APP=akaunting,akaunting,primary,user,Akaunting,akaunting,hshq" checkAddSvc "SVCD_AXELOR_APP=axelor,axelor,primary,user,Axelor,axelor,hshq" checkAddSvc "SVCD_AXELOR_GOOVEE=axelor,goovee,primary,user,Axelor Goovee,goovee,hshq" checkAddSvc "SVCD_CONVERTX_APP=convertx,convertx,primary,user,ConvertX,convertx,hshq" checkAddSvc "SVCD_KOPIA_APP=kopia,kopia,primary,admin,Kopia,kopia,hshq" checkAddSvc "SVCD_LOCALAI_SERVER=localai,localai,primary,user,LocalAI,localai,hshq" checkAddSvc "SVCD_LOCALAI_RECALL=localai,localrecall,primary,user,LocalRecall,localrecall,hshq" checkAddSvc "SVCD_LOCALAI_AGI=localai,localagi,primary,user,LocalAGI,localagi,hshq" checkAddSvc "SVCD_COMFYUI_API=comfyui,comfyui-api,primary,user,ComfyUI-API,comfyui-api,hshq" checkAddSvc "SVCD_COMFYUI_APP=comfyui,comfyui,primary,user,ComfyUI,comfyui,hshq" checkAddSvc "SVCD_LANGFLOW_APP=langflow,langflow,primary,user,Langflow,langflow,hshq" checkAddSvc "SVCD_ANYTHINGLLM_APP=anythingllm,anythingllm,primary,user,AnythingLLM,anythingllm,hshq" checkAddSvc "SVCD_PERPLEXICA_APP=perplexica,perplexica,primary,user,Perplexica,perplexica,hshq" checkAddSvc "SVCD_FIRECRAWL_API=firecrawl,firecrawl,primary,admin,Firecrawl,firecrawl,hshq" checkAddSvc "SVCD_LIBRECHAT_APP=librechat,librechat,primary,user,LibreChat,librechat,hshq" checkAddSvc "SVCD_CRAWL4AI_APP=crawl4ai,crawl4ai,primary,admin,Crawl4AI,crawl4ai,hshq" checkAddSvc "SVCD_OLLAMA_SERVER=ollama,ollama,primary,admin,Ollama,ollama,hshq" checkAddSvc "SVCD_OPENWEBUI_APP=openwebui,openwebui,primary,user,Open WebUI,openwebui,hshq" checkAddSvc "SVCD_OPENWEBUI_QDRANT=openwebui,openwebui-qdrant,primary,admin,Qdrant (Open WebUI),openwebui-qdrant,hshq" checkAddSvc "SVCD_KHOJ_SERVER=khoj,khoj,primary,user,Khoj,khoj,hshq" checkAddSvc "SVCD_LOBECHAT_APP=lobechat,lobechat,primary,user,LobeChat,lobechat,hshq" checkAddSvc "SVCD_INVOKEAI_APP=invokeai,invokeai,primary,user,InvokeAI,invokeai,hshq" checkAddSvc "SVCD_RAGFLOW_APP=ragflow,ragflow,primary,user,RAGFlow,ragflow,hshq" checkAddSvc "SVCD_RAGFLOW_API=ragflow,ragflow-api,primary,user,RAGFlow API,ragflow-api,hshq" checkAddSvc "SVCD_RAGFLOW_ADMIN=ragflow,ragflow-admin,primary,admin,RAGFlow Admin,ragflow-admin,hshq" checkAddSvc "SVCD_RAGFLOW_MCP=ragflow,ragflow-mcp,primary,user,RAGFlow MCP,ragflow-mcp,hshq" checkAddSvc "SVCD_RAGFLOW_MINIO=ragflow,ragflow-minio,primary,admin,MinIO (RAGFlow),ragflow-minio,hshq" checkAddSvc "SVCD_RAGFLOW_RUSTFS=ragflow,ragflow-rustfs,primary,admin,RustFS (RAGFlow),ragflow-rustfs,hshq" checkAddSvc "SVCD_RAGFLOW_INFINITY_WEB=ragflow,ragflow-infinity,primary,admin,RAGFlow Infinity,ragflow-infinity,hshq" checkAddSvc "SVCD_TABBYML_APP=tabbyml,tabbyml,primary,admin,TabbyML,tabbyml,hshq" checkAddSvc "SVCD_DEEPWIKI_OPEN_APP=deepwikiopen,deepwikiopen,primary,admin,DeepWiki-Open,deepwikiopen,hshq" checkAddSvc "SVCD_DEEPWIKI_OPEN_API=deepwikiopen,deepwikiopen-api,primary,admin,DeepWiki-Open API,deepwikiopen-api,hshq" checkAddSvc "SVCD_DOCLING_APP=docling,docling,primary,user,Docling,docling,hshq" checkAddSvc "SVCD_DIFY_APP=dify,dify,primary,user,Dify,dify,hshq" checkAddSvc "SVCD_MINDSDB_APP=mindsdb,mindsdb,primary,admin,MindsDB,mindsdb,hshq" checkAddSvc "SVCD_WATERCRAWL_APP=watercrawl,watercrawl,primary,user,WaterCrawl,watercrawl,hshq" checkAddSvc "SVCD_FLOWISE_APP=flowise,flowise,primary,user,Flowise,flowise,hshq" checkAddSvc "SVCD_SURFSENSE_APP=surfsense,surfsense,primary,user,SurfSense,surfsense,hshq" checkAddSvc "SVCD_SURFSENSE_BACKEND=surfsense,surfsense-backend,primary,user,SurfSense Backend,surfsense-backend,hshq" checkAddSvc "SVCD_NOCODB_APP=nocodb,nocodb,primary,user,NocoDB,nocodb,hshq" checkAddSvc "SVCD_ENTE_APP=ente,ente,primary,user,Ente,ente,hshq" checkAddSvc "SVCD_ENTE_SERVER=ente,ente-server,primary,user,Ente Server,ente-server,hshq" checkAddSvc "SVCD_MORPHIC_APP=morphic,morphic,primary,user,Morphic,morphic,hshq" checkAddSvc "SVCD_MORPHIC_SUPABASE=morphic,morphic-supabase,primary,admin,Morphic Supabase,morphic-supabase,hshq" checkAddSvc "SVCD_OPENNOTEBOOK_APP=opennotebook,opennotebook,primary,user,OpenNotebook,opennotebook,hshq" checkAddSvc "SVCD_APPSMITH_APP=appsmith,appsmith,primary,user,Appsmith,appsmith,hshq" checkAddSvc "SVCD_TRILIUM_APP=trilium,trilium,primary,user,Trilium,trilium,hshq" checkAddSvc "SVCD_DOCSGPT_APP=docsgpt,docsgpt,primary,user,DocsGPT,docsgpt,hshq" checkAddSvc "SVCD_DOCSGPT_API=docsgpt,docsgpt-api,primary,user,DocsGPT-API,docsgpt-api,hshq" checkAddSvc "SVCD_MEMOS_APP=memos,memos,primary,user,Memos,memos,hshq" checkAddSvc "SVCD_SILLYTAVERN_APP=sillytavern,sillytavern,primary,user,SillyTavern,sillytavern,hshq" checkAddSvc "SVCD_LEMONADE_SERVER=lemonade,lemonade-server,primary,user,Lemonade Server,lemonade-server,hshq" checkAddSvc "SVCD_LEMONADE_WEB=lemonade,lemonade-web,primary,user,Lemonade,lemonade-web,hshq" checkAddSvc "SVCD_SPEAKR_APP=speakr,speakr,primary,user,Speakr,speakr,hshq" checkAddSvc "SVCD_INSANELYFASTWHISPER_APP=insanelyfastwhisper,insanelyfastwhisper,primary,user,InsanelyFastWhisper,insanelyfastwhisper,hshq" checkAddSvc "SVCD_INSANELYFASTWHISPER_API=insanelyfastwhisper,insanelyfastwhisper-api,primary,user,InsanelyFastWhisper API,insanelyfastwhisper-api,hshq" checkAddSvc "SVCD_IVBOX_QWEN=ivbox,qwenimagestudio,primary,user,Qwen Image Studio (IVB),qwenimagestudio,hshq" checkAddSvc "SVCD_IVBOX_COMFYUI_WEB=ivbox,comfyui-ivb,primary,user,ComfyUI (IVB),comfyui-ivb,hshq" checkAddSvc "SVCD_IVBOX_COMFYUI_API=ivbox,comfyui-ivb-api,primary,user,ComfyUI (IVB) API,comfyui-ivb-api,hshq" checkAddSvc "SVCD_MONICA_APP=monica,monica,primary,user,Monica,monica,hshq" checkAddSvc "SVCD_AFFINE_APP=affine,affine,primary,user,AFFiNE,affine,hshq" checkAddSvc "SVCD_JOPLIN_APP=joplin,joplin,primary,user,Joplin,joplin,hshq" checkAddSvc "SVCD_JOPLIN_TRANSCRIBE_APP=joplin,joplin-transcribe,primary,user,JoplinTranscribe,joplin-transcribe,hshq" checkAddSvc "SVCD_SUPERSET_APP=superset,superset,primary,admin,Superset,superset,hshq" checkAddSvc "SVCD_KOKORO_WEB=kokoro,kokoro,primary,user,KokoroTTS,kokoro,hshq" checkAddSvc "SVCD_KOKORO_API=kokoro,kokoro-tts,primary,user,KokoroAPI,kokoro-tts,hshq" checkAddSvc "SVCD_CHATTERBOX_APP=chatterbox,chatterbox,primary,user,ChatterboxTTS,chatterbox,hshq" checkAddSvc "SVCD_CHATTERBOX_API=chatterbox,chatterbox-api,primary,user,ChatterboxAPI,chatterbox-api,hshq" checkAddSvc "SVCD_LITELLM_APP=litellm,litellm,primary,admin,LiteLLM,litellm,hshq" checkAddSvc "SVCD_LANGFUSE_APP=langfuse,langfuse,primary,admin,Langfuse,langfuse,hshq" checkAddSvc "SVCD_LANGFUSE_CLICKHOUSE=langfuse,langfuse-clickhouse,primary,admin,LangfuseClickhouse,langfuse-clickhouse,hshq" checkAddSvc "SVCD_LANGFUSE_MINIO=langfuse,langfuse-minio,primary,admin,MinIO (Langfuse),langfuse-minio,hshq" checkAddSvc "SVCD_SKYVERN_WEB=skyvern,skyvern,primary,user,Skyvern,skyvern,hshq" checkAddSvc "SVCD_SKYVERN_API=skyvern,skyvern-api,primary,user,SkyvernAPI,skyvern-api,hshq" checkAddSvc "SVCD_SKYVERN_ARTIFACT=skyvern,skyvern-artifact,primary,user,SkyvernArtifact,skyvern-artifact,hshq" checkAddSvc "SVCD_SKYVERN_BROWSER=skyvern,skyvern-browser,primary,user,SkyvernBrowser,skyvern-browser,hshq" checkAddSvc "SVCD_WGER_APP=wger,wger,primary,user,Wger,wger,hshq" checkAddSvc "SVCD_WORKOUTCOOL_APP=workoutcool,workoutcool,primary,user,WorkoutCool,workoutcool,hshq" checkAddSvc "SVCD_OPENRAG_APP=openrag,openrag,primary,user,OpenRag,openrag,hshq" checkAddSvc "SVCD_OPENRAG_OPENSEARCH=openrag,openrag-opensearch,primary,admin,OpenSearch (OpenRag),openrag-opensearch,hshq" checkAddSvc "SVCD_OPENRAG_LANGFLOW=openrag,openrag-langflow,primary,user,Langflow (OpenRag),openrag-langflow,hshq" checkAddSvc "SVCD_VOICEBOX_APP=voicebox,voicebox,primary,user,Voicebox,voicebox,hshq" checkAddSvc "SVCD_OPENCODE_APP=opencode,opencode,primary,user,OpenCode,opencode,hshq" checkAddSvc "SVCD_OPENCODE_CODESERVER=opencode,opencode-codeserver,primary,user,CodeServer (OpenCode),opencode-codeserver,hshq" checkAddSvc "SVCD_OPENSKILLS_APP=openskills,openskills,home,admin,OpenSkills,openskills,hshq" checkAddSvc "SVCD_EMAILCLASSIFIERAI_APP=emailclassifierai,emailclassifierai,primary,admin,EmailClassifierAI,emailclassifierai,hshq" checkAddSvc "SVCD_HERMES_AGENT_DASHBOARD=hermes-agent,hermes-dashboard,primary,admin,HermesAgent,hermes-dashboard,hshq" checkAddSvc "SVCD_HERMES_AGENT_API=hermes-agent,hermes-api,primary,admin,HermesAPI,hermes-api,hshq" checkAddSvc "SVCD_HERMES_AGENT_WEBUI=hermes-agent,hermes-webui,primary,admin,HermesWebUI,hermes-webui,hshq" checkAddSvc "SVCD_AUTOKB_WEB=autokb,autokb,primary,admin,AutoKB,autokb,hshq" checkAddSvc "SVCD_SUITECRM_APP=suitecrm,suitecrm,primary,user,SuiteCRM,suitecrm,hshq" checkAddSvc "SVCD_HEDGEDOC_APP=hedgedoc,hedgedoc,primary,user,HedgeDoc,hedgedoc,hshq" checkAddSvc "SVCD_PRESENTON_APP=presenton,presenton,primary,user,Presenton,presenton,hshq" checkAddSvc "SVCD_BASICMEMORY_APP=basicmemory,basicmemory,primary,user,BasicMemory,basicmemory,hshq" checkAddSvc "SVCD_COGNEE_APP=cognee,cognee-api,primary,admin,Cognee API,cognee-api,hshq" checkAddSvc "SVCD_COGNEE_FRONTEND=cognee,cognee,primary,admin,Cognee,cognee,hshq" checkAddSvc "SVCD_LIGHTRAG_APP=lightrag,lightrag,primary,admin,LightRAG,lightrag,hshq" checkAddSvc "SVCD_LIGHTRAG_QDRANT=lightrag,lightrag-qdrant,primary,admin,Qdrant (LightRAG),lightrag-qdrant,hshq" checkAddSvc "SVCD_OPENSERP_APP=openserp,openserp,primary,user,OpenSERP,openserp,hshq" checkAddSvc "SVCD_SURICATA_APP=suricata,suricata,primary,admin,Suricata,suricata,hshq" checkAddSvc "SVCD_CROWDSEC_WEB=crowdsec,crowdsec,primary,admin,CrowdSec Web UI,crowdsec,hshq" checkAddSvc "SVCD_FASTCRW_APP=fastcrw,fastcrw,primary,user,fastCRW,fastcrw,hshq" checkAddSvc "SVCD_WHITEBOARDRT_WEB=whiteboardrt,whiteboardrt,primary,user,WhiteboardRT,whiteboardrt,hshq" checkAddSvc "SVCD_WHITEBOARDRT_SERVER=whiteboardrt,whiteboardrt-server,primary,user,WhiteboardRT Server,whiteboardrt-server,hshq" checkAddSvc "SVCD_WHITEBOARDRT_STORAGE=whiteboardrt,whiteboardrt-storage,primary,user,WhiteboardRT Storage,whiteboardrt-storage,hshq" checkAddSvc "SVCD_WHITEBOARDRT_AGENT=whiteboardrt,whiteboardrt-agent,primary,user,WhiteboardRT Agent,whiteboardrt-agent,hshq" checkAddSvc "SVCD_MDCOLLAB_APP=mdcollab,mdcollab,primary,user,mdCollab,mdcollab,hshq" checkAddSvc "SVCD_HALOSPEAK_APP=halospeak,halospeak,primary,user,HaloSpeak,halospeak,hshq" checkAddSvc "SVCD_HALOSPEAK_API=halospeak,halospeak-api,primary,user,HaloSpeak API,halospeak-api,hshq" #ADD_NEW_SVC_VARS_HERE set -e } function installStackByName() { stack_name=$1 is_integrate=$2 cd ~ echo "------------------------------------------" echo " Installing stack: $stack_name" echo "------------------------------------------" echo "Current date/time: $(date '+%Y-%m-%d %H:%M:%S')" case "$stack_name" in adguard) installAdGuard $is_integrate ;; sysutils) installSysUtils $is_integrate ;; openldap) installOpenLDAP $is_integrate ;; mailu) installMailu $is_integrate ;; wazuh) installWazuh $is_integrate ;; collabora) installCollabora $is_integrate ;; nextcloud) installNextcloud $is_integrate ;; jitsi) installJitsi $is_integrate ;; matrix) installMatrix $is_integrate ;; wikijs) installWikijs $is_integrate ;; duplicati) installDuplicati $is_integrate ;; mastodon) installMastodon $is_integrate ;; dozzle) installDozzle $is_integrate ;; searxng) installSearxNG $is_integrate ;; jellyfin) installJellyfin $is_integrate ;; filebrowser) installFileBrowser $is_integrate ;; photoprism) installPhotoPrism $is_integrate ;; guacamole) installGuacamole $is_integrate ;; authelia) installAuthelia $is_integrate ;; wordpress) installWordPress $is_integrate ;; ghost) installGhost $is_integrate ;; peertube) installPeerTube $is_integrate ;; homeassistant) installHomeAssistant $is_integrate ;; gitlab) installGitlab $is_integrate ;; vaultwarden) installVaultwarden $is_integrate ;; discourse) installDiscourse $is_integrate ;; syncthing) installSyncthing $is_integrate ;; codeserver) installCodeServer $is_integrate ;; shlink) installShlink $is_integrate ;; firefly) installFirefly $is_integrate ;; excalidraw) installExcalidraw $is_integrate ;; drawio) installDrawIO $is_integrate ;; invidious) installInvidious $is_integrate ;; ittools) installITTools $is_integrate ;; gitea) installGitea $is_integrate ;; mealie) installMealie $is_integrate ;; kasm) installKasm $is_integrate ;; ntfy) installNTFY $is_integrate ;; remotely) installRemotely $is_integrate ;; calibre) installCalibre $is_integrate ;; netdata) installNetdata $is_integrate ;; linkwarden) installLinkwarden $is_integrate ;; stirlingpdf) installStirlingPDF $is_integrate ;; bar-assistant) installBarAssistant $is_integrate ;; freshrss) installFreshRSS $is_integrate ;; keila) installKeila $is_integrate ;; wallabag) installWallabag $is_integrate ;; jupyter) installJupyter $is_integrate ;; paperless) installPaperless $is_integrate ;; speedtest-tracker-local) installSpeedtestTrackerLocal $is_integrate ;; speedtest-tracker-vpn) installSpeedtestTrackerVPN $is_integrate ;; changedetection) installChangeDetection $is_integrate ;; huginn) installHuginn $is_integrate ;; coturn) installCoturn $is_integrate ;; filedrop) installFileDrop $is_integrate ;; piped) installPiped $is_integrate ;; grampsweb) installGrampsWeb $is_integrate ;; penpot) installPenpot $is_integrate ;; espocrm) installEspoCRM $is_integrate ;; immich) installImmich $is_integrate ;; homarr) installHomarr $is_integrate ;; matomo) installMatomo $is_integrate ;; pastefy) installPastefy $is_integrate ;; snippetbox) installSnippetBox $is_integrate ;; aistack) installAIStack $is_integrate ;; pixelfed) installPixelfed $is_integrate ;; yamtrack) installYamtrack $is_integrate ;; servarr) installServarr $is_integrate ;; sabnzbd) installSABnzbd $is_integrate ;; qbittorrent) installqBittorrent $is_integrate ;; ombi) installOmbi $is_integrate ;; meshcentral) installMeshCentral $is_integrate ;; navidrome) installNavidrome $is_integrate ;; adminer) installAdminer $is_integrate ;; budibase) installBudibase $is_integrate ;; audiobookshelf) installAudioBookshelf $is_integrate ;; standardnotes) installStandardNotes $is_integrate ;; metabase) installMetabase $is_integrate ;; kanboard) installKanboard $is_integrate ;; wekan) installWekan $is_integrate ;; revolt) installRevolt $is_integrate ;; frappe-hr) installFrappeHR $is_integrate ;; minthcm) installMintHCM $is_integrate ;; twenty) installTwenty $is_integrate ;; odoo) installOdoo $is_integrate ;; calcom) installCalcom $is_integrate ;; rallly) installRallly $is_integrate ;; easyappointments) installEasyAppointments $is_integrate ;; openproject) installOpenProject $is_integrate ;; zammad) installZammad $is_integrate ;; zulip) installZulip $is_integrate ;; killbill) installKillBill $is_integrate ;; invoiceshelf) installInvoiceShelf $is_integrate ;; invoiceninja) installInvoiceNinja $is_integrate ;; dolibarr) installDolibarr $is_integrate ;; n8n) installn8n $is_integrate ;; automatisch) installAutomatisch $is_integrate ;; activepieces) installActivePieces $is_integrate ;; heimdall) installHeimdall $is_integrate ;; ofelia) installOfelia $is_integrate ;; script-server) installScriptServer $is_integrate ;; cloudbeaver) installCloudBeaver $is_integrate ;; dbgate) installDbGate $is_integrate ;; sqlpad) installSQLPad $is_integrate ;; uptimekuma) installUptimeKuma $is_integrate ;; beszel) installBeszel $is_integrate ;; taiga) installTaiga $is_integrate ;; opensign) installOpenSign $is_integrate ;; docuseal) installDocuSeal $is_integrate ;; controlr) installControlR $is_integrate ;; akaunting) installAkaunting $is_integrate ;; axelor) installAxelor $is_integrate ;; convertx) installConvertX $is_integrate ;; kopia) installKopia $is_integrate ;; localai) installLocalAI $is_integrate ;; comfyui) installComfyUI $is_integrate ;; langflow) installLangflow $is_integrate ;; anythingllm) installAnythingLLM $is_integrate ;; perplexica) installPerplexica $is_integrate ;; firecrawl) installFirecrawl $is_integrate ;; librechat) installLibreChat $is_integrate ;; crawl4ai) installCrawl4AI $is_integrate ;; ollama) installOllama $is_integrate ;; openwebui) installOpenWebUI $is_integrate ;; khoj) installKhoj $is_integrate ;; lobechat) installLobeChat $is_integrate ;; invokeai) installInvokeAI $is_integrate ;; ragflow) installRAGFlow $is_integrate ;; tabbyml) installTabbyML $is_integrate ;; deepwikiopen) installDeepWikiOpen $is_integrate ;; docling) installDocling $is_integrate ;; dify) installDify $is_integrate ;; mindsdb) installMindsDB $is_integrate ;; watercrawl) installWaterCrawl $is_integrate ;; flowise) installFlowise $is_integrate ;; surfsense) installSurfSense $is_integrate ;; nocodb) installNocoDB $is_integrate ;; ente) installEnte $is_integrate ;; morphic) installMorphic $is_integrate ;; opennotebook) installOpenNotebook $is_integrate ;; appsmith) installAppsmith $is_integrate ;; trilium) installTrilium $is_integrate ;; docsgpt) installDocsGPT $is_integrate ;; memos) installMemos $is_integrate ;; sillytavern) installSillyTavern $is_integrate ;; lemonade) installLemonade $is_integrate ;; speakr) installSpeakr $is_integrate ;; insanelyfastwhisper) installInsanelyFastWhisper $is_integrate ;; ivbox) installIVBox $is_integrate ;; monica) installMonica $is_integrate ;; affine) installAFFiNE $is_integrate ;; joplin) installJoplin $is_integrate ;; superset) installSuperset $is_integrate ;; kokoro) installKokoro $is_integrate ;; chatterbox) installChatterbox $is_integrate ;; litellm) installLiteLLM $is_integrate ;; langfuse) installLangfuse $is_integrate ;; skyvern) installSkyvern $is_integrate ;; wger) installWger $is_integrate ;; workoutcool) installWorkoutCool $is_integrate ;; openrag) installOpenRag $is_integrate ;; voicebox) installVoicebox $is_integrate ;; opencode) installOpenCode $is_integrate ;; openskills) installOpenSkills $is_integrate ;; emailclassifierai) installEmailClassifierAI $is_integrate ;; hermes-agent) installHermesAgent $is_integrate ;; autokb) installAutoKB $is_integrate ;; suitecrm) installSuiteCRM $is_integrate ;; hedgedoc) installHedgeDoc $is_integrate ;; presenton) installPresenton $is_integrate ;; basicmemory) installBasicMemory $is_integrate ;; cognee) installCognee $is_integrate ;; lightrag) installLightRAG $is_integrate ;; openserp) installOpenSERP $is_integrate ;; suricata) installSuricata $is_integrate ;; crowdsec) installCrowdSec $is_integrate ;; fastcrw) installfastCRW $is_integrate ;; whiteboardrt) installWhiteboardRT $is_integrate ;; mdcollab) installmdCollab $is_integrate ;; halospeak) installHaloSpeak $is_integrate ;; #ADD_NEW_INSTALL_STACK_HERE esac stack_install_retval=$? if [ $stack_install_retval -ne 0 ]; then notifyStackInstallFailure "$stack_name" if [ "$IS_INSTALLING" = "true" ]; then logHSHQEvent error "Error installing stack ($stack_name), exiting..." exit 1 fi fi postInstallActionsByName "$stack_name" } function postInstallActionsByName() { return } function performUpdateStackByName() { stack_name=$1 case "$stack_name" in portainer) performUpdatePortainer ;; adguard) performUpdateAdGuard ;; sysutils) performUpdateSysUtils ;; openldap) performUpdateOpenLDAP ;; mailu) performUpdateMailu ;; wazuh) performUpdateWazuh ;; collabora) performUpdateCollabora ;; nextcloud) performUpdateNextcloud ;; jitsi) performUpdateJitsi ;; matrix) performUpdateMatrix ;; wikijs) performUpdateWikijs ;; duplicati) performUpdateDuplicati ;; mastodon) performUpdateMastodon ;; dozzle) performUpdateDozzle ;; searxng) performUpdateSearxNG ;; jellyfin) performUpdateJellyfin ;; filebrowser) performUpdateFileBrowser ;; photoprism) performUpdatePhotoPrism ;; guacamole) performUpdateGuacamole ;; authelia) performUpdateAuthelia ;; wordpress) performUpdateWordPress ;; ghost) performUpdateGhost ;; peertube) performUpdatePeerTube ;; homeassistant) performUpdateHomeAssistant ;; gitlab) performUpdateGitlab ;; vaultwarden) performUpdateVaultwarden ;; discourse) performUpdateDiscourse ;; syncthing) performUpdateSyncthing ;; codeserver) performUpdateCodeServer ;; shlink) performUpdateShlink ;; firefly) performUpdateFirefly ;; excalidraw) performUpdateExcalidraw ;; drawio) performUpdateDrawIO ;; invidious) performUpdateInvidious ;; ittools) performUpdateITTools ;; gitea) performUpdateGitea ;; mealie) performUpdateMealie ;; kasm) performUpdateKasm ;; ntfy) performUpdateNTFY ;; remotely) performUpdateRemotely ;; calibre) performUpdateCalibre ;; netdata) performUpdateNetdata ;; linkwarden) performUpdateLinkwarden ;; stirlingpdf) performUpdateStirlingPDF ;; bar-assistant) performUpdateBarAssistant ;; freshrss) performUpdateFreshRSS ;; keila) performUpdateKeila ;; wallabag) performUpdateWallabag ;; jupyter) performUpdateJupyter ;; paperless) performUpdatePaperless ;; speedtest-tracker-local) performUpdateSpeedtestTrackerLocal ;; speedtest-tracker-vpn) performUpdateSpeedtestTrackerVPN ;; changedetection) performUpdateChangeDetection ;; huginn) performUpdateHuginn ;; coturn) performUpdateCoturn ;; filedrop) performUpdateFileDrop ;; piped) performUpdatePiped ;; grampsweb) performUpdateGrampsWeb ;; penpot) performUpdatePenpot ;; espocrm) performUpdateEspoCRM ;; immich) performUpdateImmich ;; homarr) performUpdateHomarr ;; matomo) performUpdateMatomo ;; pastefy) performUpdatePastefy ;; snippetbox) performUpdateSnippetBox ;; aistack) performUpdateAIStack ;; pixelfed) performUpdatePixelfed ;; yamtrack) performUpdateYamtrack ;; servarr) performUpdateServarr ;; sabnzbd) performUpdateSABnzbd ;; qbittorrent) performUpdateqBittorrent ;; ombi) performUpdateOmbi ;; meshcentral) performUpdateMeshCentral ;; navidrome) performUpdateNavidrome ;; adminer) performUpdateAdminer ;; budibase) performUpdateBudibase ;; audiobookshelf) performUpdateAudioBookshelf ;; standardnotes) performUpdateStandardNotes ;; metabase) performUpdateMetabase ;; kanboard) performUpdateKanboard ;; wekan) performUpdateWekan ;; revolt) performUpdateRevolt ;; frappe-hr) performUpdateFrappeHR ;; minthcm) performUpdateMintHCM ;; twenty) performUpdateTwenty ;; odoo) performUpdateOdoo ;; calcom) performUpdateCalcom ;; rallly) performUpdateRallly ;; easyappointments) performUpdateEasyAppointments ;; openproject) performUpdateOpenProject ;; zammad) performUpdateZammad ;; zulip) performUpdateZulip ;; killbill) performUpdateKillBill ;; invoiceshelf) performUpdateInvoiceShelf ;; invoiceninja) performUpdateInvoiceNinja ;; dolibarr) performUpdateDolibarr ;; n8n) performUpdaten8n ;; automatisch) performUpdateAutomatisch ;; activepieces) performUpdateActivePieces ;; heimdall) performUpdateHeimdall ;; ofelia) performUpdateOfelia ;; script-server) performUpdateScriptServer ;; cloudbeaver) performUpdateCloudBeaver ;; dbgate) performUpdateDbGate ;; sqlpad) performUpdateSQLPad ;; uptimekuma) performUpdateUptimeKuma ;; caddy-*) performUpdateCaddy "$stack_name" ;; clientdns-*) performUpdateClientDNS "$stack_name" ;; beszel) performUpdateBeszel ;; taiga) performUpdateTaiga ;; opensign) performUpdateOpenSign ;; docuseal) performUpdateDocuSeal ;; controlr) performUpdateControlR ;; akaunting) performUpdateAkaunting ;; axelor) performUpdateAxelor ;; convertx) performUpdateConvertX ;; kopia) performUpdateKopia ;; localai) performUpdateLocalAI ;; comfyui) performUpdateComfyUI ;; langflow) performUpdateLangflow ;; anythingllm) performUpdateAnythingLLM ;; perplexica) performUpdatePerplexica ;; firecrawl) performUpdateFirecrawl ;; librechat) performUpdateLibreChat ;; crawl4ai) performUpdateCrawl4AI ;; ollama) performUpdateOllama ;; openwebui) performUpdateOpenWebUI ;; khoj) performUpdateKhoj ;; lobechat) performUpdateLobeChat ;; invokeai) performUpdateInvokeAI ;; ragflow) performUpdateRAGFlow ;; tabbyml) performUpdateTabbyML ;; deepwikiopen) performUpdateDeepWikiOpen ;; docling) performUpdateDocling ;; dify) performUpdateDify ;; mindsdb) performUpdateMindsDB ;; watercrawl) performUpdateWaterCrawl ;; flowise) performUpdateFlowise ;; surfsense) performUpdateSurfSense ;; nocodb) performUpdateNocoDB ;; ente) performUpdateEnte ;; morphic) performUpdateMorphic ;; opennotebook) performUpdateOpenNotebook ;; appsmith) performUpdateAppsmith ;; trilium) performUpdateTrilium ;; docsgpt) performUpdateDocsGPT ;; memos) performUpdateMemos ;; sillytavern) performUpdateSillyTavern ;; lemonade) performUpdateLemonade ;; speakr) performUpdateSpeakr ;; insanelyfastwhisper) performUpdateInsanelyFastWhisper ;; ivbox) performUpdateIVBox ;; monica) performUpdateMonica ;; affine) performUpdateAFFiNE ;; joplin) performUpdateJoplin ;; superset) performUpdateSuperset ;; kokoro) performUpdateKokoro ;; chatterbox) performUpdateChatterbox ;; litellm) performUpdateLiteLLM ;; langfuse) performUpdateLangfuse ;; skyvern) performUpdateSkyvern ;; wger) performUpdateWger ;; workoutcool) performUpdateWorkoutCool ;; openrag) performUpdateOpenRag ;; voicebox) performUpdateVoicebox ;; opencode) performUpdateOpenCode ;; openskills) performUpdateOpenSkills ;; emailclassifierai) performUpdateEmailClassifierAI ;; hermes-agent) performUpdateHermesAgent ;; autokb) performUpdateAutoKB ;; suitecrm) performUpdateSuiteCRM ;; hedgedoc) performUpdateHedgeDoc ;; presenton) performUpdatePresenton ;; basicmemory) performUpdateBasicMemory ;; cognee) performUpdateCognee ;; lightrag) performUpdateLightRAG ;; openserp) performUpdateOpenSERP ;; suricata) performUpdateSuricata ;; crowdsec) performUpdateCrowdSec ;; fastcrw) performUpdatefastCRW ;; whiteboardrt) performUpdateWhiteboardRT ;; mdcollab) performUpdatemdCollab ;; halospeak) performUpdateHaloSpeak ;; #ADD_NEW_PERFORM_UPDATE_STACK_HERE esac } function getAutheliaBlock() { retval="" retval="${retval}access_control:\n" retval="${retval} default_policy: deny\n" retval="${retval} rules:\n" retval="${retval} - domain:\n" retval="${retval}# Authelia bypass BEGIN\n" retval="${retval} - $SUB_ACTIVEPIECES_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AUTHELIA.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_BUDIBASE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CALCOM_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_COLLABORA.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DRAWIO_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ESPOCRM.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_EXCALIDRAW_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FILEBROWSER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FILEDROP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FRESHRSS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_GITEA.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_GRAMPSWEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HEIMDALL.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HOMARR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HSHQHOME.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HSHQSTATUS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HOMEASSISTANT_CONFIGURATOR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HOMEASSISTANT_NODERED.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_IMAGES.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_IMMICH_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_INVOICENINJA_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_JELLYFIN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_JITSI.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KASM.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KANBOARD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KEILA.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KILLBILL_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MAILU.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MASTODON.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MATRIX_SYNAPSE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MATRIX_ELEMENT_PRIVATE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MATRIX_ELEMENT_PUBLIC.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MEALIE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_N8N_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_NAVIDROME.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_NEXTCLOUD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_NCTALKHPB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_NCTALKRECORD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_NTFY.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ODOO.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OMBI_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PEERTUBE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PENPOT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PIPED_PROXY.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PIPED_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PIXELFED.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_RALLLY.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_REMOTELY.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_REVOLT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SHLINK_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_STANDARDNOTES_FILES.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_VAULTWARDEN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WALLABAG.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WEKAN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WORDPRESS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_YAMTRACK.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ZULIP_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_BESZEL_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_METABASE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CONTROLR_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AKAUNTING_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LOCALAI_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_RAGFLOW_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_RAGFLOW_MCP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OLLAMA_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENCODE_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_COMFYUI_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DEEPWIKI_OPEN_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DOCLING_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SURFSENSE_BACKEND.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ENTE_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DOCSGPT_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LEMONADE_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_INSANELYFASTWHISPER_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_IVBOX_COMFYUI_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_JOPLIN_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_JOPLIN_TRANSCRIBE_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AFFINE_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KOKORO_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CHATTERBOX_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LITELLM_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SKYVERN_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SKYVERN_ARTIFACT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HERMES_AGENT_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_BASICMEMORY_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_COGNEE_APP.$HOMESERVER_DOMAIN\n" #ADD_NEW_AUTHELIA_BYPASS_HERE retval="${retval}# Authelia bypass END\n" retval="${retval} policy: bypass\n" retval="${retval} - domain:\n" retval="${retval}# Authelia $LDAP_BASIC_USER_GROUP_NAME BEGIN\n" retval="${retval} - $SUB_DISCOURSE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PHOTOPRISM.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_INVIDIOUS.$HOMESERVER_DOMAIN\n" retval="${retval}# Authelia $LDAP_BASIC_USER_GROUP_NAME END\n" retval="${retval} policy: one_factor\n" retval="${retval} subject:\n" retval="${retval} - \"group:$LDAP_BASIC_USER_GROUP_NAME\"\n" retval="${retval} - domain:\n" retval="${retval}# Authelia ${LDAP_PRIMARY_USER_GROUP_NAME} BEGIN\n" retval="${retval} - $SUB_BARASSISTANT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CHANGEDETECTION.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FRAPPE_HR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_GITLAB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HUGINN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KILLBILL_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LINKWARDEN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MESHCENTRAL.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MINTHCM.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PASTEFY.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PIPED_FRONTEND.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SNIPPETBOX.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_STANDARDNOTES_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_STIRLINGPDF.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SEARXNG.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_TWENTY.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_TAIGA_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AXELOR_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CONVERTX_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LOCALAI_RECALL.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LOCALAI_AGI.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_COMFYUI_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LANGFLOW_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PERPLEXICA_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FIRECRAWL_API.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CRAWL4AI_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_INVOKEAI_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DIFY_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FLOWISE_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SURFSENSE_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_NOCODB_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ENTE_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MORPHIC_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DRAWIO_NEXTAI.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENNOTEBOOK_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_APPSMITH_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_TRILIUM_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DOCSGPT_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MEMOS_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SILLYTAVERN_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SPEAKR_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_INSANELYFASTWHISPER_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_IVBOX_QWEN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_IVBOX_COMFYUI_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MONICA_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KOKORO_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CHATTERBOX_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LANGFUSE_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LANGFUSE_CLICKHOUSE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LANGFUSE_MINIO.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SKYVERN_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SKYVERN_BROWSER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WGER_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WORKOUTCOOL_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENRAG_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENRAG_LANGFLOW.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_VOICEBOX_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENCODE_CODESERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SUITECRM_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HEDGEDOC_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PRESENTON_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FASTCRW_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WHITEBOARDRT_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WHITEBOARDRT_AGENT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MDCOLLAB_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HALOSPEAK_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HALOSPEAK_API.$HOMESERVER_DOMAIN\n" #ADD_NEW_AUTHELIA_PRIMARY_HERE retval="${retval}# Authelia ${LDAP_PRIMARY_USER_GROUP_NAME} END\n" retval="${retval} policy: one_factor\n" retval="${retval} subject:\n" retval="${retval} - \"group:$LDAP_PRIMARY_USER_GROUP_NAME\"\n" retval="${retval} - domain:\n" retval="${retval}# Authelia ${LDAP_ADMIN_USER_GROUP_NAME} BEGIN\n" retval="${retval} - $SUB_ADGUARD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ADMINER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CALCOM_STUDIO.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CALIBRE_SERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - ${SUB_CLIENTDNS}-user1.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CLOUDBEAVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CODESERVER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DBGATE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DOZZLE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DUPLICATI.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FIREFLY_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FIREFLY_IMPORTER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_FIREFLY_FINTSIMPORTER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_GHOST.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_GRAFANA.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_GUACAMOLE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_INFLUXDB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ITTOOLS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_JUPYTER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KASM_WIZARD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MATOMO.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_NETDATA.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENLDAP_PHP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PORTAINER.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_PROMETHEUS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_QBITTORRENT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SABNZBD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SERVARR_SONARR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SERVARR_RADARR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SERVARR_LIDARR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SERVARR_READARR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SERVARR_BAZARR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SERVARR_MYLAR3.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SERVARR_PROWLARR.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SHLINK_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SPEEDTEST_TRACKER_LOCAL.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SPEEDTEST_TRACKER_VPN.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SQLPAD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SYNCTHING.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_UPTIMEKUMA.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WAZUH.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_WIKIJS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CONTROLR_ASPIRE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_KOPIA_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_TABBYML_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_DEEPWIKI_OPEN_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MINDSDB_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_RAGFLOW_RUSTFS.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LEMONADE_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SUPERSET_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENRAG_OPENSEARCH.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENSKILLS_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_EMAILCLASSIFIERAI_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HERMES_AGENT_DASHBOARD.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_HERMES_AGENT_WEBUI.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_AUTOKB_WEB.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_COGNEE_FRONTEND.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LIGHTRAG_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_LIGHTRAG_QDRANT.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_ALLOY.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_SURICATA_APP.$HOMESERVER_DOMAIN\n" retval="${retval} - $SUB_CROWDSEC_WEB.$HOMESERVER_DOMAIN\n" #ADD_NEW_AUTHELIA_ADMIN_HERE retval="${retval}# Authelia ${LDAP_ADMIN_USER_GROUP_NAME} END\n" retval="${retval} policy: one_factor\n" retval="${retval} subject:\n" retval="${retval} - \"group:$LDAP_ADMIN_USER_GROUP_NAME\"\n" echo "$retval" } function emailVaultwardenCredentials() { strOutput="_________________________________________________________________________\n\n" strOutput=$strOutput"folder,favorite,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PORTAINER}" "\"https://$SUB_PORTAINER.$HOMESERVER_DOMAIN/#!/auth,https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$PORTAINER_LOCAL_HTTPS_PORT/#!/auth\"" $HOMESERVER_ABBREV $PORTAINER_ADMIN_USERNAME $PORTAINER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ADGUARD}" https://$SUB_ADGUARD.$HOMESERVER_DOMAIN/login.html $HOMESERVER_ABBREV $ADGUARD_ADMIN_USERNAME $ADGUARD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SCRIPTSERVER}" "\"https://$SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN/login.html,https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$SCRIPTSERVER_LOCALHOST_PORT/login.html\"" $HOMESERVER_ABBREV $SCRIPTSERVER_ADMIN_USERNAME $SCRIPTSERVER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENLDAP_PHP}" https://$SUB_OPENLDAP_PHP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV \"$LDAP_ADMIN_BIND_DN\" $LDAP_ADMIN_BIND_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AUTHELIA}" https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WAZUH}" https://$SUB_WAZUH.$HOMESERVER_DOMAIN/app/login $HOMESERVER_ABBREV $WAZUH_USERS_ADMIN_USERNAME $WAZUH_USERS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_GRAFANA}" https://$SUB_GRAFANA.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $GRAFANA_ADMIN_USERNAME $GRAFANA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ALLOY}" https://$SUB_ALLOY.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $ALLOY_ADMIN_USERNAME $ALLOY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_INFLUXDB}" https://$SUB_INFLUXDB.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $INFLUXDB_ADMIN_USERNAME $INFLUXDB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DOZZLE}" https://$SUB_DOZZLE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $DOZZLE_USERNAME $DOZZLE_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_JELLYFIN}-Admin" "\"https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN/web/#/login,https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN/web/#/wizard/user\"" $HOMESERVER_ABBREV $JELLYFIN_ADMIN_USERNAME $JELLYFIN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_JELLYFIN}-User" https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN/web/#/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PHOTOPRISM}-Admin" https://$SUB_PHOTOPRISM.$HOMESERVER_DOMAIN/library/login $HOMESERVER_ABBREV $PHOTOPRISM_ADMIN_USERNAME $PHOTOPRISM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_GUACAMOLE}" https://$SUB_GUACAMOLE.$HOMESERVER_DOMAIN/guacamole/ $HOMESERVER_ABBREV $GUACAMOLE_DEFAULT_ADMIN_USERNAME $GUACAMOLE_DEFAULT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WORDPRESS}" https://$SUB_WORDPRESS.$HOMESERVER_DOMAIN/wp-login $HOMESERVER_ABBREV $WORDPRESS_ADMIN_USERNAME $WORDPRESS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WIKIJS}" https://$SUB_WIKIJS.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $WIKIJS_ADMIN_EMAIL_ADDRESS $WIKIJS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_UPTIMEKUMA}" https://$SUB_UPTIMEKUMA.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $UPTIMEKUMA_USERNAME $UPTIMEKUMA_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SQLPAD}" https://$SUB_SQLPAD.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $SQLPAD_ADMIN_USERNAME $SQLPAD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SYNCTHING}" https://$SUB_SYNCTHING.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $SYNCTHING_ADMIN_USERNAME $SYNCTHING_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CODESERVER}" https://$SUB_CODESERVER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CODESERVER_ADMIN_USERNAME $CODESERVER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HOMEASSISTANT_TASMOADMIN}" "\"https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN/tasmoadmin,https://$SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN/login\"" $HOMESERVER_ABBREV $HOMEASSISTANT_TASMOADMIN_USER $HOMEASSISTANT_TASMOADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HOMEASSISTANT_CONFIGURATOR}" "\"https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN/configurator,https://$SUB_HOMEASSISTANT_CONFIGURATOR.$HOMESERVER_DOMAIN/\"" $HOMESERVER_ABBREV $HOMEASSISTANT_CONFIGURATOR_USER $HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HEIMDALL}-Admin" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_ADMIN_USERNAME $HEIMDALL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HEIMDALL}-Users" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_USER_USERNAME $HEIMDALL_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HEIMDALL}-RemoteServers" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_HOMESERVERS_USERNAME $HEIMDALL_HOMESERVERS_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HEIMDALL}-RelayServer" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_RELAYSERVER_USERNAME $HEIMDALL_RELAYSERVER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_GITEA}-Admin" https://$SUB_GITEA.$HOMESERVER_DOMAIN/user/login $HOMESERVER_ABBREV $GITEA_ADMIN_USERNAME $GITEA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_GITEA}-User" https://$SUB_GITEA.$HOMESERVER_DOMAIN/user/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_GITLAB}" https://$SUB_GITLAB.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENLDAP_MANAGER}" https://$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN/log_in/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MAILU}-Admin" https://$SUB_MAILU.$HOMESERVER_DOMAIN/sso/login $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $EMAIL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MAILU}-Shared" https://$SUB_MAILU.$HOMESERVER_DOMAIN/sso/login $HOMESERVER_ABBREV $EMAIL_JOINT_EMAIL_ADDRESS $EMAIL_JOINT_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MATRIX_ELEMENT_PRIVATE}" https://$SUB_MATRIX_ELEMENT_PRIVATE.$HOMESERVER_DOMAIN/#/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MATRIX_ELEMENT_PUBLIC}" https://$SUB_MATRIX_ELEMENT_PUBLIC.$HOMESERVER_DOMAIN/#/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MEALIE}-Admin" https://$SUB_MEALIE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MEALIE_ADMIN_USERNAME $MEALIE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MEALIE}-User" https://$SUB_MEALIE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_KASM}-Admin" "\"https://$SUB_KASM.$HOMESERVER_DOMAIN/#/login,https://$SUB_KASM_WIZARD.$HOMESERVER_DOMAIN\"" $HOMESERVER_ABBREV $KASM_ADMIN_EMAIL_ADDRESS $KASM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_KASM}-User" "\"https://$SUB_KASM.$HOMESERVER_DOMAIN/#/login,https://$SUB_KASM_WIZARD.$HOMESERVER_DOMAIN\"" $HOMESERVER_ABBREV $KASM_USER_EMAIL_ADDRESS $KASM_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_REMOTELY}" "\"https://$SUB_REMOTELY.$HOMESERVER_DOMAIN/Account/Register,https://$SUB_REMOTELY.$HOMESERVER_DOMAIN/Account/Login\"" $HOMESERVER_ABBREV $REMOTELY_ADMIN_EMAIL_ADDRESS $REMOTELY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DUPLICATI}" "\"https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/login.html,https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/ngclient/login\"" $HOMESERVER_ABBREV "NA" $DUPLICATI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_FILEBROWSER}" https://$SUB_FILEBROWSER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $FILEBROWSER_USERNAME $FILEBROWSER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MASTODON}-Admin" https://$SUB_MASTODON.$HOMESERVER_DOMAIN/auth/sign_in $HOMESERVER_ABBREV $MASTODON_ADMIN_EMAIL_ADDRESS $MASTODON_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MASTODON}-User" https://$SUB_MASTODON.$HOMESERVER_DOMAIN/auth/sign_in $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PEERTUBE}-Admin" https://$SUB_PEERTUBE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PEERTUBE_ADMIN_USERNAME $PEERTUBE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PEERTUBE}-User" https://$SUB_PEERTUBE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_COLLABORA}-Admin" https://$SUB_COLLABORA.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $COLLABORA_ADMIN_USERNAME $COLLABORA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_NEXTCLOUD}-Admin" https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $NEXTCLOUD_ADMIN_USERNAME $NEXTCLOUD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_NEXTCLOUD}-User" https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DISCOURSE}-Admin" https://$SUB_DISCOURSE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $DISCOURSE_ADMIN_USERNAME $DISCOURSE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_VAULTWARDEN}-Admin" https://$SUB_VAULTWARDEN.$HOMESERVER_DOMAIN/admin $HOMESERVER_ABBREV admin $VAULTWARDEN_ADMIN_TOKEN)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CALIBRE_WEB}-Admin" https://$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CALIBRE_WEB_ADMIN_USERNAME $CALIBRE_WEB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CALIBRE_WEB}-User" https://$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_FRESHRSS}-Admin" https://$SUB_FRESHRSS.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $FRESHRSS_ADMIN_USERNAME $FRESHRSS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_KEILA}-Admin" https://$SUB_KEILA.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $KEILA_ADMIN_EMAIL_ADDRESS $KEILA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WALLABAG}-Admin" https://$SUB_WALLABAG.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $WALLABAG_ADMIN_USERNAME $WALLABAG_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_JUPYTER}-Admin" https://$SUB_JUPYTER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV admin $JUPYTER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PAPERLESS_APP}-Admin" https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN/accounts/login/ $HOMESERVER_ABBREV $PAPERLESS_ADMIN_USERNAME $PAPERLESS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PAPERLESS_AI_WEB}-Admin" https://$SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PAPERLESS_AI_ADMIN_USERNAME $PAPERLESS_AI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PAPERLESS_GPT_WEB}-Admin" https://$SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PAPERLESS_GPT_ADMIN_USERNAME $PAPERLESS_GPT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SPEEDTEST_TRACKER_LOCAL}-Admin" https://$SUB_SPEEDTEST_TRACKER_LOCAL.$HOMESERVER_DOMAIN/admin/login $HOMESERVER_ABBREV $SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS $SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SPEEDTEST_TRACKER_VPN}-Admin" https://$SUB_SPEEDTEST_TRACKER_VPN.$HOMESERVER_DOMAIN/admin/login $HOMESERVER_ABBREV $SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS $SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CHANGEDETECTION}-Admin" https://$SUB_CHANGEDETECTION.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV admin $CHANGEDETECTION_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HUGINN}-Admin" https://$SUB_HUGINN.$HOMESERVER_DOMAIN/users/sign_in $HOMESERVER_ABBREV $HUGINN_ADMIN_USERNAME $HUGINN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_GRAMPSWEB}-Admin" https://$SUB_GRAMPSWEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $GRAMPSWEB_ADMIN_USERNAME $GRAMPSWEB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PENPOT}-User" https://$SUB_PENPOT.$HOMESERVER_DOMAIN/#/auth/login $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ESPOCRM}-Admin" https://$SUB_ESPOCRM.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $ESPOCRM_ADMIN_USERNAME $ESPOCRM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_IMMICH_APP}-Admin" https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $IMMICH_ADMIN_EMAIL_ADDRESS $IMMICH_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HOMARR}-Admin" https://$SUB_HOMARR.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $HOMARR_ADMIN_USERNAME $HOMARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MATOMO}-Admin" https://$SUB_MATOMO.$HOMESERVER_DOMAIN/index.php $HOMESERVER_ABBREV $MATOMO_ADMIN_USERNAME $MATOMO_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PASTEFY}-Admin" https://$SUB_PASTEFY.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $PASTEFY_ADMIN_USERNAME $PASTEFY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PIXELFED}-Admin" "\"https://$SUB_PIXELFED.$HOMESERVER_DOMAIN/login,https://$SUB_PIXELFED.$HOMESERVER_DOMAIN/i/auth/sudo\"" $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_YAMTRACK}-Admin" https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN/accounts/login $HOMESERVER_ABBREV $YAMTRACK_ADMIN_USERNAME $YAMTRACK_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SERVARR_SONARR}-Admin" https://$SUB_SERVARR_SONARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SONARR_ADMIN_USERNAME $SONARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SERVARR_RADARR}-Admin" https://$SUB_SERVARR_RADARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $RADARR_ADMIN_USERNAME $RADARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SERVARR_LIDARR}-Admin" https://$SUB_SERVARR_LIDARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LIDARR_ADMIN_USERNAME $LIDARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SERVARR_READARR}-Admin" https://$SUB_SERVARR_READARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $READARR_ADMIN_USERNAME $READARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SERVARR_BAZARR}-Admin" https://$SUB_SERVARR_BAZARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $BAZARR_ADMIN_USERNAME $BAZARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SERVARR_MYLAR3}-Admin" https://$SUB_SERVARR_MYLAR3.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $MYLAR3_ADMIN_USERNAME $MYLAR3_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SERVARR_PROWLARR}-Admin" https://$SUB_SERVARR_PROWLARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PROWLARR_ADMIN_USERNAME $PROWLARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SABNZBD}-Admin" https://$SUB_SABNZBD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SABNZBD_ADMIN_USERNAME $SABNZBD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_QBITTORRENT}-Admin" https://$SUB_QBITTORRENT.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $QBITTORRENT_ADMIN_USERNAME $QBITTORRENT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OMBI_APP}-Admin" "\"https://$SUB_OMBI_APP.$HOMESERVER_DOMAIN/login,https://$SUB_OMBI_APP.$HOMESERVER_DOMAIN/Wizard\"" $HOMESERVER_ABBREV $OMBI_ADMIN_USERNAME $OMBI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MESHCENTRAL}-Admin" https://$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_NAVIDROME}-Admin" https://$SUB_NAVIDROME.$HOMESERVER_DOMAIN/app/#/login $HOMESERVER_ABBREV $NAVIDROME_ADMIN_USERNAME $NAVIDROME_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_BUDIBASE}-Admin" https://$SUB_BUDIBASE.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $BUDIBASE_ADMIN_EMAIL_ADDRESS $BUDIBASE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AUDIOBOOKSHELF}-Admin" https://$SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN/audiobookshelf/login $HOMESERVER_ABBREV $AUDIOBOOKSHELF_ADMIN_USERNAME $AUDIOBOOKSHELF_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_METABASE}-Admin" https://$SUB_METABASE.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $METABASE_ADMIN_EMAIL_ADDRESS $METABASE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_KANBOARD}-Admin" https://$SUB_KANBOARD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $KANBOARD_ADMIN_USERNAME $KANBOARD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_FRAPPE_HR}-Admin" https://$SUB_FRAPPE_HR.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $FRAPPE_HR_ADMIN_USERNAME $FRAPPE_HR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MINTHCM}-Admin" https://$SUB_MINTHCM.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $MINTHCM_ADMIN_USERNAME $MINTHCM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CLOUDBEAVER}-Admin" https://$SUB_CLOUDBEAVER.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $CLOUDBEAVER_ADMIN_USERNAME $CLOUDBEAVER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DBGATE}-Admin" https://$SUB_DBGATE.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $DBGATE_ADMIN_USERNAME $DBGATE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_TWENTY}-Admin" https://$SUB_TWENTY.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $TWENTY_ADMIN_EMAIL_ADDRESS $TWENTY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ODOO}-Admin" https://$SUB_ODOO.$HOMESERVER_DOMAIN/web/login $HOMESERVER_ABBREV $ODOO_ADMIN_USERNAME $ODOO_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CALCOM_APP}-Admin" https://$SUB_CALCOM_APP.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $CALCOM_ADMIN_EMAIL_ADDRESS $CALCOM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_RALLLY}-Admin" https://$SUB_RALLLY.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $RALLLY_ADMIN_EMAIL_ADDRESS $RALLLY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_EASYAPPOINTMENTS}-Admin" https://$SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN/index.php/login $HOMESERVER_ABBREV $EASYAPPOINTMENTS_ADMIN_USERNAME $EASYAPPOINTMENTS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENPROJECT_APP}-Admin" https://$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENPROJECT_ADMIN_USERNAME $OPENPROJECT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ZAMMAD_APP}-Admin" https://$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN/#login $HOMESERVER_ABBREV $ZAMMAD_ADMIN_EMAIL_ADDRESS $ZAMMAD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ZULIP_APP}-Admin" "\"https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN/login/,https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN/accounts/login/\"" $HOMESERVER_ABBREV $ZULIP_ADMIN_EMAIL_ADDRESS $ZULIP_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_KILLBILL_WEB}-Admin" https://$SUB_KILLBILL_WEB.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $KILLBILL_ADMIN_USERNAME $KILLBILL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_INVOICESHELF_APP}-Admin" https://$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $INVOICESHELF_ADMIN_EMAIL_ADDRESS $INVOICESHELF_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_INVOICENINJA_WEB}-Admin" https://$SUB_INVOICENINJA_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $INVOICENINJA_ADMIN_EMAIL_ADDRESS $INVOICENINJA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DOLIBARR_APP}-Admin" https://$SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $DOLIBARR_ADMIN_USERNAME $DOLIBARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_N8N_APP}-Admin" https://$SUB_N8N_APP.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $N8N_ADMIN_EMAIL_ADDRESS $N8N_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AUTOMATISCH_APP}-Admin" https://$SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $AUTOMATISCH_ADMIN_EMAIL_ADDRESS $AUTOMATISCH_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ACTIVEPIECES_APP}-Admin" https://$SUB_ACTIVEPIECES_APP.$HOMESERVER_DOMAIN/sign-in $HOMESERVER_ABBREV $ACTIVEPIECES_ADMIN_EMAIL_ADDRESS $ACTIVEPIECES_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_BESZEL_APP}-Admin" https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $BESZEL_ADMIN_EMAIL_ADDRESS $BESZEL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_TAIGA_APP}-Admin" https://$SUB_TAIGA_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $TAIGA_ADMIN_USERNAME $TAIGA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENSIGN_APP}-Admin" https://$SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $OPENSIGN_ADMIN_EMAIL_ADDRESS $OPENSIGN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DOCUSEAL_APP}-Admin" https://$SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN/sign_in $HOMESERVER_ABBREV $DOCUSEAL_ADMIN_EMAIL_ADDRESS $DOCUSEAL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CONTROLR_APP}-Admin" "\"https://$SUB_CONTROLR_APP.$HOMESERVER_DOMAIN/Account/Login,https://$SUB_CONTROLR_APP.$HOMESERVER_DOMAIN/Account/Register\"" $HOMESERVER_ABBREV $CONTROLR_ADMIN_EMAIL_ADDRESS $CONTROLR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CONTROLR_ASPIRE}-Admin" https://$SUB_CONTROLR_ASPIRE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CONTROLR_ADMIN_EMAIL_ADDRESS $CONTROLR_ASPIRE_TOKEN)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AKAUNTING_APP}-Admin" https://$SUB_AKAUNTING_APP.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $AKAUNTING_ADMIN_EMAIL_ADDRESS $AKAUNTING_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AXELOR_APP}-Admin" https://$SUB_AXELOR_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $AXELOR_ADMIN_USERNAME $AXELOR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AXELOR_GOOVEE}-Admin" https://$SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $AXELOR_GOOVEE_ADMIN_USERNAME $AXELOR_GOOVEE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CONVERTX_APP}-Admin" https://$SUB_CONVERTX_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CONVERTX_ADMIN_EMAIL_ADDRESS $CONVERTX_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_KOPIA_APP}-Admin" https://$SUB_KOPIA_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $KOPIA_ADMIN_USERNAME $KOPIA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LOCALAI_SERVER}-Admin" https://$SUB_LOCALAI_SERVER.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $LOCALAI_ADMIN_USERNAME $LOCALAI_API_KEY)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LANGFLOW_APP}-Admin" https://$SUB_LANGFLOW_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LANGFLOW_ADMIN_USERNAME $LANGFLOW_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ANYTHINGLLM_APP}-Admin" "\"https://$SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN/login,https://$SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN/onboarding/user-setup\"" $HOMESERVER_ABBREV $ANYTHINGLLM_ADMIN_USERNAME $ANYTHINGLLM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LIBRECHAT_APP}-Admin" https://$SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LIBRECHAT_ADMIN_EMAIL_ADDRESS $LIBRECHAT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENWEBUI_APP}-Admin" https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/auth $HOMESERVER_ABBREV $OPENWEBUI_ADMIN_EMAIL_ADDRESS $OPENWEBUI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENWEBUI_QDRANT}-Admin" https://$SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $OPENWEBUI_ADMIN_EMAIL_ADDRESS $OPENWEBUI_QDRANT_API_KEY)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_KHOJ_SERVER}-Admin" https://$SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $KHOJ_ADMIN_EMAIL_ADDRESS $KHOJ_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LOBECHAT_APP}-Admin" https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LOBECHAT_ADMIN_USERNAME $LOBECHAT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_RAGFLOW_APP}-Admin" https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN/admin $HOMESERVER_ABBREV $RAGFLOW_ADMIN_EMAIL_ADDRESS $RAGFLOW_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_RAGFLOW_APP}-TeamAdmin" https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $EMAIL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_RAGFLOW_RUSTFS}-Admin" https://$SUB_RAGFLOW_RUSTFS.$HOMESERVER_DOMAIN/rustfs/console/auth/login/ $HOMESERVER_ABBREV $RAGFLOW_MINIO_KEY $RAGFLOW_MINIO_SECRET)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_TABBYML_APP}-Admin" https://$SUB_TABBYML_APP.$HOMESERVER_DOMAIN/auth/signin $HOMESERVER_ABBREV $TABBYML_ADMIN_EMAIL_ADDRESS $TABBYML_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DIFY_APP}-Admin" https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $DIFY_ADMIN_EMAIL_ADDRESS $DIFY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MINDSDB_APP}-Admin" https://$SUB_MINDSDB_APP.$HOMESERVER_DOMAIN/local-login $HOMESERVER_ABBREV $MINDSDB_ADMIN_USERNAME $MINDSDB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WATERCRAWL_APP}-Admin" https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $WATERCRAWL_ADMIN_EMAIL_ADDRESS $WATERCRAWL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_FLOWISE_APP}-Admin" https://$SUB_FLOWISE_APP.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $FLOWISE_ADMIN_EMAIL_ADDRESS $FLOWISE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SURFSENSE_APP}-Admin" https://$SUB_SURFSENSE_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SURFSENSE_ADMIN_EMAIL_ADDRESS $SURFSENSE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_NOCODB_APP}-Admin" https://$SUB_NOCODB_APP.$HOMESERVER_DOMAIN/dashboard/#/signin $HOMESERVER_ABBREV $NOCODB_ADMIN_EMAIL_ADDRESS $NOCODB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ENTE_APP}-Admin" https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $ENTE_ADMIN_USERNAME $ENTE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MORPHIC_APP}-Admin" https://$SUB_MORPHIC_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MORPHIC_ADMIN_USERNAME $MORPHIC_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENNOTEBOOK_APP}-Admin" https://$SUB_OPENNOTEBOOK_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENNOTEBOOK_ADMIN_USERNAME $OPENNOTEBOOK_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_APPSMITH_APP}-Admin" "\"https://$SUB_APPSMITH_APP.$HOMESERVER_DOMAIN/user/login,https://$SUB_APPSMITH_APP.$HOMESERVER_DOMAIN/setup/welcome\"" $HOMESERVER_ABBREV $APPSMITH_ADMIN_EMAIL_ADDRESS $APPSMITH_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_TRILIUM_APP}-Admin" "\"https://$SUB_TRILIUM_APP.$HOMESERVER_DOMAIN/login,https://$SUB_TRILIUM_APP.$HOMESERVER_DOMAIN/set-password\"" $HOMESERVER_ABBREV $TRILIUM_ADMIN_USERNAME $TRILIUM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_DOCSGPT_APP}-Admin" https://$SUB_DOCSGPT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $DOCSGPT_ADMIN_USERNAME $DOCSGPT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MEMOS_APP}-Admin" https://$SUB_MEMOS_APP.$HOMESERVER_DOMAIN/auth $HOMESERVER_ABBREV $MEMOS_ADMIN_USERNAME $MEMOS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SPEAKR_APP}-Admin" https://$SUB_SPEAKR_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SPEAKR_ADMIN_EMAIL_ADDRESS $SPEAKR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MONICA_APP}-Admin" https://$SUB_MONICA_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MONICA_ADMIN_EMAIL_ADDRESS $MONICA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AFFINE_APP}-Admin" "\"https://$SUB_AFFINE_APP.$HOMESERVER_DOMAIN/sign-in,https://$SUB_AFFINE_APP.$HOMESERVER_DOMAIN/admin/auth\"" $HOMESERVER_ABBREV $AFFINE_ADMIN_EMAIL_ADDRESS $AFFINE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_JOPLIN_APP}-Admin" https://$SUB_JOPLIN_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $JOPLIN_ADMIN_EMAIL_ADDRESS $JOPLIN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SUPERSET_APP}-Admin" https://$SUB_SUPERSET_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SUPERSET_ADMIN_USERNAME $SUPERSET_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LITELLM_APP}-Admin" https://$SUB_LITELLM_APP.$HOMESERVER_DOMAIN/ui/login/ $HOMESERVER_ABBREV $LITELLM_ADMIN_USERNAME $LITELLM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LANGFUSE_APP}-Admin" https://$SUB_LANGFUSE_APP.$HOMESERVER_DOMAIN/auth/sign-in $HOMESERVER_ABBREV $LANGFUSE_ADMIN_EMAIL_ADDRESS $LANGFUSE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LANGFUSE_CLICKHOUSE}-Admin" https://$SUB_LANGFUSE_CLICKHOUSE.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $LANGFUSE_CLICKHOUSE_USER $LANGFUSE_CLICKHOUSE_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LANGFUSE_MINIO}-Admin" https://$SUB_LANGFUSE_MINIO.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LANGFUSE_MINIO_KEY $LANGFUSE_MINIO_SECRET)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SKYVERN_API}-Admin" https://$SUB_SKYVERN_API.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SKYVERN_ADMIN_USERNAME $SKYVERN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WGER_APP}-Admin" https://$SUB_WGER_APP.$HOMESERVER_DOMAIN/en/user/login $HOMESERVER_ABBREV $WGER_ADMIN_USERNAME $WGER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WORKOUTCOOL_APP}-Admin" https://$SUB_WORKOUTCOOL_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $WORKOUTCOOL_ADMIN_USERNAME $WORKOUTCOOL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENRAG_APP}-Admin" https://$SUB_OPENRAG_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENRAG_ADMIN_USERNAME $OPENRAG_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_VOICEBOX_APP}-Admin" https://$SUB_VOICEBOX_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $VOICEBOX_ADMIN_USERNAME $VOICEBOX_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENCODE_APP}-Admin" https://$SUB_OPENCODE_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $OPENCODE_ADMIN_USERNAME $OPENCODE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_OPENCODE_CODESERVER}-Admin" https://$SUB_OPENCODE_CODESERVER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENCODE_CODESERVER_USERNAME $OPENCODE_CODESERVER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_EMAILCLASSIFIERAI_APP}-Admin" https://$SUB_EMAILCLASSIFIERAI_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $EMAILCLASSIFIERAI_ADMIN_USERNAME $EMAILCLASSIFIERAI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HERMES_AGENT_DASHBOARD}-Admin" https://$SUB_HERMES_AGENT_DASHBOARD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HERMES_AGENT_ADMIN_USERNAME $HERMES_AGENT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HERMES_AGENT_WEBUI}-Admin" https://$SUB_HERMES_AGENT_WEBUI.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HERMES_AGENT_ADMIN_USERNAME $HERMES_AGENT_WEBUI_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_AUTOKB_WEB}-Admin" https://$SUB_AUTOKB_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $AUTOKB_ADMIN_USERNAME $AUTOKB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SUITECRM_APP}-Admin" https://$SUB_SUITECRM_APP.$HOMESERVER_DOMAIN/#/Login $HOMESERVER_ABBREV $SUITECRM_ADMIN_USERNAME $SUITECRM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HEDGEDOC_APP}-Admin" https://$SUB_HEDGEDOC_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEDGEDOC_ADMIN_USERNAME $HEDGEDOC_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_HEDGEDOC_APP}-User" https://$SUB_HEDGEDOC_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PRESENTON_APP}-Admin" https://$SUB_PRESENTON_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $PRESENTON_ADMIN_USERNAME $PRESENTON_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_BASICMEMORY_APP}-Admin" https://$SUB_BASICMEMORY_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $BASICMEMORY_ADMIN_USERNAME $BASICMEMORY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_COGNEE_FRONTEND}-Admin" https://$SUB_COGNEE_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $COGNEE_ADMIN_USERNAME $COGNEE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LIGHTRAG_APP}-Admin" https://$SUB_LIGHTRAG_APP.$HOMESERVER_DOMAIN/webui/#/login $HOMESERVER_ABBREV $LIGHTRAG_ADMIN_USERNAME $LIGHTRAG_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_LIGHTRAG_QDRANT}-Admin" https://$SUB_LIGHTRAG_QDRANT.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $LIGHTRAG_ADMIN_EMAIL_ADDRESS $LIGHTRAG_QDRANT_API_KEY)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SURICATA_APP}-Admin" https://$SUB_SURICATA_APP.$HOMESERVER_DOMAIN/#/login $HOMESERVER_ABBREV $SURICATA_ADMIN_USERNAME $SURICATA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CROWDSEC_WEB}-Admin" https://$SUB_CROWDSEC_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CROWDSEC_ADMIN_USERNAME $CROWDSEC_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_MDCOLLAB_APP}-Admin" https://$SUB_MDCOLLAB_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MDCOLLAB_ADMIN_USERNAME $MDCOLLAB_ADMIN_PASSWORD)"\n" #ADD_NEW_VW_CREDS_HERE # RelayServer strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CLIENTDNS}-user1" https://${SUB_CLIENTDNS}-user1.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $CLIENTDNS_USER1_ADMIN_USERNAME $CLIENTDNS_USER1_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_ADGUARD}-RelayServer" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/login.html $HOMESERVER_ABBREV $RELAYSERVER_ADGUARD_ADMIN_USERNAME $RELAYSERVER_ADGUARD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CLIENTDNS}-RelayServer" https://$SUB_CLIENTDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_CLIENTDNS_ADMIN_USERNAME $RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_PORTAINER}-RelayServer" https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/#!/auth $HOMESERVER_ABBREV $RELAYSERVER_PORTAINER_ADMIN_USERNAME $RELAYSERVER_PORTAINER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_RSPAMD}-RelayServer" https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_RSPAMD_ADMIN_USERNAME $RELAYSERVER_RSPAMD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_SYNCTHING}-RelayServer" https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_SYNCTHING_ADMIN_USERNAME $RELAYSERVER_SYNCTHING_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_FILEBROWSER}-RelayServer" https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_FILEBROWSER_ADMIN_USERNAME $RELAYSERVER_FILEBROWSER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_CADDYDNS}-RelayServer" https://$SUB_CADDYDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_CADDYDNS_ADMIN_USERNAME $RELAYSERVER_CADDYDNS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "${FMLNAME_WGPORTAL}-RelayServer" https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $RELAYSERVER_WGPORTAL_ADMIN_EMAIL $RELAYSERVER_WGPORTAL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}"\n\n\n\n" strInstructions="Vaultwarden Import Instructions - !!! READ ALL STEPS !!!\n_________________________________________________________________________\n\n" strInstructions=$strInstructions"1. Vaultwarden is only accessible on your private network, so any devices that you wish to access this service with must be correctly added. \n\n2. Upon installation, you should receive a seperate 'Join Vaultwarden' email from the Vaultwarden service. Click the provided 'Join Organization Now' button and create an account. ENSURE you DESELECT 'Check known data breaches for this password'. This option requires the vaultwarden container to have access to the internet, but this has been disabled by default due to the nature of this service. \n\n3. After creating your account, log in through the same web interface. \n\n4. Select Tools on top of page, then Import Data on left side. For File format, select Bitwarden(csv) from the drop down. Then copy all of the data AFTER the line below (including field headers) and paste it into the provided empty text box. Then click Import Data. \n\n5. Download and install Bitwarden plugin/extension for your browser (https://bitwarden.com/download/). \n\n6. In the browser plugin, select Self-hosted for Region. Then enter https://$SUB_VAULTWARDEN.$HOMESERVER_DOMAIN in both Server URL and Web vault server URL fields, and Save. \n\n7. Log in with email and master password. Go to Settings (bottom right), then Auto-fill, then under Default URI match detection, select Starts with. (You can also check the Auto-fill on page load option, but ensure you know how it works and the risks)\n\n8. Delete this email (and empty it from Trash) once you have imported the passwords into Vaultwarden. There is an option within Script-server or the console UI to send yourself another copy if need be (01 Misc Utils -> 08 Email Vaultwarden Credentials).\n\n9. All of these passwords are randomly generated during install and stored in your configuration file, which is encrypted at rest (thus the need to enter your config decrypt password for nearly every operation). Nota Bene: If you change any of these generated passwords it will not sync back to this source. If you change the Portainer, AdguardHome, or WG Portal admin passwords without also changing them in the configuration file, then you will break any of the script functions that use these utilities (they use API calls that require authorization). There could also be consequences for a few others as well. For more information, ask on the forum (https://forum.homeserverhq.com). To view/edit the configuration file, run the console UI (enter 'bash hshq.sh' on your HomeServer), then go to System Utils -> 1 Edit Encrypted Config File. BE VERY CAREFUL editing anything in this config file, you could break things!!!\n\n10. After any operation that requires the config decrypt password, whether via the console UI or Script-server web interface, you should ALWAYS see a confirmation that the configuration file has been encrypted. If a script function terminates abnormally, ensure to re-run another simple function, for example (01 Misc Utils -> 09 Email Root CA) just to ensure the configuration file is back to an encrypted state." sendEmail -s "Vaultwarden Login Import !!! READ ALL STEPS !!!" -b "$strInstructions\n\n$strOutput" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -t $EMAIL_ADMIN_EMAIL_ADDRESS } function emailUserVaultwardenCredentials() { vw_username=$1 vw_email=$2 strOutput="________________________________________________________________________\n\n" strOutput=$strOutput"folder,favorite,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp\n" strOutput=${strOutput}$(getSvcCredentialsVW "LDAP Services - Username" "\"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/,https://$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN/login,https://$SUB_GITEA.$HOMESERVER_DOMAIN/user/login,https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN/web/#/login,https://$SUB_MASTODON.$HOMESERVER_DOMAIN/auth/sign_in,https://$SUB_MATRIX_ELEMENT_PUBLIC.$HOMESERVER_DOMAIN/#/login,https://$SUB_MATRIX_ELEMENT_PRIVATE.$HOMESERVER_DOMAIN/#/login,https://$SUB_MEALIE.$HOMESERVER_DOMAIN/login,https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/login,https://$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN/log_in/,https://$SUB_PEERTUBE.$HOMESERVER_DOMAIN/login,https://$SUB_ESPOCRM.$HOMESERVER_DOMAIN/,https://$SUB_PIXELFED.$HOMESERVER_DOMAIN/login,https://$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN/,https://$SUB_KANBOARD.$HOMESERVER_DOMAIN/,https://$SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN/index.php/login,https://$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN/login,https://$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN/#login,https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN/login/,https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN/accounts/login/,https://$SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN/,https://$SUB_METABASE.$HOMESERVER_DOMAIN/auth/login,https://$SUB_HEDGEDOC_APP.$HOMESERVER_DOMAIN/login\"" $HOMESERVER_ABBREV $vw_username abcdefg)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "LDAP Services - Email" "\"https://$SUB_PENPOT.$HOMESERVER_DOMAIN/#/auth/login,https://$SUB_PIXELFED.$HOMESERVER_DOMAIN/login,https://$SUB_JOPLIN_APP.$HOMESERVER_DOMAIN/login\"" $HOMESERVER_ABBREV ${vw_username}@$HOMESERVER_DOMAIN abcdefg)"\n" strOutput=${strOutput}$(getSvcCredentialsVW "Mailu-User" "https://$SUB_MAILU.$HOMESERVER_DOMAIN/sso/login" $HOMESERVER_ABBREV ${vw_username}@$HOMESERVER_DOMAIN abcdefg)"\n" strOutput=${strOutput}"\n\n" strInstructions="Vaultwarden User Import Instructions:\n\n" strInstructions=$strInstructions"For convenience, import the text BELOW the following solid line into Vaultwarden. Then simply change the password (abcdefg) to your correct password. It will be reflected for all LDAP-based services. If you change your password in the future, then you only need to update this one entry within the Vaultwarden password manager. There is a separate entry for Penpot, since it requires the email address to be entered rather than the ldap username. There is also an entry for the email(Mailu) login as well." sendEmail -s "Vaultwarden User Login Import" -b "$strInstructions\n\n$strOutput" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -t $vw_email } function emailFormattedCredentials() { is_sudo=$1 strOutput="" strOutput=$strOutput" All Services Login Info " strOutput=$strOutput"========================================================================\n" strOutput=$strOutput"Be careful keeping this in your mailbox, it contains all of your Admin passwords in plain text! If you intend to install and use Vaultwarden, then you will receive an email once it is installed, and it will contain this information in a format that you can easily import into your vault. You can also resend yourself this email later from within Script-server, i.e. 01 Misc Utils -> 11 Email All Credentials\n\n\n\n" strOutput=$strOutput"Service Name, Username, Password, URL(s)\n" strOutput=$strOutput"________________________________________________________________________\n\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PORTAINER}" "\"https://$SUB_PORTAINER.$HOMESERVER_DOMAIN/#!/auth,https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$PORTAINER_LOCAL_HTTPS_PORT/#!/auth\"" $HOMESERVER_ABBREV $PORTAINER_ADMIN_USERNAME $PORTAINER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ADGUARD}" https://$SUB_ADGUARD.$HOMESERVER_DOMAIN/login.html $HOMESERVER_ABBREV $ADGUARD_ADMIN_USERNAME $ADGUARD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SCRIPTSERVER}" "\"https://$SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN/login.html,https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$SCRIPTSERVER_LOCALHOST_PORT/login.html\"" $HOMESERVER_ABBREV $SCRIPTSERVER_ADMIN_USERNAME $SCRIPTSERVER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENLDAP_PHP}" https://$SUB_OPENLDAP_PHP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV \"$LDAP_ADMIN_BIND_DN\" $LDAP_ADMIN_BIND_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AUTHELIA}" https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WAZUH}" https://$SUB_WAZUH.$HOMESERVER_DOMAIN/app/login $HOMESERVER_ABBREV $WAZUH_USERS_ADMIN_USERNAME $WAZUH_USERS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_GRAFANA}" https://$SUB_GRAFANA.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $GRAFANA_ADMIN_USERNAME $GRAFANA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ALLOY}" https://$SUB_ALLOY.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $ALLOY_ADMIN_USERNAME $ALLOY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_INFLUXDB}" https://$SUB_INFLUXDB.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $INFLUXDB_ADMIN_USERNAME $INFLUXDB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DOZZLE}" https://$SUB_DOZZLE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $DOZZLE_USERNAME $DOZZLE_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_JELLYFIN}-Admin" "\"https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN/web/#/login,https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN/web/#/wizard/user\"" $HOMESERVER_ABBREV $JELLYFIN_ADMIN_USERNAME $JELLYFIN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_JELLYFIN}-User" https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN/web/#/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PHOTOPRISM}-Admin" https://$SUB_PHOTOPRISM.$HOMESERVER_DOMAIN/library/login $HOMESERVER_ABBREV $PHOTOPRISM_ADMIN_USERNAME $PHOTOPRISM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_GUACAMOLE}" https://$SUB_GUACAMOLE.$HOMESERVER_DOMAIN/guacamole/ $HOMESERVER_ABBREV $GUACAMOLE_DEFAULT_ADMIN_USERNAME $GUACAMOLE_DEFAULT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WORDPRESS}" https://$SUB_WORDPRESS.$HOMESERVER_DOMAIN/wp-login $HOMESERVER_ABBREV $WORDPRESS_ADMIN_USERNAME $WORDPRESS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WIKIJS}" https://$SUB_WIKIJS.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $WIKIJS_ADMIN_EMAIL_ADDRESS $WIKIJS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_UPTIMEKUMA}" https://$SUB_UPTIMEKUMA.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $UPTIMEKUMA_USERNAME $UPTIMEKUMA_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SQLPAD}" https://$SUB_SQLPAD.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $SQLPAD_ADMIN_USERNAME $SQLPAD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SYNCTHING}" https://$SUB_SYNCTHING.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $SYNCTHING_ADMIN_USERNAME $SYNCTHING_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CODESERVER}" https://$SUB_CODESERVER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CODESERVER_ADMIN_USERNAME $CODESERVER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HOMEASSISTANT_TASMOADMIN}" "\"https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN/tasmoadmin,https://$SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN/login\"" $HOMESERVER_ABBREV $HOMEASSISTANT_TASMOADMIN_USER $HOMEASSISTANT_TASMOADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HOMEASSISTANT_CONFIGURATOR}" "\"https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN/configurator,https://$SUB_HOMEASSISTANT_CONFIGURATOR.$HOMESERVER_DOMAIN/\"" $HOMESERVER_ABBREV $HOMEASSISTANT_CONFIGURATOR_USER $HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HEIMDALL}-Admin" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_ADMIN_USERNAME $HEIMDALL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HEIMDALL}-Users" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_USER_USERNAME $HEIMDALL_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HEIMDALL}-RemoteServers" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_HOMESERVERS_USERNAME $HEIMDALL_HOMESERVERS_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HEIMDALL}-RelayServer" https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEIMDALL_RELAYSERVER_USERNAME $HEIMDALL_RELAYSERVER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_GITEA}-Admin" https://$SUB_GITEA.$HOMESERVER_DOMAIN/user/login $HOMESERVER_ABBREV $GITEA_ADMIN_USERNAME $GITEA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_GITEA}-User" https://$SUB_GITEA.$HOMESERVER_DOMAIN/user/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_GITLAB}" https://$SUB_GITLAB.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENLDAP_MANAGER}" https://$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN/log_in/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MAILU}-Admin" https://$SUB_MAILU.$HOMESERVER_DOMAIN/sso/login $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $EMAIL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MAILU}-Shared" https://$SUB_MAILU.$HOMESERVER_DOMAIN/sso/login $HOMESERVER_ABBREV $EMAIL_JOINT_EMAIL_ADDRESS $EMAIL_JOINT_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MATRIX_ELEMENT_PRIVATE}" https://$SUB_MATRIX_ELEMENT_PRIVATE.$HOMESERVER_DOMAIN/#/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MATRIX_ELEMENT_PUBLIC}" https://$SUB_MATRIX_ELEMENT_PUBLIC.$HOMESERVER_DOMAIN/#/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MEALIE}-Admin" https://$SUB_MEALIE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MEALIE_ADMIN_USERNAME $MEALIE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MEALIE}-User" https://$SUB_MEALIE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_KASM}-Admin" "\"https://$SUB_KASM.$HOMESERVER_DOMAIN/#/login,https://$SUB_KASM_WIZARD.$HOMESERVER_DOMAIN\"" $HOMESERVER_ABBREV $KASM_ADMIN_EMAIL_ADDRESS $KASM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_KASM}-User" "\"https://$SUB_KASM.$HOMESERVER_DOMAIN/#/login,https://$SUB_KASM_WIZARD.$HOMESERVER_DOMAIN\"" $HOMESERVER_ABBREV $KASM_USER_EMAIL_ADDRESS $KASM_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_REMOTELY}" "\"https://$SUB_REMOTELY.$HOMESERVER_DOMAIN/Account/Register,https://$SUB_REMOTELY.$HOMESERVER_DOMAIN/Account/Login\"" $HOMESERVER_ABBREV $REMOTELY_ADMIN_EMAIL_ADDRESS $REMOTELY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DUPLICATI}" "\"https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/login.html,https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/ngclient/login\"" $HOMESERVER_ABBREV "NA" $DUPLICATI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_FILEBROWSER}" https://$SUB_FILEBROWSER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $FILEBROWSER_USERNAME $FILEBROWSER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MASTODON}-Admin" https://$SUB_MASTODON.$HOMESERVER_DOMAIN/auth/sign_in $HOMESERVER_ABBREV $MASTODON_ADMIN_EMAIL_ADDRESS $MASTODON_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MASTODON}-User" https://$SUB_MASTODON.$HOMESERVER_DOMAIN/auth/sign_in $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PEERTUBE}-Admin" https://$SUB_PEERTUBE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PEERTUBE_ADMIN_USERNAME $PEERTUBE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PEERTUBE}-User" https://$SUB_PEERTUBE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_COLLABORA}-Admin" https://$SUB_COLLABORA.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $COLLABORA_ADMIN_USERNAME $COLLABORA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_NEXTCLOUD}-Admin" https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $NEXTCLOUD_ADMIN_USERNAME $NEXTCLOUD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_NEXTCLOUD}-User" https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DISCOURSE}-Admin" https://$SUB_DISCOURSE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $DISCOURSE_ADMIN_USERNAME $DISCOURSE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_VAULTWARDEN}-Admin" https://$SUB_VAULTWARDEN.$HOMESERVER_DOMAIN/admin $HOMESERVER_ABBREV admin $VAULTWARDEN_ADMIN_TOKEN)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CALIBRE_WEB}-Admin" https://$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CALIBRE_WEB_ADMIN_USERNAME $CALIBRE_WEB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CALIBRE_WEB}-User" https://$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_FRESHRSS}-Admin" https://$SUB_FRESHRSS.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $FRESHRSS_ADMIN_USERNAME $FRESHRSS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_KEILA}-Admin" https://$SUB_KEILA.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $KEILA_ADMIN_EMAIL_ADDRESS $KEILA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WALLABAG}-Admin" https://$SUB_WALLABAG.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $WALLABAG_ADMIN_USERNAME $WALLABAG_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_JUPYTER}-Admin" https://$SUB_JUPYTER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV admin $JUPYTER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PAPERLESS_APP}-Admin" https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN/accounts/login/ $HOMESERVER_ABBREV $PAPERLESS_ADMIN_USERNAME $PAPERLESS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PAPERLESS_AI_WEB}-Admin" https://$SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PAPERLESS_AI_ADMIN_USERNAME $PAPERLESS_AI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PAPERLESS_GPT_WEB}-Admin" https://$SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PAPERLESS_GPT_ADMIN_USERNAME $PAPERLESS_GPT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SPEEDTEST_TRACKER_LOCAL}-Admin" https://$SUB_SPEEDTEST_TRACKER_LOCAL.$HOMESERVER_DOMAIN/admin/login $HOMESERVER_ABBREV $SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS $SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SPEEDTEST_TRACKER_VPN}-Admin" https://$SUB_SPEEDTEST_TRACKER_VPN.$HOMESERVER_DOMAIN/admin/login $HOMESERVER_ABBREV $SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS $SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CHANGEDETECTION}-Admin" https://$SUB_CHANGEDETECTION.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV admin $CHANGEDETECTION_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HUGINN}-Admin" https://$SUB_HUGINN.$HOMESERVER_DOMAIN/users/sign_in $HOMESERVER_ABBREV $HUGINN_ADMIN_USERNAME $HUGINN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_GRAMPSWEB}-Admin" https://$SUB_GRAMPSWEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $GRAMPSWEB_ADMIN_USERNAME $GRAMPSWEB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PENPOT}-User" https://$SUB_PENPOT.$HOMESERVER_DOMAIN/#/auth/login $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ESPOCRM}-Admin" https://$SUB_ESPOCRM.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $ESPOCRM_ADMIN_USERNAME $ESPOCRM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_IMMICH_APP}-Admin" https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $IMMICH_ADMIN_EMAIL_ADDRESS $IMMICH_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HOMARR}-Admin" https://$SUB_HOMARR.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $HOMARR_ADMIN_USERNAME $HOMARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MATOMO}-Admin" https://$SUB_MATOMO.$HOMESERVER_DOMAIN/index.php $HOMESERVER_ABBREV $MATOMO_ADMIN_USERNAME $MATOMO_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PASTEFY}-Admin" https://$SUB_PASTEFY.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $PASTEFY_ADMIN_USERNAME $PASTEFY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PIXELFED}-Admin" "\"https://$SUB_PIXELFED.$HOMESERVER_DOMAIN/login,https://$SUB_PIXELFED.$HOMESERVER_DOMAIN/i/auth/sudo\"" $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_YAMTRACK}-Admin" https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN/accounts/login $HOMESERVER_ABBREV $YAMTRACK_ADMIN_USERNAME $YAMTRACK_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SERVARR_SONARR}-Admin" https://$SUB_SERVARR_SONARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SONARR_ADMIN_USERNAME $SONARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SERVARR_RADARR}-Admin" https://$SUB_SERVARR_RADARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $RADARR_ADMIN_USERNAME $RADARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SERVARR_LIDARR}-Admin" https://$SUB_SERVARR_LIDARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LIDARR_ADMIN_USERNAME $LIDARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SERVARR_READARR}-Admin" https://$SUB_SERVARR_READARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $READARR_ADMIN_USERNAME $READARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SERVARR_BAZARR}-Admin" https://$SUB_SERVARR_BAZARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $BAZARR_ADMIN_USERNAME $BAZARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SERVARR_MYLAR3}-Admin" https://$SUB_SERVARR_MYLAR3.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $MYLAR3_ADMIN_USERNAME $MYLAR3_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SERVARR_PROWLARR}-Admin" https://$SUB_SERVARR_PROWLARR.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $PROWLARR_ADMIN_USERNAME $PROWLARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SABNZBD}-Admin" https://$SUB_SABNZBD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SABNZBD_ADMIN_USERNAME $SABNZBD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_QBITTORRENT}-Admin" https://$SUB_QBITTORRENT.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $QBITTORRENT_ADMIN_USERNAME $QBITTORRENT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OMBI_APP}-Admin" "\"https://$SUB_OMBI_APP.$HOMESERVER_DOMAIN/login,https://$SUB_OMBI_APP.$HOMESERVER_DOMAIN/Wizard\"" $HOMESERVER_ABBREV $OMBI_ADMIN_USERNAME $OMBI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MESHCENTRAL}-Admin" https://$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_NAVIDROME}-Admin" https://$SUB_NAVIDROME.$HOMESERVER_DOMAIN/app/#/login $HOMESERVER_ABBREV $NAVIDROME_ADMIN_USERNAME $NAVIDROME_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_BUDIBASE}-Admin" https://$SUB_BUDIBASE.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $BUDIBASE_ADMIN_EMAIL_ADDRESS $BUDIBASE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AUDIOBOOKSHELF}-Admin" https://$SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN/audiobookshelf/login $HOMESERVER_ABBREV $AUDIOBOOKSHELF_ADMIN_USERNAME $AUDIOBOOKSHELF_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_METABASE}-Admin" https://$SUB_METABASE.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $METABASE_ADMIN_EMAIL_ADDRESS $METABASE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_KANBOARD}-Admin" https://$SUB_KANBOARD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $KANBOARD_ADMIN_USERNAME $KANBOARD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_FRAPPE_HR}-Admin" https://$SUB_FRAPPE_HR.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $FRAPPE_HR_ADMIN_USERNAME $FRAPPE_HR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MINTHCM}-Admin" https://$SUB_MINTHCM.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $MINTHCM_ADMIN_USERNAME $MINTHCM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CLOUDBEAVER}-Admin" https://$SUB_CLOUDBEAVER.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $CLOUDBEAVER_ADMIN_USERNAME $CLOUDBEAVER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DBGATE}-Admin" https://$SUB_DBGATE.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $DBGATE_ADMIN_USERNAME $DBGATE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_TWENTY}-Admin" https://$SUB_TWENTY.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $TWENTY_ADMIN_EMAIL_ADDRESS $TWENTY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ODOO}-Admin" https://$SUB_ODOO.$HOMESERVER_DOMAIN/web/login $HOMESERVER_ABBREV $ODOO_ADMIN_USERNAME $ODOO_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CALCOM_APP}-Admin" https://$SUB_CALCOM_APP.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $CALCOM_ADMIN_EMAIL_ADDRESS $CALCOM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_RALLLY}-Admin" https://$SUB_RALLLY.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $RALLLY_ADMIN_EMAIL_ADDRESS $RALLLY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_EASYAPPOINTMENTS}-Admin" https://$SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN/index.php/login $HOMESERVER_ABBREV $EASYAPPOINTMENTS_ADMIN_USERNAME $EASYAPPOINTMENTS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENPROJECT_APP}-Admin" https://$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENPROJECT_ADMIN_USERNAME $OPENPROJECT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ZAMMAD_APP}-Admin" https://$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN/#login $HOMESERVER_ABBREV $ZAMMAD_ADMIN_EMAIL_ADDRESS $ZAMMAD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ZULIP_APP}-Admin" "\"https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN/login/,https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN/accounts/login/\"" $HOMESERVER_ABBREV $ZULIP_ADMIN_EMAIL_ADDRESS $ZULIP_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_KILLBILL_WEB}-Admin" https://$SUB_KILLBILL_WEB.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $KILLBILL_ADMIN_USERNAME $KILLBILL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_INVOICESHELF_APP}-Admin" https://$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $INVOICESHELF_ADMIN_EMAIL_ADDRESS $INVOICESHELF_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_INVOICENINJA_WEB}-Admin" https://$SUB_INVOICENINJA_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $INVOICENINJA_ADMIN_EMAIL_ADDRESS $INVOICENINJA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DOLIBARR_APP}-Admin" https://$SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $DOLIBARR_ADMIN_USERNAME $DOLIBARR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_N8N_APP}-Admin" https://$SUB_N8N_APP.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $N8N_ADMIN_EMAIL_ADDRESS $N8N_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AUTOMATISCH_APP}-Admin" https://$SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $AUTOMATISCH_ADMIN_EMAIL_ADDRESS $AUTOMATISCH_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ACTIVEPIECES_APP}-Admin" https://$SUB_ACTIVEPIECES_APP.$HOMESERVER_DOMAIN/sign-in $HOMESERVER_ABBREV $ACTIVEPIECES_ADMIN_EMAIL_ADDRESS $ACTIVEPIECES_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_BESZEL_APP}-Admin" https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $BESZEL_ADMIN_EMAIL_ADDRESS $BESZEL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_TAIGA_APP}-Admin" https://$SUB_TAIGA_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $TAIGA_ADMIN_USERNAME $TAIGA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENSIGN_APP}-Admin" https://$SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $OPENSIGN_ADMIN_EMAIL_ADDRESS $OPENSIGN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DOCUSEAL_APP}-Admin" https://$SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN/sign_in $HOMESERVER_ABBREV $DOCUSEAL_ADMIN_EMAIL_ADDRESS $DOCUSEAL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CONTROLR_APP}-Admin" "\"https://$SUB_CONTROLR_APP.$HOMESERVER_DOMAIN/Account/Login,https://$SUB_CONTROLR_APP.$HOMESERVER_DOMAIN/Account/Register\"" $HOMESERVER_ABBREV $CONTROLR_ADMIN_EMAIL_ADDRESS $CONTROLR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CONTROLR_ASPIRE}-Admin" https://$SUB_CONTROLR_ASPIRE.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CONTROLR_ADMIN_EMAIL_ADDRESS $CONTROLR_ASPIRE_TOKEN)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AKAUNTING_APP}-Admin" https://$SUB_AKAUNTING_APP.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $AKAUNTING_ADMIN_EMAIL_ADDRESS $AKAUNTING_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AXELOR_APP}-Admin" https://$SUB_AXELOR_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $AXELOR_ADMIN_USERNAME $AXELOR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AXELOR_GOOVEE}-Admin" https://$SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $AXELOR_GOOVEE_ADMIN_USERNAME $AXELOR_GOOVEE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CONVERTX_APP}-Admin" https://$SUB_CONVERTX_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CONVERTX_ADMIN_EMAIL_ADDRESS $CONVERTX_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_KOPIA_APP}-Admin" https://$SUB_KOPIA_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $KOPIA_ADMIN_USERNAME $KOPIA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LOCALAI_SERVER}-Admin" https://$SUB_LOCALAI_SERVER.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $LOCALAI_ADMIN_USERNAME $LOCALAI_API_KEY)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LANGFLOW_APP}-Admin" https://$SUB_LANGFLOW_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LANGFLOW_ADMIN_USERNAME $LANGFLOW_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ANYTHINGLLM_APP}-Admin" "\"https://$SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN/login,https://$SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN/onboarding/user-setup\"" $HOMESERVER_ABBREV $ANYTHINGLLM_ADMIN_USERNAME $ANYTHINGLLM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LIBRECHAT_APP}-Admin" https://$SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LIBRECHAT_ADMIN_EMAIL_ADDRESS $LIBRECHAT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENWEBUI_APP}-Admin" https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/auth $HOMESERVER_ABBREV $OPENWEBUI_ADMIN_EMAIL_ADDRESS $OPENWEBUI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENWEBUI_QDRANT}-Admin" https://$SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $OPENWEBUI_ADMIN_EMAIL_ADDRESS $OPENWEBUI_QDRANT_API_KEY)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_KHOJ_SERVER}-Admin" https://$SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $KHOJ_ADMIN_EMAIL_ADDRESS $KHOJ_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LOBECHAT_APP}-Admin" https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LOBECHAT_ADMIN_USERNAME $LOBECHAT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_RAGFLOW_APP}-Admin" https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN/admin $HOMESERVER_ABBREV $RAGFLOW_ADMIN_EMAIL_ADDRESS $RAGFLOW_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_RAGFLOW_APP}-TeamAdmin" https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $EMAIL_ADMIN_EMAIL_ADDRESS $EMAIL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_RAGFLOW_RUSTFS}-Admin" https://$SUB_RAGFLOW_RUSTFS.$HOMESERVER_DOMAIN/rustfs/console/auth/login/ $HOMESERVER_ABBREV $RAGFLOW_MINIO_KEY $RAGFLOW_MINIO_SECRET)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_TABBYML_APP}-Admin" https://$SUB_TABBYML_APP.$HOMESERVER_DOMAIN/auth/signin $HOMESERVER_ABBREV $TABBYML_ADMIN_EMAIL_ADDRESS $TABBYML_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DIFY_APP}-Admin" https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $DIFY_ADMIN_EMAIL_ADDRESS $DIFY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MINDSDB_APP}-Admin" https://$SUB_MINDSDB_APP.$HOMESERVER_DOMAIN/local-login $HOMESERVER_ABBREV $MINDSDB_ADMIN_USERNAME $MINDSDB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WATERCRAWL_APP}-Admin" https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $WATERCRAWL_ADMIN_EMAIL_ADDRESS $WATERCRAWL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_FLOWISE_APP}-Admin" https://$SUB_FLOWISE_APP.$HOMESERVER_DOMAIN/signin $HOMESERVER_ABBREV $FLOWISE_ADMIN_EMAIL_ADDRESS $FLOWISE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SURFSENSE_APP}-Admin" https://$SUB_SURFSENSE_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SURFSENSE_ADMIN_EMAIL_ADDRESS $SURFSENSE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_NOCODB_APP}-Admin" https://$SUB_NOCODB_APP.$HOMESERVER_DOMAIN/dashboard/#/signin $HOMESERVER_ABBREV $NOCODB_ADMIN_EMAIL_ADDRESS $NOCODB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ENTE_APP}-Admin" https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $ENTE_ADMIN_USERNAME $ENTE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MORPHIC_APP}-Admin" https://$SUB_MORPHIC_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MORPHIC_ADMIN_USERNAME $MORPHIC_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENNOTEBOOK_APP}-Admin" https://$SUB_OPENNOTEBOOK_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENNOTEBOOK_ADMIN_USERNAME $OPENNOTEBOOK_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_APPSMITH_APP}-Admin" "\"https://$SUB_APPSMITH_APP.$HOMESERVER_DOMAIN/user/login,https://$SUB_APPSMITH_APP.$HOMESERVER_DOMAIN/setup/welcome\"" $HOMESERVER_ABBREV $APPSMITH_ADMIN_EMAIL_ADDRESS $APPSMITH_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_TRILIUM_APP}-Admin" "\"https://$SUB_TRILIUM_APP.$HOMESERVER_DOMAIN/login,https://$SUB_TRILIUM_APP.$HOMESERVER_DOMAIN/set-password\"" $HOMESERVER_ABBREV $TRILIUM_ADMIN_USERNAME $TRILIUM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_DOCSGPT_APP}-Admin" https://$SUB_DOCSGPT_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $DOCSGPT_ADMIN_USERNAME $DOCSGPT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MEMOS_APP}-Admin" https://$SUB_MEMOS_APP.$HOMESERVER_DOMAIN/auth $HOMESERVER_ABBREV $MEMOS_ADMIN_USERNAME $MEMOS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SPEAKR_APP}-Admin" https://$SUB_SPEAKR_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SPEAKR_ADMIN_EMAIL_ADDRESS $SPEAKR_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MONICA_APP}-Admin" https://$SUB_MONICA_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MONICA_ADMIN_EMAIL_ADDRESS $MONICA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AFFINE_APP}-Admin" "\"https://$SUB_AFFINE_APP.$HOMESERVER_DOMAIN/sign-in,https://$SUB_AFFINE_APP.$HOMESERVER_DOMAIN/admin/auth\"" $HOMESERVER_ABBREV $AFFINE_ADMIN_EMAIL_ADDRESS $AFFINE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_JOPLIN_APP}-Admin" https://$SUB_JOPLIN_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $JOPLIN_ADMIN_EMAIL_ADDRESS $JOPLIN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SUPERSET_APP}-Admin" https://$SUB_SUPERSET_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SUPERSET_ADMIN_USERNAME $SUPERSET_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LITELLM_APP}-Admin" https://$SUB_LITELLM_APP.$HOMESERVER_DOMAIN/ui/login/ $HOMESERVER_ABBREV $LITELLM_ADMIN_USERNAME $LITELLM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LANGFUSE_APP}-Admin" https://$SUB_LANGFUSE_APP.$HOMESERVER_DOMAIN/auth/sign-in $HOMESERVER_ABBREV $LANGFUSE_ADMIN_EMAIL_ADDRESS $LANGFUSE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LANGFUSE_CLICKHOUSE}-Admin" https://$SUB_LANGFUSE_CLICKHOUSE.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $LANGFUSE_CLICKHOUSE_USER $LANGFUSE_CLICKHOUSE_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LANGFUSE_MINIO}-Admin" https://$SUB_LANGFUSE_MINIO.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LANGFUSE_MINIO_KEY $LANGFUSE_MINIO_SECRET)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SKYVERN_API}-Admin" https://$SUB_SKYVERN_API.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $SKYVERN_ADMIN_USERNAME $SKYVERN_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WGER_APP}-Admin" https://$SUB_WGER_APP.$HOMESERVER_DOMAIN/en/user/login $HOMESERVER_ABBREV $WGER_ADMIN_USERNAME $WGER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WORKOUTCOOL_APP}-Admin" https://$SUB_WORKOUTCOOL_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $WORKOUTCOOL_ADMIN_USERNAME $WORKOUTCOOL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENRAG_APP}-Admin" https://$SUB_OPENRAG_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENRAG_ADMIN_USERNAME $OPENRAG_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_VOICEBOX_APP}-Admin" https://$SUB_VOICEBOX_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $VOICEBOX_ADMIN_USERNAME $VOICEBOX_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENCODE_APP}-Admin" https://$SUB_OPENCODE_APP.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $OPENCODE_ADMIN_USERNAME $OPENCODE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_OPENCODE_CODESERVER}-Admin" https://$SUB_OPENCODE_CODESERVER.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $OPENCODE_CODESERVER_USERNAME $OPENCODE_CODESERVER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_EMAILCLASSIFIERAI_APP}-Admin" https://$SUB_EMAILCLASSIFIERAI_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $EMAILCLASSIFIERAI_ADMIN_USERNAME $EMAILCLASSIFIERAI_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HERMES_AGENT_DASHBOARD}-Admin" https://$SUB_HERMES_AGENT_DASHBOARD.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HERMES_AGENT_ADMIN_USERNAME $HERMES_AGENT_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HERMES_AGENT_WEBUI}-Admin" https://$SUB_HERMES_AGENT_WEBUI.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HERMES_AGENT_ADMIN_USERNAME $HERMES_AGENT_WEBUI_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_AUTOKB_WEB}-Admin" https://$SUB_AUTOKB_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $AUTOKB_ADMIN_USERNAME $AUTOKB_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SUITECRM_APP}-Admin" https://$SUB_SUITECRM_APP.$HOMESERVER_DOMAIN/#/Login $HOMESERVER_ABBREV $SUITECRM_ADMIN_USERNAME $SUITECRM_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HEDGEDOC_APP}-Admin" https://$SUB_HEDGEDOC_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $HEDGEDOC_ADMIN_USERNAME $HEDGEDOC_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_HEDGEDOC_APP}-User" https://$SUB_HEDGEDOC_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $LDAP_ADMIN_USER_USERNAME $LDAP_ADMIN_USER_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PRESENTON_APP}-Admin" https://$SUB_PRESENTON_APP.$HOMESERVER_DOMAIN $HOMESERVER_ABBREV $PRESENTON_ADMIN_USERNAME $PRESENTON_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_BASICMEMORY_APP}-Admin" https://$SUB_BASICMEMORY_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $BASICMEMORY_ADMIN_USERNAME $BASICMEMORY_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_COGNEE_FRONTEND}-Admin" https://$SUB_COGNEE_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $COGNEE_ADMIN_USERNAME $COGNEE_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LIGHTRAG_APP}-Admin" https://$SUB_LIGHTRAG_APP.$HOMESERVER_DOMAIN/webui/#/login $HOMESERVER_ABBREV $LIGHTRAG_ADMIN_USERNAME $LIGHTRAG_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_LIGHTRAG_QDRANT}-Admin" https://$SUB_LIGHTRAG_QDRANT.$HOMESERVER_DOMAIN/dashboard $HOMESERVER_ABBREV $LIGHTRAG_ADMIN_EMAIL_ADDRESS $LIGHTRAG_QDRANT_API_KEY)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SURICATA_APP}-Admin" https://$SUB_SURICATA_APP.$HOMESERVER_DOMAIN/#/login $HOMESERVER_ABBREV $SURICATA_ADMIN_USERNAME $SURICATA_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CROWDSEC_WEB}-Admin" https://$SUB_CROWDSEC_WEB.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $CROWDSEC_ADMIN_USERNAME $CROWDSEC_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_MDCOLLAB_APP}-Admin" https://$SUB_MDCOLLAB_APP.$HOMESERVER_DOMAIN/login $HOMESERVER_ABBREV $MDCOLLAB_ADMIN_USERNAME $MDCOLLAB_ADMIN_PASSWORD)"\n" #ADD_NEW_FMT_CREDS_HERE # RelayServer strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CLIENTDNS}-user1" https://${SUB_CLIENTDNS}-user1.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $CLIENTDNS_USER1_ADMIN_USERNAME $CLIENTDNS_USER1_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_ADGUARD}-RelayServer" https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/login.html $HOMESERVER_ABBREV $RELAYSERVER_ADGUARD_ADMIN_USERNAME $RELAYSERVER_ADGUARD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CLIENTDNS}-RelayServer" https://$SUB_CLIENTDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_CLIENTDNS_ADMIN_USERNAME $RELAYSERVER_CLIENTDNS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_PORTAINER}-RelayServer" https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/#!/auth $HOMESERVER_ABBREV $RELAYSERVER_PORTAINER_ADMIN_USERNAME $RELAYSERVER_PORTAINER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_RSPAMD}-RelayServer" https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_RSPAMD_ADMIN_USERNAME $RELAYSERVER_RSPAMD_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_SYNCTHING}-RelayServer" https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_SYNCTHING_ADMIN_USERNAME $RELAYSERVER_SYNCTHING_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_FILEBROWSER}-RelayServer" https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_FILEBROWSER_ADMIN_USERNAME $RELAYSERVER_FILEBROWSER_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_CADDYDNS}-RelayServer" https://$SUB_CADDYDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/ $HOMESERVER_ABBREV $RELAYSERVER_CADDYDNS_ADMIN_USERNAME $RELAYSERVER_CADDYDNS_ADMIN_PASSWORD)"\n" strOutput=${strOutput}$(getFmtCredentials "${FMLNAME_WGPORTAL}-RelayServer" https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN/auth/login $HOMESERVER_ABBREV $RELAYSERVER_WGPORTAL_ADMIN_EMAIL $RELAYSERVER_WGPORTAL_ADMIN_PASSWORD)"\n" strOutput=${strOutput}"\n\n" if [ "$is_sudo" = "true" ]; then # This special case is only need during the initial installation due # to issues with adding members to groups (mailsenders) within a script. set +e num_email_tries=1 max_email_tries=5 is_success=false while [ $num_email_tries -lt $max_email_tries ] do rm -f dead.letter echo -e "$strOutput" | sudo timeout $MAILX_TIMEOUT mailx -s "All Services Login Info" -a "From: $(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -a "Message-Id: <$(uuidgen)@$HOMESERVER_DOMAIN>" "$EMAIL_ADMIN_EMAIL_ADDRESS" if [ $? -eq 0 ]; then is_success=true break fi echo "ERROR: Could not send email, retrying ($num_email_tries of $max_email_tries)..." sleep 3 ((num_email_tries++)) done if ! [ "$is_success" = "true" ]; then echo "ERROR: There was a problem sending the Root CA certificate..." fi set -e else sendEmail -s "All Services Login Info" -b "$strOutput" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -t $EMAIL_ADMIN_EMAIL_ADDRESS fi } function insertServicesHeimdall() { curdt=$(getCurrentDate) cur_id=1 # Admin Tab insertIntoHeimdallDB "$FMLNAME_SCRIPTSERVER" $USERTYPE_SCRIPTSERVER "https://$SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN" 1 "script-server.png" "$(getHeimdallOrderFromSub $SUB_SCRIPTSERVER $USERTYPE_SCRIPTSERVER)" insertIntoHeimdallDB "$FMLNAME_SCRIPTSERVER (IP)" $USERTYPE_SCRIPTSERVER "https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$SCRIPTSERVER_LOCALHOST_PORT" 1 "script-server.png" "$(getHeimdallOrderFromSub $SUB_SCRIPTSERVER $USERTYPE_SCRIPTSERVER)" insertIntoHeimdallDB "$FMLNAME_PORTAINER" $USERTYPE_PORTAINER "https://$SUB_PORTAINER.$HOMESERVER_DOMAIN" 1 "portainer.png" "$(getHeimdallOrderFromSub $SUB_PORTAINER $USERTYPE_PORTAINER)" insertIntoHeimdallDB "$FMLNAME_PORTAINER (IP)" $USERTYPE_PORTAINER "https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$PORTAINER_LOCAL_HTTPS_PORT" 1 "portainer.png" "$(getHeimdallOrderFromSub $SUB_PORTAINER $USERTYPE_PORTAINER)" insertIntoHeimdallDB "$FMLNAME_ADGUARD" $USERTYPE_ADGUARD "https://$SUB_ADGUARD.$HOMESERVER_DOMAIN" 1 "adguardhome.png" "$(getHeimdallOrderFromSub $SUB_ADGUARD $USERTYPE_ADGUARD)" if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then insertIntoHeimdallDB "${FMLNAME_CLIENTDNS}-user1" $USERTYPE_CLIENTDNS "https://${SUB_CLIENTDNS}-user1.$HOMESERVER_DOMAIN" 1 "dnsmasq.png" "$(getHeimdallOrderFromSub $SUB_CLIENTDNS $USERTYPE_CLIENTDNS)" fi insertIntoHeimdallDB "$FMLNAME_OPENLDAP_PHP" $USERTYPE_OPENLDAP_PHP "https://$SUB_OPENLDAP_PHP.$HOMESERVER_DOMAIN" 1 "ldapphp.png" "$(getHeimdallOrderFromSub $SUB_OPENLDAP_PHP $USERTYPE_OPENLDAP_PHP)" insertIntoHeimdallDB "$FMLNAME_UPTIMEKUMA" $USERTYPE_UPTIMEKUMA "https://$SUB_UPTIMEKUMA.$HOMESERVER_DOMAIN" 1 "uptimekuma.png" "$(getHeimdallOrderFromSub $SUB_UPTIMEKUMA $USERTYPE_UPTIMEKUMA)" insertIntoHeimdallDB "$FMLNAME_DUPLICATI" $USERTYPE_DUPLICATI "https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN" 1 "duplicati.png" "$(getHeimdallOrderFromSub $SUB_DUPLICATI $USERTYPE_DUPLICATI)" insertIntoHeimdallDB "$FMLNAME_SYNCTHING" $USERTYPE_SYNCTHING "https://$SUB_SYNCTHING.$HOMESERVER_DOMAIN" 1 "syncthing.png" "$(getHeimdallOrderFromSub $SUB_SYNCTHING $USERTYPE_SYNCTHING)" insertIntoHeimdallDB "Logout $FMLNAME_AUTHELIA" admin "https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/logout" 1 "authelia.png" "$(getHeimdallOrderFromSub $SUB_AUTHELIA admin)" # Users Tab insertIntoHeimdallDB "HomeServerHQ" $USERTYPE_AUTHELIA "https://www.homeserverhq.com" 1 "homeserverhq.png" "1" insertIntoHeimdallDB "$FMLNAME_OPENLDAP_MANAGER" $USERTYPE_OPENLDAP_MANAGER "https://$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN" 1 "ldapmanager.png" "$(getHeimdallOrderFromSub $SUB_OPENLDAP_MANAGER $USERTYPE_OPENLDAP_MANAGER)" insertIntoHeimdallDB "$FMLNAME_MAILU" $USERTYPE_MAILU "https://$SUB_MAILU.$HOMESERVER_DOMAIN" 1 "mailu.png" "$(getHeimdallOrderFromSub $SUB_MAILU $USERTYPE_MAILU)" insertIntoHeimdallDB "Logout $FMLNAME_AUTHELIA" $USERTYPE_AUTHELIA "https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/logout" 1 "authelia.png" "$(getHeimdallOrderFromSub $SUB_AUTHELIA $USERTYPE_AUTHELIA)" # RemoteServers Tab insertIntoHeimdallDB "$HOMESERVER_NAME" homeservers "https://$SUB_HSHQHOME.$HOMESERVER_DOMAIN" 1 "hs1.png" "$(getHeimdallOrderFromSub $SUB_HSHQHOME homeservers)" } function insertServicesUptimeKuma() { curdt=$(getCurrentDate) insertServiceUptimeKuma "$FMLNAME_AUTHELIA" $USERTYPE_AUTHELIA "https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_PORTAINER" $USERTYPE_PORTAINER "https://$SUB_PORTAINER.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_ADGUARD" $USERTYPE_ADGUARD "https://$SUB_ADGUARD.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_OPENLDAP_MANAGER" $USERTYPE_OPENLDAP_MANAGER "https://$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_OPENLDAP_PHP" $USERTYPE_OPENLDAP_PHP "https://$SUB_OPENLDAP_PHP.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_MAILU" $USERTYPE_MAILU "https://$SUB_MAILU.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_SCRIPTSERVER" $USERTYPE_SCRIPTSERVER "https://$SUB_SCRIPTSERVER.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_HEIMDALL" $USERTYPE_HEIMDALL "https://$SUB_HEIMDALL.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_DUPLICATI" $USERTYPE_DUPLICATI "https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$FMLNAME_SYNCTHING" $USERTYPE_SYNCTHING "https://$SUB_SYNCTHING.$HOMESERVER_DOMAIN" 1 insertServiceUptimeKuma "$HOMESERVER_NAME" homeservers "https://$SUB_HSHQSTATUS.$HOMESERVER_DOMAIN" 1 } function getHeimdallOrderFromSub() { ord_subdom="$1" ord_usertype="$2" order_num=0 order_uid=$(getHeimdallUserIDFromType $ord_usertype) case "$ord_subdom" in "$SUB_SCRIPTSERVER") order_num=2 ;; "$SUB_PORTAINER") order_num=3 ;; "$SUB_ADGUARD") order_num=4 ;; "$SUB_CLIENTDNS") order_num=5 ;; "$SUB_OPENLDAP_PHP") order_num=6 ;; "$SUB_WAZUH") order_num=7 ;; "$SUB_GRAFANA") order_num=8 ;; "$SUB_PROMETHEUS") order_num=9 ;; "$SUB_INFLUXDB") order_num=10 ;; "$SUB_ALLOY") order_num=10 ;; "$SUB_DOZZLE") order_num=11 ;; "$SUB_GUACAMOLE") order_num=12 ;; "$SUB_UPTIMEKUMA") order_num=13 ;; "$SUB_DUPLICATI") order_num=14 ;; "$SUB_SYNCTHING") order_num=15 ;; "$SUB_CODESERVER") order_num=16 ;; "$SUB_SQLPAD") order_num=17 ;; "$SUB_CLOUDBEAVER") order_num=17 ;; "$SUB_DBGATE") order_num=17 ;; "$SUB_HOMEASSISTANT_APP") order_num=18 ;; "$SUB_SHLINK_WEB") order_num=19 ;; "$SUB_FIREFLY_APP") order_num=20 ;; "$SUB_FIREFLY_IMPORTER") order_num=21 ;; "$SUB_FIREFLY_FINTSIMPORTER") order_num=21 ;; "$SUB_KASM_WIZARD") order_num=22 ;; "$SUB_COLLABORA") order_num=24 ;; "$SUB_NTFY") order_num=25 ;; "$SUB_ITTOOLS") order_num=26 ;; "$SUB_REMOTELY") order_num=27 ;; "$SUB_CALIBRE_SERVER") order_num=28 ;; "$SUB_NETDATA") order_num=29 ;; "$SUB_JUPYTER") order_num=30 ;; "$SUB_SPEEDTEST_TRACKER_LOCAL") order_num=31 ;; "$SUB_SPEEDTEST_TRACKER_VPN") order_num=32 ;; "$SUB_MATOMO") order_num=33 ;; "$SUB_AISTACK_MINDSDB_APP") order_num=34 ;; "$SUB_AISTACK_LANGFUSE") order_num=35 ;; "$SUB_OPENLDAP_MANAGER") order_num=38 ;; "$SUB_MAILU") order_num=39 ;; "$SUB_NEXTCLOUD") order_num=40 ;; "$SUB_JITSI") order_num=41 ;; "$SUB_MATRIX_ELEMENT_PRIVATE") order_num=42 ;; "$SUB_MATRIX_ELEMENT_PUBLIC") order_num=43 ;; "$SUB_MASTODON") order_num=44 ;; "$SUB_PEERTUBE") order_num=45 ;; "$SUB_SEARXNG") order_num=46 ;; "$SUB_PHOTOPRISM") order_num=47 ;; "$SUB_WIKIJS") order_num=48 ;; "$SUB_WORDPRESS") order_num=49 ;; "$SUB_GHOST") order_num=50 ;; "$SUB_JELLYFIN") order_num=51 ;; "$SUB_GITLAB") order_num=52 ;; "$SUB_VAULTWARDEN") if [ "$order_uid" = "1" ]; then order_num=23 elif [ "$order_uid" = "2" ]; then order_num=53 fi ;; "$SUB_DISCOURSE") order_num=54 ;; "$SUB_GITEA") order_num=55 ;; "$SUB_INVIDIOUS") order_num=56 ;; "$SUB_EXCALIDRAW_WEB") order_num=57 ;; "$SUB_EXCALIDRAW_AI") order_num=57 ;; "$SUB_DRAWIO_WEB") order_num=58 ;; "$SUB_MEALIE") order_num=59 ;; "$SUB_BARASSISTANT") order_num=60 ;; "$SUB_KASM") order_num=61 ;; "$SUB_CALIBRE_WEB") order_num=62 ;; "$SUB_STIRLINGPDF") order_num=63 ;; "$SUB_FRESHRSS") order_num=64 ;; "$SUB_KEILA") order_num=65 ;; "$SUB_WALLABAG") order_num=66 ;; "$SUB_PAPERLESS_AI_WEB") order_num=67 ;; "$SUB_PAPERLESS_APP") order_num=67 ;; "$SUB_PAPERLESS_GPT_WEB") order_num=67 ;; "$SUB_LINKWARDEN") order_num=68 ;; "$SUB_FILEBROWSER") order_num=69 ;; "$SUB_CHANGEDETECTION") order_num=70 ;; "$SUB_HUGINN") order_num=71 ;; "$SUB_FILEDROP") order_num=72 ;; "$SUB_PIPED_FRONTEND") order_num=73 ;; "$SUB_GRAMPSWEB") order_num=74 ;; "$SUB_PENPOT") order_num=75 ;; "$SUB_ESPOCRM") order_num=76 ;; "$SUB_IMMICH_APP") order_num=77 ;; "$SUB_HOMARR") order_num=78 ;; "$SUB_PASTEFY") order_num=79 ;; "$SUB_SNIPPETBOX") order_num=80 ;; "$SUB_AISTACK_OPENWEBUI") order_num=81 ;; "$SUB_PIXELFED") order_num=82 ;; "$SUB_YAMTRACK") order_num=83 ;; "$SUB_SERVARR_SONARR") order_num=84 ;; "$SUB_SERVARR_RADARR") order_num=85 ;; "$SUB_SERVARR_LIDARR") order_num=86 ;; "$SUB_SERVARR_READARR") order_num=87 ;; "$SUB_SERVARR_BAZARR") order_num=88 ;; "$SUB_SERVARR_MYLAR3") order_num=89 ;; "$SUB_SERVARR_PROWLARR") order_num=90 ;; "$SUB_SABNZBD") order_num=91 ;; "$SUB_QBITTORRENT") order_num=92 ;; "$SUB_OMBI_APP") order_num=93 ;; "$SUB_MESHCENTRAL") order_num=94 ;; "$SUB_NAVIDROME") order_num=95 ;; "$SUB_AUDIOBOOKSHELF") order_num=96 ;; "$SUB_STANDARDNOTES_SERVER") order_num=97 ;; "$SUB_STANDARDNOTES_FILES") order_num=98 ;; "$SUB_STANDARDNOTES_WEB") order_num=99 ;; "$SUB_ADMINER") order_num=100 ;; "$SUB_BUDIBASE") order_num=101 ;; "$SUB_METABASE") order_num=102 ;; "$SUB_KANBOARD") order_num=103 ;; "$SUB_WEKAN") order_num=104 ;; "$SUB_REVOLT") order_num=105 ;; "$SUB_FRAPPE_HR") order_num=106 ;; "$SUB_MINTHCM") order_num=107 ;; "$SUB_TWENTY") order_num=108 ;; "$SUB_ODOO") order_num=109 ;; "$SUB_CALCOM_APP") order_num=110 ;; "$SUB_CALCOM_STUDIO") order_num=110 ;; "$SUB_RALLLY") order_num=111 ;; "$SUB_EASYAPPOINTMENTS") order_num=112 ;; "$SUB_OPENPROJECT_APP") order_num=113 ;; "$SUB_ZAMMAD_APP") order_num=114 ;; "$SUB_ZULIP_APP") order_num=115 ;; "$SUB_KILLBILL_WEB") order_num=116 ;; "$SUB_INVOICESHELF_APP") order_num=117 ;; "$SUB_INVOICENINJA_WEB") order_num=118 ;; "$SUB_DOLIBARR_APP") order_num=119 ;; "$SUB_N8N_APP") order_num=120 ;; "$SUB_AUTOMATISCH_APP") order_num=121 ;; "$SUB_ACTIVEPIECES_APP") order_num=122 ;; "$SUB_BESZEL_APP") order_num=123 ;; "$SUB_TAIGA_APP") order_num=124 ;; "$SUB_OPENSIGN_APP") order_num=125 ;; "$SUB_DOCUSEAL_APP") order_num=126 ;; "$SUB_CONTROLR_APP") order_num=127 ;; "$SUB_CONTROLR_ASPIRE") order_num=128 ;; "$SUB_AKAUNTING_APP") order_num=129 ;; "$SUB_AXELOR_APP") order_num=130 ;; "$SUB_AXELOR_GOOVEE") order_num=131 ;; "$SUB_CONVERTX_APP") order_num=132 ;; "$SUB_KOPIA_APP") order_num=133 ;; "$SUB_OLLAMA_SERVER") order_num=134 ;; "$SUB_OPENWEBUI_APP") order_num=135 ;; "$SUB_OPENWEBUI_QDRANT") order_num=135 ;; "$SUB_IVBOX_QWEN") order_num=136 ;; "$SUB_IVBOX_COMFYUI_WEB") order_num=136 ;; "$SUB_SPEAKR_APP") order_num=137 ;; "$SUB_OPENNOTEBOOK_APP") order_num=138 ;; "$SUB_LOCALAI_RECALL") order_num=139 ;; "$SUB_LOCALAI_SERVER") order_num=140 ;; "$SUB_LOCALAI_AGI") order_num=141 ;; "$SUB_COMFYUI_APP") order_num=142 ;; "$SUB_LANGFLOW_APP") order_num=143 ;; "$SUB_ANYTHINGLLM_APP") order_num=144 ;; "$SUB_PERPLEXICA_APP") order_num=145 ;; "$SUB_FIRECRAWL_API") order_num=146 ;; "$SUB_LIBRECHAT_APP") order_num=147 ;; "$SUB_CRAWL4AI_APP") order_num=148 ;; "$SUB_KHOJ_SERVER") order_num=149 ;; "$SUB_LOBECHAT_APP") order_num=150 ;; "$SUB_INVOKEAI_APP") order_num=151 ;; "$SUB_RAGFLOW_APP") order_num=152 ;; "$SUB_RAGFLOW_RUSTFS") order_num=152 ;; "$SUB_TABBYML_APP") order_num=153 ;; "$SUB_DEEPWIKI_OPEN_APP") order_num=154 ;; "$SUB_DOCLING_APP") order_num=155 ;; "$SUB_DIFY_APP") order_num=156 ;; "$SUB_MINDSDB_APP") order_num=157 ;; "$SUB_WATERCRAWL_APP") order_num=158 ;; "$SUB_FLOWISE_APP") order_num=159 ;; "$SUB_SURFSENSE_APP") order_num=160 ;; "$SUB_NOCODB_APP") order_num=161 ;; "$SUB_DRAWIO_NEXTAI") order_num=162 ;; "$SUB_MORPHIC_APP") order_num=163 ;; "$SUB_MORPHIC_SUPABASE") order_num=163 ;; "$SUB_APPSMITH_APP") order_num=164 ;; "$SUB_TRILIUM_APP") order_num=165 ;; "$SUB_DOCSGPT_APP") order_num=166 ;; "$SUB_MEMOS_APP") order_num=167 ;; "$SUB_SILLYTAVERN_APP") order_num=170 ;; "$SUB_LEMONADE_SERVER") order_num=171 ;; "$SUB_INSANELYFASTWHISPER_APP") order_num=172 ;; "$SUB_MONICA_APP") order_num=173 ;; "$SUB_AFFINE_APP") order_num=174 ;; "$SUB_JOPLIN_APP") order_num=175 ;; "$SUB_SUPERSET_APP") order_num=176 ;; "$SUB_ENTE_APP") order_num=177 ;; "$SUB_KOKORO_WEB") order_num=178 ;; "$SUB_CHATTERBOX_APP") order_num=179 ;; "$SUB_LITELLM_APP") order_num=180 ;; "$SUB_LANGFUSE_APP") order_num=181 ;; "$SUB_SKYVERN_API") order_num=182 ;; "$SUB_SKYVERN_ARTIFACT") order_num=183 ;; "$SUB_SKYVERN_WEB") order_num=184 ;; "$SUB_SKYVERN_BROWSER") order_num=185 ;; "$SUB_WGER_APP") order_num=186 ;; "$SUB_WORKOUTCOOL_APP") order_num=187 ;; "$SUB_OPENRAG_APP") order_num=188 ;; "$SUB_VOICEBOX_APP") order_num=189 ;; "$SUB_OPENCODE_CODESERVER") order_num=190 ;; "$SUB_OPENCODE_APP") order_num=191 ;; "$SUB_OPENSKILLS_APP") order_num=192 ;; "$SUB_EMAILCLASSIFIERAI_APP") order_num=193 ;; "$SUB_HERMES_AGENT_DASHBOARD") order_num=194 ;; "$SUB_HERMES_AGENT_WEBUI") order_num=195 ;; "$SUB_AUTOKB_WEB") order_num=196 ;; "$SUB_SUITECRM_APP") order_num=197 ;; "$SUB_HEDGEDOC_APP") order_num=198 ;; "$SUB_PRESENTON_APP") order_num=199 ;; "$SUB_BASICMEMORY_APP") order_num=200 ;; "$SUB_COGNEE_APP") order_num=201 ;; "$SUB_COGNEE_FRONTEND") order_num=202 ;; "$SUB_LIGHTRAG_APP") order_num=203 ;; "$SUB_LIGHTRAG_QDRANT") order_num=204 ;; "$SUB_OPENSERP_APP") order_num=205 ;; "$SUB_SURICATA_APP") order_num=206 ;; "$SUB_CROWDSEC_WEB") order_num=207 ;; "$SUB_FASTCRW_APP") order_num=208 ;; "$SUB_WHITEBOARDRT_WEB") order_num=209 ;; "$SUB_WHITEBOARDRT_AGENT") order_num=209 ;; "$SUB_MDCOLLAB_APP") order_num=210 ;; "$SUB_HALOSPEAK_APP") order_num=211 ;; #ADD_NEW_HEIMDALL_ORDER_HERE "$SUB_ADGUARD.$INT_DOMAIN_PREFIX") order_num=900 ;; "$SUB_CLIENTDNS.$INT_DOMAIN_PREFIX") order_num=901 ;; "$SUB_PORTAINER.$INT_DOMAIN_PREFIX") order_num=902 ;; "$SUB_RSPAMD.$INT_DOMAIN_PREFIX") order_num=903 ;; "$SUB_SYNCTHING.$INT_DOMAIN_PREFIX") order_num=904 ;; "$SUB_CADDYDNS.$INT_DOMAIN_PREFIX") order_num=905 ;; "$SUB_WGPORTAL.$INT_DOMAIN_PREFIX") order_num=906 ;; "$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX") order_num=907 ;; "$SUB_EXAMPLESERVICE") order_num=999 ;; "$SUB_HSHQHOME") order_num=1000 ;; "$SUB_AUTHELIA") order_num=1001 ;; *) order_num=999 ;; esac echo "$order_num" } function getLetsEncryptCertsDefault() { echo "$SUB_JITSI.$HOMESERVER_DOMAIN,$SUB_MASTODON.$HOMESERVER_DOMAIN,$SUB_MATRIX_SYNAPSE.$HOMESERVER_DOMAIN,$SUB_JELLYFIN.$HOMESERVER_DOMAIN,$SUB_GITEA.$HOMESERVER_DOMAIN,$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN,$SUB_FRESHRSS.$HOMESERVER_DOMAIN,$SUB_WALLABAG.$HOMESERVER_DOMAIN,$SUB_GRAMPSWEB.$HOMESERVER_DOMAIN,$SUB_IMMICH_APP.$HOMESERVER_DOMAIN,$SUB_OMBI_APP.$HOMESERVER_DOMAIN,$SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN,$SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN,$SUB_STANDARDNOTES_FILES.$HOMESERVER_DOMAIN" } function initServiceDefaults() { #INIT_SERVICE_DEFAULTS_BEGIN HSHQ_REQUIRED_STACKS=adguard,authelia,duplicati,heimdall,mailu,openldap,portainer,syncthing,ofelia,uptimekuma HSHQ_OPTIONAL_STACKS=vaultwarden,sysutils,beszel,wazuh,jitsi,collabora,nextcloud,matrix,mastodon,dozzle,searxng,jellyfin,filebrowser,photoprism,guacamole,codeserver,ghost,wikijs,wordpress,peertube,homeassistant,gitlab,shlink,firefly,excalidraw,drawio,invidious,gitea,mealie,kasm,ntfy,ittools,remotely,calibre,netdata,linkwarden,stirlingpdf,bar-assistant,freshrss,keila,wallabag,jupyter,paperless,speedtest-tracker-local,speedtest-tracker-vpn,changedetection,huginn,coturn,filedrop,piped,grampsweb,penpot,espocrm,immich,homarr,matomo,pastefy,snippetbox,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,adminer,budibase,audiobookshelf,standardnotes,metabase,kanboard,wekan,revolt,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,easyappointments,openproject,zammad,zulip,invoiceshelf,invoiceninja,dolibarr,n8n,automatisch,activepieces,dbgate,sqlpad,taiga,opensign,docuseal,controlr,convertx,kopia,localai,langflow,anythingllm,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,opennotebook,appsmith,trilium,memos,lemonade,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,speakr,insanelyfastwhisper,ivbox,wger,workoutcool,voicebox,opencode,emailclassifierai,hermes-agent,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak DS_MEM_LOW=minimal DS_MEM_12=gitlab,discourse,netdata,jupyter,paperless,speedtest-tracker-local,speedtest-tracker-vpn,huginn,grampsweb,drawio,firefly,shlink,homeassistant,wordpress,ghost,wikijs,guacamole,searxng,excalidraw,invidious,jitsi,jellyfin,peertube,photoprism,sysutils,wazuh,gitea,mealie,kasm,bar-assistant,remotely,calibre,linkwarden,stirlingpdf,freshrss,keila,wallabag,changedetection,piped,penpot,espocrm,immich,homarr,matomo,pastefy,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,adminer,budibase,audiobookshelf,standardnotes,metabase,kanboard,wekan,revolt,frappe-hr,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,easyappointments,openproject,zammad,zulip,killbill,invoiceshelf,invoiceninja,dolibarr,n8n,automatisch,activepieces,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak DS_MEM_16=gitlab,discourse,netdata,jupyter,paperless,speedtest-tracker-local,speedtest-tracker-vpn,huginn,grampsweb,drawio,firefly,shlink,homeassistant,wordpress,ghost,wikijs,guacamole,searxng,excalidraw,invidious,peertube,photoprism,wazuh,gitea,mealie,kasm,bar-assistant,remotely,calibre,linkwarden,stirlingpdf,freshrss,keila,wallabag,changedetection,piped,penpot,espocrm,immich,homarr,matomo,pastefy,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,adminer,budibase,audiobookshelf,standardnotes,metabase,kanboard,wekan,revolt,frappe-hr,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,openproject,zammad,zulip,killbill,invoiceshelf,invoiceninja,dolibarr,n8n,automatisch,activepieces,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak DS_MEM_22=gitlab,discourse,netdata,jupyter,paperless,speedtest-tracker-local,speedtest-tracker-vpn,huginn,grampsweb,drawio,firefly,shlink,homeassistant,wordpress,ghost,wikijs,guacamole,searxng,invidious,peertube,photoprism,wazuh,gitea,kasm,remotely,calibre,stirlingpdf,keila,piped,penpot,espocrm,homarr,matomo,pastefy,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,adminer,budibase,audiobookshelf,standardnotes,metabase,kanboard,wekan,revolt,frappe-hr,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,openproject,zammad,zulip,killbill,invoiceshelf,invoiceninja,dolibarr,n8n,automatisch,activepieces,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak DS_MEM_28=gitlab,discourse,netdata,jupyter,huginn,grampsweb,drawio,invidious,photoprism,wazuh,kasm,penpot,espocrm,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,adminer,budibase,audiobookshelf,standardnotes,metabase,kanboard,wekan,revolt,frappe-hr,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,openproject,zammad,zulip,killbill,invoiceshelf,invoiceninja,dolibarr,n8n,automatisch,activepieces,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak DS_MEM_HIGH=discourse,netdata,photoprism,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,adminer,budibase,audiobookshelf,standardnotes,metabase,kanboard,wekan,revolt,frappe-hr,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,openproject,zammad,zulip,killbill,invoiceshelf,invoiceninja,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak BDS_MEM_12=sysutils,wazuh,jitsi,matrix,mastodon,searxng,jellyfin,photoprism,guacamole,ghost,wikijs,peertube,homeassistant,gitlab,discourse,shlink,firefly,drawio,invidious,gitea,mealie,kasm,ntfy,remotely,calibre,netdata,linkwarden,bar-assistant,freshrss,wallabag,jupyter,speedtest-tracker-local,speedtest-tracker-vpn,huginn,filedrop,piped,grampsweb,penpot,espocrm,immich,homarr,matomo,pastefy,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,adminer,budibase,audiobookshelf,standardnotes,metabase,wekan,revolt,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,openproject,zammad,zulip,killbill,invoiceshelf,invoiceninja,dolibarr,n8n,automatisch,activepieces,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak BDS_MEM_16=wazuh,jitsi,matrix,mastodon,searxng,jellyfin,photoprism,guacamole,ghost,wikijs,peertube,homeassistant,gitlab,discourse,shlink,drawio,invidious,gitea,mealie,kasm,ntfy,remotely,calibre,netdata,bar-assistant,freshrss,wallabag,jupyter,speedtest-tracker-local,speedtest-tracker-vpn,huginn,filedrop,piped,grampsweb,immich,homarr,matomo,pastefy,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,meshcentral,navidrome,budibase,audiobookshelf,standardnotes,metabase,wekan,revolt,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,openproject,zammad,zulip,killbill,invoiceshelf,invoiceninja,n8n,automatisch,activepieces,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak BDS_MEM_22=wazuh,matrix,mastodon,searxng,jellyfin,photoprism,peertube,homeassistant,gitlab,discourse,drawio,invidious,mealie,kasm,remotely,calibre,netdata,bar-assistant,freshrss,wallabag,jupyter,speedtest-tracker-local,speedtest-tracker-vpn,filedrop,piped,grampsweb,immich,homarr,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,navidrome,audiobookshelf,standardnotes,wekan,revolt,minthcm,cloudbeaver,twenty,odoo,calcom,rallly,openproject,zammad,zulip,killbill,invoiceninja,n8n,automatisch,activepieces,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak BDS_MEM_28=matrix,mastodon,jellyfin,photoprism,peertube,homeassistant,gitlab,discourse,drawio,invidious,mealie,kasm,calibre,netdata,bar-assistant,freshrss,wallabag,jupyter,speedtest-tracker-local,speedtest-tracker-vpn,filedrop,piped,grampsweb,immich,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,navidrome,audiobookshelf,revolt,calcom,rallly,killbill,invoiceninja,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak BDS_MEM_HIGH=mastodon,jellyfin,photoprism,peertube,homeassistant,gitlab,discourse,invidious,mealie,kasm,calibre,netdata,bar-assistant,freshrss,piped,grampsweb,immich,pixelfed,yamtrack,servarr,sabnzbd,qbittorrent,ombi,navidrome,audiobookshelf,rallly,killbill,taiga,opensign,docuseal,controlr,akaunting,axelor,convertx,kopia,localai,comfyui,langflow,anythingllm,perplexica,firecrawl,librechat,crawl4ai,ollama,openwebui,khoj,lobechat,invokeai,ragflow,tabbyml,deepwikiopen,docling,dify,mindsdb,watercrawl,flowise,nocodb,surfsense,ente,morphic,opennotebook,appsmith,trilium,docsgpt,memos,sillytavern,lemonade,speakr,insanelyfastwhisper,ivbox,monica,affine,joplin,superset,kokoro,chatterbox,litellm,langfuse,skyvern,wger,workoutcool,openrag,voicebox,opencode,openskills,emailclassifierai,hermes-agent,autokb,suitecrm,hedgedoc,presenton,basicmemory,cognee,lightrag,openserp,suricata,crowdsec,fastcrw,whiteboardrt,mdcollab,halospeak #INIT_SERVICE_DEFAULTS_END if [ "$IS_HSHQ_DEV_TEST" = "true" ]; then HSHQ_OPTIONAL_STACKS=${HSHQ_OPTIONAL_STACKS},surfsense,ente,comfyui,perplexica,morphic,skyvern,openrag,openskills,sillytavern,suitecrm fi } function getScriptImageByContainerName() { imgOverride=$(getImageOverride "$1") if ! [ -z "$imgOverride" ]; then echo "$imgOverride" return fi case "$1" in "portainer") container_image=$IMG_PORTAINER ;; "adguard") container_image=$IMG_ADGUARD ;; "grafana") container_image=$IMG_GRAFANA ;; "prometheus") container_image=$IMG_PROMETHEUS ;; "node-exporter") container_image=$IMG_NODE_EXPORTER ;; "influxdb") container_image=$IMG_INFLUXDB ;; "loki") container_image=$IMG_LOKI ;; "alloy-app") container_image=$IMG_ALLOY ;; "alloy-web") container_image=mirror.gcr.io/caddy:2.11.4 ;; "ldapserver") container_image=$IMG_OPENLDAP_SERVER ;; "ldapphp") container_image=$IMG_OPENLDAP_PHP ;; "ldapmanager") container_image=$IMG_OPENLDAP_MANAGER ;; "mailu-front") container_image=$IMG_MAILU_FRONT ;; "mailu-unbound") container_image=$IMG_MAILU_UNBOUND ;; "mailu-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "mailu-admin") container_image=$IMG_MAILU_ADMIN ;; "mailu-imap") container_image=$IMG_MAILU_IMAP ;; "mailu-smtp") container_image=$IMG_MAILU_SMTP ;; "mailu-oletools") container_image=$IMG_MAILU_OLETOOLS ;; "mailu-tika") container_image=$IMG_MAILU_TIKA ;; "mailu-antispam") container_image=$IMG_MAILU_ANTISPAM ;; "mailu-antivirus") container_image=$IMG_MAILU_ANTIVIRUS ;; "mailu-webdav") container_image=$IMG_MAILU_WEBDAV ;; "mailu-fetchmail") container_image=$IMG_MAILU_FETCHMAIL ;; "mailu-webmail") container_image=$IMG_MAILU_WEBMAIL ;; "caddy") container_image=$IMG_CADDY ;; "wazuh.manager") container_image=$IMG_WAZUH_MANAGER ;; "wazuh.indexer") container_image=$IMG_WAZUH_INDEXER ;; "wazuh.dashboard") container_image=$IMG_WAZUH_DASHBOARD ;; "collabora") container_image=$IMG_COLLABORA ;; "nextcloud-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "nextcloud-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "nextcloud-app") container_image=$IMG_NEXTCLOUD_APP ;; "nextcloud-cron") container_image=$IMG_NEXTCLOUD_APP ;; "nextcloud-push") container_image=$IMG_NEXTCLOUD_APP ;; "nextcloud-imaginary") container_image=$IMG_NEXTCLOUD_IMAGINARY ;; "nextcloud-web") container_image=mirror.gcr.io/nginx:1.31.2-alpine ;; "nextcloud-harp") container_image=$IMG_NEXTCLOUD_HARP ;; "nextcloud-talkhpb") container_image=$IMG_NEXTCLOUD_TALKHPB ;; "nextcloud-talkrecord") container_image=$IMG_NEXTCLOUD_TALKRECORD ;; "nextcloud-mcp") container_image=$IMG_NEXTCLOUD_MCP ;; "jitsi-web") container_image=$IMG_JITSI_WEB ;; "jitsi-prosody") container_image=$IMG_JITSI_PROSODY ;; "jitsi-jicofo") container_image=$IMG_JITSI_JICOFO ;; "jitsi-jvb") container_image=$IMG_JITSI_JVB ;; "matrix-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "matrix-synapse") container_image=$IMG_MATRIX_SYNAPSE ;; "matrix-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "matrix-element-private") container_image=$IMG_MATRIX_ELEMENT ;; "matrix-element-public") container_image=$IMG_MATRIX_ELEMENT ;; "wikijs-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "wikijs-web") container_image=$IMG_WIKIJS_APP ;; "wikijs-mcp") container_image=$IMG_WIKIJS_MCP ;; "duplicati") container_image=$IMG_DUPLICATI ;; "mastodon-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "mastodon-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "mastodon-redis-cache") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "mastodon-app") container_image=$IMG_MASTODON_APP ;; "mastodon-streaming") container_image=$IMG_MASTODON_STREAMING ;; "mastodon-sidekiq") container_image=$IMG_MASTODON_APP ;; "mastodon-web") container_image=mirror.gcr.io/nginx:1.29.3-alpine ;; "mastodon-elasticsearch") container_image=mirror.gcr.io/elasticsearch:9.2.2 ;; "dozzle") container_image=$IMG_DOZZLE ;; "searxng-caddy") container_image=mirror.gcr.io/caddy:2.11.4 ;; "searxng-app") container_image=$IMG_SEARXNG_APP ;; "searxng-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "searxng-mcp") container_image=$IMG_SEARXNG_MCP ;; "jellyfin") container_image=$IMG_JELLYFIN ;; "filebrowser") container_image=$IMG_FILEBROWSER ;; "photoprism-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "photoprism-app") container_image=$IMG_PHOTOPRISM_APP ;; "guacamole-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "guacamole-daemon") container_image=$IMG_GUACAMOLE_GUACD ;; "guacamole-web") container_image=$IMG_GUACAMOLE_WEB ;; "authelia") container_image=$IMG_AUTHELIA ;; "authelia-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "wordpress-db") container_image=mirror.gcr.io/mariadb:11.4.12 ;; "wordpress-web") container_image=$IMG_WORDPRESS_APP ;; "wordpress-cli") container_image=$IMG_WORDPRESS_CLI ;; "wordpress-mcp") container_image=$IMG_WORDPRESS_MCP ;; "ghost-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "ghost-web") container_image=$IMG_GHOST ;; "peertube-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "peertube-app") container_image=$IMG_PEERTUBE_APP ;; "peertube-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "homeassistant-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "homeassistant-app") container_image=$IMG_HOMEASSISTANT_APP ;; "homeassistant-nodered") container_image=$IMG_HOMEASSISTANT_NODERED ;; "homeassistant-configurator") container_image=$IMG_HOMEASSISTANT_CONFIGURATOR ;; "homeassistant-tasmoadmin") container_image=$IMG_HOMEASSISTANT_TASMOADMIN ;; "homeassistant-mcp") container_image=$IMG_HOMEASSISTANT_MCP ;; "gitlab-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "gitlab-app") container_image=$IMG_GITLAB_APP ;; "gitlab-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "vaultwarden-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "vaultwarden-app") container_image=$IMG_VAULTWARDEN_APP ;; "vaultwarden-ldap") container_image=$IMG_VAULTWARDEN_LDAP ;; "discourse-db") container_image=mirror.gcr.io/postgres:17.5-bookworm ;; "discourse-app") container_image=$IMG_DISCOURSE ;; "discourse-sidekiq") container_image=$IMG_DISCOURSE ;; "discourse-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "syncthing") container_image=$IMG_SYNCTHING ;; "codeserver") container_image=$IMG_CODESERVER ;; "shlink-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "shlink-app") container_image=$IMG_SHLINK_APP ;; "shlink-web") container_image=$IMG_SHLINK_WEB ;; "shlink-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "firefly-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "firefly-app") container_image=$IMG_FIREFLY_APP ;; "firefly-importer") container_image=$IMG_FIREFLY_IMPORTER ;; "firefly-fints-importer") container_image=$IMG_FIREFLY_FINTSIMPORTER ;; "firefly-cron") container_image=mirror.gcr.io/alpine:3.23.0 ;; "firefly-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "excalidraw-storage") container_image=$IMG_EXCALIDRAW_STORAGE ;; "excalidraw-server") container_image=$IMG_EXCALIDRAW_SERVER ;; "excalidraw-web") container_image=$IMG_EXCALIDRAW_WEB ;; "excalidraw-ai") container_image=$IMG_EXCALIDRAW_AI ;; "excalidraw-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "excalidraw-mcp") container_image=$IMG_EXCALIDRAW_MCP ;; "drawio-plantuml") container_image=$IMG_DRAWIO_PLANTUML ;; "drawio-export") container_image=$IMG_DRAWIO_EXPORT ;; "drawio-web") container_image=$IMG_DRAWIO_WEB ;; "drawio-nextai") container_image=$IMG_DRAWIO_NEXTAI ;; "invidious-db") container_image=postgres:15.0-bullseye ;; "invidious-web") container_image=$IMG_INVIDIOUS_WEB ;; "invidious-companion") container_image=$IMG_INVIDIOUS_COMPANION ;; "gitea-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "gitea-app") container_image=$IMG_GITEA_APP ;; "gitea-mcp") container_image=$IMG_GITEA_MCP ;; "mealie-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "mealie-app") container_image=$IMG_MEALIE_APP ;; "mealie-mcp") container_image=$IMG_MEALIE_MCP ;; "kasm") container_image=$IMG_KASM ;; "ntfy") container_image=$IMG_NTFY ;; "ittools") container_image=$IMG_ITTOOLS ;; "remotely") container_image=$IMG_REMOTELY ;; "calibre-server") container_image=$IMG_CALIBRE_SERVER ;; "calibre-web") container_image=$IMG_CALIBRE_WEB ;; "netdata") container_image=$IMG_NETDATA ;; "linkwarden-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "linkwarden-app") container_image=$IMG_LINKWARDEN_APP ;; "linkwarden-mcp") container_image=$IMG_LINKWARDEN_MCP ;; "stirlingpdf") container_image=$IMG_STIRLINGPDF ;; "bar-assistant-app") container_image=$IMG_BARASSISTANT_APP ;; "bar-assistant-meilisearch") container_image=mirror.gcr.io/getmeili/meilisearch:v1.49.0 ;; "bar-assistant-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "bar-assistant-saltrim") container_image=$IMG_BARASSISTANT_SALTRIM ;; "bar-assistant-web") container_image=mirror.gcr.io/nginx:1.31.2-alpine ;; "bar-assistant-mcp") container_image=$IMG_BARASSISTANT_MCP ;; "freshrss-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "freshrss-app") container_image=$IMG_FRESHRSS ;; "keila-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "keila-app") container_image=$IMG_KEILA_APP ;; "keila-mcp") container_image=$IMG_KEILA_MCP ;; "wallabag-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "wallabag-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "wallabag-app") container_image=$IMG_WALLABAG ;; "jupyter") container_image=$IMG_JUPYTER ;; "paperless-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "paperless-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "paperless-app") container_image=$IMG_PAPERLESS_APP ;; "paperless-gotenberg") container_image=$IMG_PAPERLESS_GOTENBERG ;; "paperless-tika") container_image=$IMG_PAPERLESS_TIKA ;; "paperless-ai") container_image=$IMG_PAPERLESS_AI ;; "paperless-gpt") container_image=$IMG_PAPERLESS_GPT ;; "paperless-mcp") container_image=$IMG_PAPERLESS_MCP ;; "speedtest-tracker-local-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "speedtest-tracker-local-app") container_image=$IMG_SPEEDTEST_TRACKER_APP ;; "speedtest-tracker-vpn-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "speedtest-tracker-vpn-app") container_image=$IMG_SPEEDTEST_TRACKER_APP ;; "changedetection-app") container_image=$IMG_CHANGEDETECTION_APP ;; "changedetection-playwright-chrome") container_image=$IMG_CHANGEDETECTION_PLAYWRIGHT_CHROME ;; "huginn-db") container_image=postgres:15.0-bullseye ;; "huginn-app") container_image=$IMG_HUGINN_APP ;; "coturn") container_image=$IMG_COTURN ;; "filedrop") container_image=$IMG_FILEDROP ;; "piped-db") container_image=postgres:15.0-bullseye ;; "piped-frontend") container_image=$IMG_PIPED_FRONTEND ;; "piped-proxy") container_image=$IMG_PIPED_PROXY ;; "piped-api") container_image=$IMG_PIPED_API ;; "piped-cron") container_image=$IMG_PIPED_CRON ;; "piped-web") container_image=nginx:1.27.4-alpine ;; "sqlpad") container_image=$IMG_SQLPAD ;; "heimdall") container_image=$IMG_HEIMDALL ;; "ofelia") container_image=$IMG_OFELIA ;; "uptimekuma") container_image=$IMG_UPTIMEKUMA ;; "grampsweb-app") container_image=$IMG_GRAMPSWEB ;; "grampsweb-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "penpot-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "penpot-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "penpot-backend") container_image=$IMG_PENPOT_BACKEND ;; "penpot-frontend") container_image=$IMG_PENPOT_FRONTEND ;; "penpot-exporter") container_image=$IMG_PENPOT_EXPORTER ;; "espocrm-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "espocrm-app") container_image=$IMG_ESPOCRM_APP ;; "espocrm-daemon") container_image=$IMG_ESPOCRM_APP ;; "espocrm-websocket") container_image=$IMG_ESPOCRM_APP ;; "immich-db") container_image=$IMG_IMMICH_DB ;; "immich-app") container_image=$IMG_IMMICH_APP ;; "immich-ml") container_image=$IMG_IMMICH_ML ;; "immich-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "immich-mcp") container_image=$IMG_IMMICH_MCP ;; "homarr-app") container_image=$IMG_HOMARR_APP ;; "matomo-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "matomo-app") container_image=$IMG_MATOMO_APP ;; "matomo-web") container_image=mirror.gcr.io/nginx:1.29.3-alpine ;; "pastefy-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "pastefy-app") container_image=$IMG_PASTEFY ;; "snippetbox") container_image=$IMG_SNIPPETBOX ;; "aistack-mindsdb-app") container_image=$IMG_AISTACK_MINDSDB_APP ;; "aistack-mindsdb-mod-app") container_image=$IMG_AISTACK_MINDSDB_MOD_APP ;; "aistack-mindsdb-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "aistack-otel") container_image=$IMG_AISTACK_OPENTELEMETRY ;; "aistack-langfuse") container_image=$IMG_AISTACK_LANGFUSE ;; "aistack-openwebui") container_image=$IMG_AISTACK_OPENWEBUI ;; "aistack-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "pixelfed-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "pixelfed-app") container_image=$IMG_PIXELFED_MOD_APP ;; "pixelfed-worker") container_image=$IMG_PIXELFED_MOD_APP ;; "pixelfed-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "yamtrack-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "yamtrack-app") container_image=$IMG_YAMTRACK_APP ;; "yamtrack-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "servarr-sonarr") container_image=$IMG_SERVARR_SONARR ;; "servarr-radarr") container_image=$IMG_SERVARR_RADARR ;; "servarr-lidarr") container_image=$IMG_SERVARR_LIDARR ;; "servarr-readarr") container_image=$IMG_SERVARR_READARR ;; "servarr-bazarr") container_image=$IMG_SERVARR_BAZARR ;; "servarr-mylar3") container_image=$IMG_SERVARR_MYLAR3 ;; "servarr-prowlarr") container_image=$IMG_SERVARR_PROWLARR ;; "sabnzbd") container_image=$IMG_SABNZBD ;; "qbittorrent") container_image=$IMG_QBITTORRENT ;; "ombi-db") container_image=mariadb:10.7.3 ;; "ombi-app") container_image=$IMG_OMBI_APP ;; "meshcentral-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "meshcentral-app") container_image=$IMG_MESHCENTRAL ;; "navidrome-app") container_image=$IMG_NAVIDROME ;; "adminer-db") container_image=mirror.gcr.io/mariadb:12.0.2-ubi10 ;; "adminer-app") container_image=$IMG_ADMINER ;; "budibase-db") container_image=mirror.gcr.io/mariadb:12.0.2-ubi10 ;; "budibase-couchdb") container_image=$IMG_BUDIBASE_COUCHDB ;; "budibase-app") container_image=$IMG_BUDIBASE_APP ;; "budibase-worker") container_image=$IMG_BUDIBASE_WORKER ;; "budibase-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "budibase-proxy") container_image=$IMG_BUDIBASE_PROXY ;; "budibase-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "audiobookshelf-app") container_image=$IMG_AUDIOBOOKSHELF ;; "standardnotes-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "standardnotes-server") container_image=$IMG_STANDARDNOTES_SERVER ;; "standardnotes-localstack") container_image=$IMG_STANDARDNOTES_LOCALSTACK ;; "standardnotes-web") container_image=$IMG_STANDARDNOTES_WEB ;; "standardnotes-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "metabase-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "metabase-app") container_image=$IMG_METABASE ;; "kanboard-db") container_image=mirror.gcr.io/postgres:17.6 ;; "kanboard-app") container_image=$IMG_KANBOARD_APP ;; "wekan-db") container_image=mirror.gcr.io/mongo:8.0.13 ;; "wekan-app") container_image=$IMG_WEKAN_APP ;; "revolt-db") container_image=mirror.gcr.io/mongo:8.0.13 ;; "revolt-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "revolt-rabbitmq") container_image=mirror.gcr.io/rabbitmq:4.1.4 ;; "revolt-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "revolt-caddy") container_image=mirror.gcr.io/caddy:2.10.0 ;; "revolt-api") container_image=$IMG_REVOLT_API ;; "revolt-events") container_image=$IMG_REVOLT_EVENTS ;; "revolt-web") container_image=$IMG_REVOLT_WEB ;; "revolt-autumn") container_image=$IMG_REVOLT_AUTUMN ;; "revolt-january") container_image=$IMG_REVOLT_JANUARY ;; "revolt-crond") container_image=$IMG_REVOLT_CROND ;; "revolt-pushd") container_image=$IMG_REVOLT_PUSHD ;; "frappe-hr-db") container_image=mirror.gcr.io/mariadb:10.8.8 ;; "frappe-hr-app") container_image=mirror.gcr.io/frappe/bench:v5.25.9 ;; "frappe-hr-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "minthcm-db") container_image=mirror.gcr.io/percona:8.0.43-34 ;; "minthcm-web") container_image=$IMG_MINTHCM_WEB ;; "minthcm-es") container_image=$IMG_MINTHCM_ELASTICSEARCH ;; "cloudbeaver-app") container_image=$IMG_CLOUDBEAVER_APP ;; "dbgate-app") container_image=$IMG_DBGATE_APP ;; "twenty-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "twenty-app") container_image=$IMG_TWENTY_APP ;; "twenty-worker") container_image=$IMG_TWENTY_APP ;; "twenty-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "twenty-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "twenty-mcp") container_image=$IMG_TWENTY_MCP ;; "odoo-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "odoo-app") container_image=$IMG_ODOO_APP ;; "calcom-db") container_image=mirror.gcr.io/postgres:17.6 ;; "calcom-app") container_image=$IMG_CALCOM ;; "calcom-studio") container_image=$IMG_CALCOM ;; "rallly-db") container_image=mirror.gcr.io/postgres:17.6 ;; "rallly-app") container_image=$IMG_RALLLY_APP ;; "rallly-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "easyappointments-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "easyappointments-app") container_image=$IMG_EASYAPPOINTMENTS_APP ;; "openproject-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "openproject-web") container_image=$IMG_OPENPROJECT_APP ;; "openproject-worker") container_image=$IMG_OPENPROJECT_APP ;; "openproject-cron") container_image=$IMG_OPENPROJECT_APP ;; "openproject-seeder") container_image=$IMG_OPENPROJECT_APP ;; "openproject-memcache") container_image=mirror.gcr.io/memcached:1.6.45-alpine ;; "openproject-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "openproject-mcp") container_image=$IMG_OPENPROJECT_MCP ;; "zammad-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "zammad-init") container_image=$IMG_ZAMMAD ;; "zammad-backup") container_image=$IMG_ZAMMAD ;; "zammad-nginx") container_image=$IMG_ZAMMAD ;; "zammad-railsserver") container_image=$IMG_ZAMMAD ;; "zammad-scheduler") container_image=$IMG_ZAMMAD ;; "zammad-websocket") container_image=$IMG_ZAMMAD ;; "zammad-memcache") container_image=mirror.gcr.io/memcached:1.6.39-alpine ;; "zammad-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "zammad-es") container_image=mirror.gcr.io/elasticsearch:9.2.2 ;; "zammad-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "zulip-db") container_image=$IMG_ZULIP_DB ;; "zulip-app") container_image=$IMG_ZULIP_APP ;; "zulip-memcache") container_image=mirror.gcr.io/memcached:1.6.39-alpine ;; "zulip-rabbitmq") container_image=mirror.gcr.io/rabbitmq:4.2.1 ;; "zulip-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "killbill-db") container_image=$IMG_KILLBILL_DB ;; "killbill-app") container_image=$IMG_KILLBILL_APP ;; "killbill-web") container_image=$IMG_KILLBILL_WEB ;; "killbill-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "invoiceshelf-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "invoiceshelf-app") container_image=$IMG_INVOICESHELF_APP ;; "invoiceshelf-mcp") container_image=$IMG_INVOICESHELF_MCP ;; "invoiceninja-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "invoiceninja-app") container_image=$IMG_INVOICENINJA_APP ;; "invoiceninja-web") container_image=mirror.gcr.io/nginx:1.29.3-alpine ;; "invoiceninja-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "invoiceninja-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "dolibarr-db") container_image=mirror.gcr.io/mariadb:10.7.3 ;; "dolibarr-app") container_image=$IMG_DOLIBARR_APP ;; "dolibarr-cron") container_image=$IMG_DOLIBARR_APP ;; "dolibarr-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "dolibarr-mcp") container_image=$IMG_DOLIBARR_MCP ;; "n8n-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "n8n-app") container_image=$IMG_N8N_APP ;; "n8n-worker") container_image=$IMG_N8N_APP ;; "n8n-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "automatisch-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "automatisch-app") container_image=$IMG_AUTOMATISCH_APP ;; "automatisch-worker") container_image=$IMG_AUTOMATISCH_APP ;; "automatisch-redis") container_image=mirror.gcr.io/redis:8.2.0-bookworm ;; "activepieces-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "activepieces-app") container_image=$IMG_ACTIVEPIECES_APP ;; "activepieces-redis") container_image=mirror.gcr.io/redis:8.4.0-bookworm ;; "beszel-app") container_image=$IMG_BESZEL_APP ;; "beszel-agent") container_image=$IMG_BESZEL_AGENT ;; "taiga-db") container_image=mirror.gcr.io/postgres:17.6 ;; "taiga-back") container_image=$IMG_TAIGA_BACK ;; "taiga-async") container_image=$IMG_TAIGA_BACK ;; "taiga-async-rabbitmq") container_image=mirror.gcr.io/rabbitmq:4.1.4-management-alpine ;; "taiga-front") container_image=$IMG_TAIGA_FRONT ;; "taiga-events") container_image=$IMG_TAIGA_EVENTS ;; "taiga-events-rabbitmq") container_image=mirror.gcr.io/rabbitmq:4.1.4-management-alpine ;; "taiga-protected") container_image=$IMG_TAIGA_PROTECTED ;; "taiga-gateway") container_image=mirror.gcr.io/nginx:1.28.0-alpine ;; "opensign-db") container_image=mirror.gcr.io/mongo:8.0.13 ;; "opensign-server") container_image=$IMG_OPENSIGN_SERVER ;; "opensign-client") container_image=$IMG_OPENSIGN_CLIENT ;; "docuseal-db") container_image=mirror.gcr.io/postgres:17.6 ;; "docuseal-app") container_image=$IMG_DOCUSEAL_APP ;; "controlr-db") container_image=mirror.gcr.io/postgres:18.0-bookworm ;; "controlr-app") container_image=$IMG_CONTROLR_APP ;; "controlr-aspire") container_image=$IMG_CONTROLR_ASPIRE ;; "akaunting-db") container_image=mirror.gcr.io/mariadb:12.0.2-ubi10 ;; "akaunting-app") container_image=$IMG_AKAUNTING_APP ;; "axelor-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "axelor-app") container_image=$IMG_AXELOR_APP ;; "axelor-goovee") container_image=$IMG_AXELOR_GOOVEE ;; "convertx-app") container_image=$IMG_CONVERTX_APP ;; "kopia-app") container_image=$IMG_KOPIA_APP ;; "localai-server") container_image=$IMG_LOCALAI_SERVER ;; "localai-recall") container_image=$IMG_LOCALAI_RECALL ;; "localai-lrhealth") container_image=$IMG_LOCALAI_LRHEALTH ;; "localai-sshbox") container_image=$IMG_LOCALAI_SSHBOX ;; "localai-dind") container_image=$IMG_LOCALAI_DIND ;; "localai-agi") container_image=$IMG_LOCALAI_AGI ;; "comfyui-app") container_image=$IMG_COMFYUI_APP ;; "langflow-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "langflow-app") container_image=$IMG_LANGFLOW_APP ;; "anythingllm-app") container_image=$IMG_ANYTHINGLLM_APP ;; "perplexica-app") container_image=$IMG_PERPLEXICA_APP ;; "firecrawl-db") container_image=$IMG_FIRECRAWL_DB ;; "firecrawl-playwright") container_image=$IMG_FIRECRAWL_PLAYWRIGHT ;; "firecrawl-api") container_image=$IMG_FIRECRAWL_API ;; "firecrawl-rabbitmq") container_image=mirror.gcr.io/rabbitmq:4.1.4-management-alpine ;; "firecrawl-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "librechat-db") container_image=mirror.gcr.io/pgvector/pgvector:0.8.0-pg15-trixie ;; "librechat-mongodb") container_image=mirror.gcr.io/mongo:8.0.17 ;; "librechat-app") container_image=$IMG_LIBRECHAT_APP ;; "librechat-meilisearch") container_image=mirror.gcr.io/getmeili/meilisearch:v1.31.0 ;; "librechat-ragapi") container_image=$IMG_LIBRECHAT_RAGAPI ;; "librechat-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "crawl4ai-app") container_image=$IMG_CRAWL4AI_APP ;; "crawl4ai-proxy") container_image=$IMG_CRAWL4AI_PROXY ;; "ollama-server") container_image=$IMG_OLLAMA_SERVER ;; "openwebui-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "openwebui-app") container_image=$IMG_OPENWEBUI_APP ;; "openwebui-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "openwebui-qdrant") container_image=mirror.gcr.io/qdrant/qdrant:v1.20.0-unprivileged ;; "openwebui-openterminal") container_image=$IMG_OPENWEBUI_OPENTERMINAL ;; "openwebui-mcpo") container_image=$IMG_OPENWEBUI_MCPO ;; "openwebui-pipelines") container_image=$IMG_OPENWEBUI_PIPELINES ;; "openwebui-mcp") container_image=$IMG_OPENWEBUI_MCP ;; "khoj-db") container_image=mirror.gcr.io/pgvector/pgvector:0.8.1-pg16-bookworm ;; "khoj-sandbox") container_image=$IMG_KHOJ_SANDBOX ;; "khoj-computer") container_image=$IMG_KHOJ_COMPUTER ;; "khoj-server") container_image=$IMG_KHOJ_SERVER ;; "lobechat-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "lobechat-app") container_image=$IMG_LOBECHAT_APP ;; "lobechat-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "lobechat-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "invokeai-app") container_image=$IMG_INVOKEAI_APP ;; "ragflow-db") container_image=mirror.gcr.io/mariadb:11.4.12 ;; "ragflow-infinity") container_image=$IMG_RAGFLOW_INFINITY ;; "ragflow-app") container_image=$IMG_RAGFLOW_APP ;; "ragflow-sandbox") container_image=$IMG_RAGFLOW_SANDBOX ;; "ragflow-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "ragflow-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "ragflow-sandbox-nodejs") container_image=$IMG_RAGFLOW_SB_NODEJS ;; "ragflow-sandbox-python") container_image=$IMG_RAGFLOW_SB_PYTHON ;; "ragflow-rustfs") container_image=mirror.gcr.io/rustfs/rustfs:1.0.4 ;; "ragflow-kvrocks") container_image=mirror.gcr.io/apache/kvrocks:2.16.0 ;; "ragflow-nats") container_image=mirror.gcr.io/nats:2.14.2 ;; "tabbyml-app") container_image=$IMG_TABBYML_APP ;; "deepwikiopen-app") container_image=$IMG_DEEPWIKIOPEN_APP ;; "docling-app") container_image=$IMG_DOCLING_APP ;; "docling-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "dify-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "dify-vectordb") container_image=mirror.gcr.io/langgenius/qdrant:v1.8.3 ;; "dify-ssrf") container_image=$IMG_DIFY_SSRF ;; "dify-api") container_image=$IMG_DIFY_API ;; "dify-worker") container_image=$IMG_DIFY_API ;; "dify-beat") container_image=$IMG_DIFY_API ;; "dify-plugind") container_image=$IMG_DIFY_PLUGIND ;; "dify-sandbox") container_image=$IMG_DIFY_SANDBOX ;; "dify-web") container_image=$IMG_DIFY_WEB ;; "dify-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "dify-nginx") container_image=mirror.gcr.io/nginx:1.29.4-trixie ;; "dify-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "mindsdb-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "mindsdb-app") container_image=$IMG_MINDSDB_APP ;; "mindsdb-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "watercrawl-db") container_image=mirror.gcr.io/postgres:17.5-bookworm ;; "watercrawl-app") container_image=$IMG_WATERCRAWL_APP ;; "watercrawl-celery") container_image=$IMG_WATERCRAWL_APP ;; "watercrawl-beat") container_image=$IMG_WATERCRAWL_APP ;; "watercrawl-playwright") container_image=$IMG_WATERCRAWL_PLAYWRIGHT ;; "watercrawl-frontend") container_image=$IMG_WATERCRAWL_FRONTEND ;; "watercrawl-nginx") container_image=mirror.gcr.io/nginx:1.29.3-alpine ;; "watercrawl-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "watercrawl-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "watercrawl-mcp") container_image=$IMG_WATERCRAWL_MCP ;; "flowise-db") container_image=mirror.gcr.io/postgres:17.6 ;; "flowise-app") container_image=$IMG_FLOWISE_APP ;; "flowise-worker") container_image=$IMG_FLOWISE_WORKER ;; "flowise-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "flowise-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "surfsense-app") container_image=$IMG_SURFSENSE_APP ;; "nocodb-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "nocodb-app") container_image=$IMG_NOCODB_APP ;; "nocodb-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "nocodb-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "ente-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "ente-server") container_image=$IMG_ENTE_SERVER ;; "ente-web") container_image=$IMG_ENTE_WEB ;; "ente-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "morphic-db") container_image=mirror.gcr.io/supabase/postgres:15.14.1.071 ;; "morphic-supabase-vector") container_image=mirror.gcr.io/timberio/vector:0.52.0-alpine ;; "morphic-supabase-studio") container_image=mirror.gcr.io/supabase/studio:2026.01.12-sha-4e9b718 ;; "morphic-supabase-kong") container_image=mirror.gcr.io/kong:2.8.1 ;; "morphic-supabase-auth") container_image=mirror.gcr.io/supabase/gotrue:v2.185.0 ;; "morphic-supabase-rest") container_image=mirror.gcr.io/postgrest/postgrest:v14.3 ;; "morphic-supabase-realtime") container_image=mirror.gcr.io/supabase/realtime:v2.69.2 ;; "morphic-supabase-storage") container_image=mirror.gcr.io/supabase/storage-api:v1.33.5 ;; "morphic-supabase-imgproxy") container_image=mirror.gcr.io/darthsim/imgproxy:v3.30.1 ;; "morphic-supabase-meta") container_image=mirror.gcr.io/supabase/postgres-meta:v0.95.2 ;; "morphic-supabase-functions") container_image=mirror.gcr.io/supabase/edge-runtime:v1.70.0 ;; "morphic-supabase-analytics") container_image=mirror.gcr.io/supabase/logflare:1.28.0 ;; "morphic-supabase-supavisor") container_image=mirror.gcr.io/supabase/supavisor:2.7.4 ;; "morphic-app") container_image=$IMG_MORPHIC_APP ;; "morphic-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "opennotebook-db") container_image=$IMG_OPENNOTEBOOK_DB ;; "opennotebook-app") container_image=$IMG_OPENNOTEBOOK_APP ;; "opennotebook-mcp") container_image=$IMG_OPENNOTEBOOK_MCP ;; "appsmith-app") container_image=$IMG_APPSMITH_APP ;; "trilium-app") container_image=$IMG_TRILIUM ;; "docsgpt-db") container_image=mirror.gcr.io/mongo:8.0.17 ;; "docsgpt-frontend") container_image=$IMG_DOCSGPT_FRONTEND ;; "docsgpt-backend") container_image=$IMG_DOCSGPT_BACKEND ;; "docsgpt-worker") container_image=$IMG_DOCSGPT_BACKEND ;; "docsgpt-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "memos-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "memos-app") container_image=$IMG_MEMOS_APP ;; "sillytavern-app") container_image=$IMG_SILLYTAVERN ;; "lemonade-server") container_image=$IMG_LEMONADE_SERVER ;; "speakr-db") container_image=mirror.gcr.io/postgres:16.9-bookworm ;; "speakr-app") container_image=$IMG_SPEAKR_APP ;; "speakr-whisperx") container_image=$IMG_SPEAKR_WHISPERX ;; "insanelyfastwhisper-app") container_image=$IMG_INSANELYFASTWHISPER_APP ;; "insanelyfastwhisper-api") container_image=$IMG_INSANELYFASTWHISPER_APP ;; "ivbox-app") container_image=$IMG_IVBOX_APP ;; "monica-db") container_image=mirror.gcr.io/mariadb:12.0.2-ubi10 ;; "monica-app") container_image=$IMG_MONICA_APP ;; "monica-meilisearch") container_image=mirror.gcr.io/getmeili/meilisearch:v1.31.0 ;; "monica-web") container_image=$IMG_MONICA_WEB ;; "monica-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "affine-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "affine-app") container_image=$IMG_AFFINE_APP ;; "affine-indexer") container_image=manticoresearch/manticore:13.2.3 ;; "affine-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "joplin-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "joplin-app") container_image=$IMG_JOPLIN_APP ;; "joplin-transcribe-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "joplin-transcribe-app") container_image=$IMG_JOPLIN_TRANSCRIBE_APP ;; "superset-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "superset-app") container_image=$IMG_SUPERSET_APP ;; "superset-worker") container_image=$IMG_SUPERSET_WORKER ;; "superset-websocket") container_image=$IMG_SUPERSET_WEBSOCKET ;; "superset-web") container_image=mirror.gcr.io/nginx:1.29.3-alpine ;; "superset-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "kokoro-tts") container_image=$IMG_KOKORO_TTS ;; "kokoro-web") container_image=$IMG_KOKORO_WEB ;; "chatterbox-app") container_image=$IMG_CHATTERBOX_APP ;; "litellm-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "litellm-proxy") container_image=$IMG_LITELLM_APP ;; "litellm-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "langfuse-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "langfuse-worker") container_image=$IMG_LANGFUSE_WORKER ;; "langfuse-app") container_image=$IMG_LANGFUSE_APP ;; "langfuse-clickhouse") container_image=mirror.gcr.io/clickhouse/clickhouse-server:25.8.28.1-alpine ;; "langfuse-minio") container_image=ghcr.io/homeserverhq/minio:v1.0 ;; "langfuse-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "skyvern-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "skyvern-api") container_image=$IMG_SKYVERN_API ;; "skyvern-web") container_image=$IMG_SKYVERN_WEB ;; "wger-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "wger-app") container_image=$IMG_WGER ;; "wger-nginx") container_image=mirror.gcr.io/nginx:1.29.3-alpine ;; "wger-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "workoutcool-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "workoutcool-app") container_image=$IMG_WORKOUTCOOL ;; "openrag-opensearch-db") container_image=$IMG_OPENRAG_DB ;; "openrag-opensearch-dashboard") container_image=mirror.gcr.io/opensearchproject/opensearch-dashboards:3.5.0 ;; "openrag-backend") container_image=$IMG_OPENRAG_BACKEND ;; "openrag-frontend") container_image=$IMG_OPENRAG_FRONTEND ;; "openrag-langflow") container_image=$IMG_OPENRAG_LANGFLOW ;; "voicebox-app") container_image=$IMG_VOICEBOX_APP ;; "opencode-app") container_image=$IMG_OPENCODE_APP ;; "opencode-playwright-mcp") container_image=$IMG_OPENCODE_PLAYWRIGHT ;; "openskills-app") container_image=$IMG_OPENSKILLS_APP ;; "emailclassifierai-monitor") container_image=$IMG_EMAILCLASSIFIERAI_APP ;; "emailclassifierai-worker") container_image=$IMG_EMAILCLASSIFIERAI_APP ;; "emailclassifierai-app") container_image=$IMG_EMAILCLASSIFIERAI_APP ;; "emailclassifierai-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "hermes-agent-gateway") container_image=$IMG_HERMES_APP ;; "hermes-agent-dashboard") container_image=$IMG_HERMES_APP ;; "hermes-agent-terminal") container_image=$IMG_HERMES_TERMINAL ;; "hermes-agent-camofox") container_image=$IMG_HERMES_CAMOFOX ;; "hermes-agent-webui") container_image=$IMG_HERMES_WEBUI ;; "autokb-db") container_image=mirror.gcr.io/postgres:15.0-bullseye ;; "autokb-app") container_image=$IMG_AUTOKB_APP ;; "autokb-manager") container_image=$IMG_AUTOKB_APP ;; "autokb-worker") container_image=$IMG_AUTOKB_APP ;; "autokb-web") container_image=$IMG_AUTOKB_APP ;; "autokb-mcp") container_image=$IMG_AUTOKB_MCP ;; "autokb-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "suitecrm-db") container_image=mirror.gcr.io/mariadb:10.11 ;; "suitecrm-app") container_image=$IMG_SUITECRM_APP ;; "suitecrm-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "suitecrm-mcp") container_image=$IMG_SUITECRM_MCP ;; "hedgedoc-db") container_image=mirror.gcr.io/postgres:17.6 ;; "hedgedoc-frontend") container_image=$IMG_HEDGEDOC_FRONTEND ;; "hedgedoc-backend") container_image=$IMG_HEDGEDOC_BACKEND ;; "hedgedoc-mcp") container_image=$IMG_HEDGEDOC_MCP ;; "presenton-app") container_image=$IMG_PRESENTON_APP ;; "presenton-mcp") container_image=$IMG_PRESENTON_MCP ;; "basicmemory-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "basicmemory-app") container_image=$IMG_BASICMEMORY_APP ;; "cognee-db") container_image=mirror.gcr.io/pgvector/pgvector:pg17 ;; "cognee-app") container_image=$IMG_COGNEE_APP ;; "cognee-frontend") container_image=$IMG_COGNEE_FRONTEND ;; "cognee-mcp") container_image=$IMG_COGNEE_MCP ;; "cognee-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "lightrag-db") container_image=mirror.gcr.io/pgvector/pgvector:pg18 ;; "lightrag-app") container_image=$IMG_LIGHTRAG_APP ;; "lightrag-qdrant") container_image=mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged ;; "lightrag-neo4j") container_image=mirror.gcr.io/neo4j:5.26.32-community ;; "openserp-app") container_image=$IMG_OPENSERP_APP ;; "suricata-app") container_image=$IMG_SURICATA_APP ;; "suricata-web") container_image=$IMG_SURICATA_WEB ;; "crowdsec-app") container_image=$IMG_CROWDSEC_APP ;; "crowdsec-web") container_image=$IMG_CROWDSEC_WEB ;; "fastcrw-app") container_image=$IMG_FASTCRW_APP ;; "lightpanda") container_image=$IMG_LIGHTPANDA ;; "fastcrw-proxy") container_image=$IMG_FASTCRW_PROXY ;; "headless-browser") container_image=$IMG_HEADLESS_BROWSER ;; "whiteboardrt-web") container_image=$IMG_WHITEBOARDRT_WEB ;; "whiteboardrt-agent") container_image=$IMG_WHITEBOARDRT_AGENT ;; "whiteboardrt-mcp") container_image=$IMG_WHITEBOARDRT_MCP ;; "whiteboardrt-storage") container_image=$IMG_WHITEBOARDRT_STORAGE ;; "whiteboardrt-server") container_image=$IMG_WHITEBOARDRT_SERVER ;; "whiteboardrt-redis") container_image=mirror.gcr.io/valkey/valkey:alpine3.23 ;; "mdcollab-db") container_image=mirror.gcr.io/postgres:17.6 ;; "mdcollab-backend") container_image=$IMG_MDCOLLAB_BACKEND ;; "mdcollab-frontend") container_image=$IMG_MDCOLLAB_FRONTEND ;; "mdcollab-mcp") container_image=$IMG_MDCOLLAB_MCP ;; "halospeak-app") container_image=$IMG_HALOSPEAK_APP ;; "halospeak-api") container_image=$IMG_HALOSPEAK_APP ;; #ADD_NEW_SCRIPT_IMG_BY_NAME_HERE *) ;; esac echo "$container_image" } function getImageOverride() { return } function performPostStackRemoval() { case "$1" in "wazuh") removeWazuhAgent ;; *) ;; esac } function checkAddAllNewSvcs() { checkAddServiceToConfig "clientdns-user1" "CLIENTDNS_USER1_ADMIN_USERNAME=,CLIENTDNS_USER1_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Wikijs" "WIKIJS_INIT_ENV=false,WIKIJS_ADMIN_USERNAME=,WIKIJS_ADMIN_EMAIL_ADDRESS=,WIKIJS_ADMIN_PASSWORD=,WIKIJS_ADMIN_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Collabora" "COLLABORA_ADMIN_USERNAME=,COLLABORA_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Invidious" "INVIDIOUS_DATABASE_NAME=,INVIDIOUS_DATABASE_USER=,INVIDIOUS_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Mealie" "MEALIE_ADMIN_USERNAME=,MEALIE_ADMIN_EMAIL_ADDRESS=,MEALIE_ADMIN_PASSWORD=,MEALIE_DATABASE_NAME=,MEALIE_DATABASE_USER=,MEALIE_DATABASE_USER_PASSWORD=,MEALIE_ADMIN_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Kasm" "KASM_INIT_ENV=false,KASM_ADMIN_EMAIL_ADDRESS=,KASM_ADMIN_PASSWORD=,KASM_USER_EMAIL_ADDRESS=,KASM_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Remotely" "REMOTELY_INIT_ENV=false,REMOTELY_ADMIN_USERNAME=,REMOTELY_ADMIN_EMAIL_ADDRESS=,REMOTELY_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Calibre" "CALIBRE_WEB_INIT_ENV=false,CALIBRE_WEB_ADMIN_USERNAME=,CALIBRE_WEB_ADMIN_EMAIL_ADDRESS=,CALIBRE_WEB_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Linkwarden" "LINKWARDEN_DATABASE_NAME=,LINKWARDEN_DATABASE_USER=,LINKWARDEN_DATABASE_USER_PASSWORD=,LINKWARDEN_NEXTAUTH_SECRET=,LINKWARDEN_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "FreshRSS" "FRESHRSS_INIT_ENV=false,FRESHRSS_ADMIN_USERNAME=,FRESHRSS_ADMIN_PASSWORD=,FRESHRSS_ADMIN_EMAIL_ADDRESS=,FRESHRSS_DATABASE_NAME=,FRESHRSS_DATABASE_USER=,FRESHRSS_DATABASE_USER_PASSWORD=,FRESHRSS_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Bar Assistant" "BARASSISTANT_REDIS_PASSWORD=,BARASSISTANT_MEILISEARCH_KEY=,BARASSISTANT_API_KEY=,BARASSISTANT_MCP_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Keila" "KEILA_INIT_ENV=false,KEILA_ADMIN_USERNAME=,KEILA_ADMIN_EMAIL_ADDRESS=,KEILA_ADMIN_PASSWORD=,KEILA_DATABASE_NAME=,KEILA_DATABASE_USER=,KEILA_DATABASE_USER_PASSWORD=,KEILA_ADMIN_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Wallabag" "WALLABAG_INIT_ENV=false,WALLABAG_ADMIN_USERNAME=,WALLABAG_ADMIN_EMAIL_ADDRESS=,WALLABAG_ADMIN_PASSWORD=,WALLABAG_DATABASE_NAME=,WALLABAG_DATABASE_USER=,WALLABAG_DATABASE_USER_PASSWORD=,WALLABAG_ENV_SECRET=,WALLABAG_REDIS_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Jupyter" "JUPYTER_INIT_ENV=false,JUPYTER_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Paperless" "PAPERLESS_INIT_ENV=false,PAPERLESS_SECRET_KEY=,PAPERLESS_CLIENT_SECRET=,PAPERLESS_REDIS_PASSWORD=,PAPERLESS_ADMIN_USERNAME=,PAPERLESS_ADMIN_EMAIL_ADDRESS=,PAPERLESS_ADMIN_PASSWORD=,PAPERLESS_DATABASE_NAME=,PAPERLESS_DATABASE_USER=,PAPERLESS_DATABASE_USER_PASSWORD=,PAPERLESS_AI_ADMIN_USERNAME=,PAPERLESS_AI_ADMIN_PASSWORD=,PAPERLESS_AI_API_KEY=,PAPERLESS_AI_JWT_SECRET=,PAPERLESS_GPT_ADMIN_USERNAME=,PAPERLESS_GPT_ADMIN_PASSWORD=,PAPERLESS_API_TOKEN=,PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME=,PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID=,PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME=,PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID=,PAPERLESS_KNOWLEDGEBASE_TAG_NAME=,PAPERLESS_KNOWLEDGEBASE_TAG_ID=,PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME=,PAPERLESS_TRANSCRIPTION_DOCTYPE_ID=" $CONFIG_FILE false checkAddServiceToConfig "SpeedtestTrackerLocal" "SPEEDTEST_TRACKER_LOCAL_INIT_ENV=false,SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME=,SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS=,SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD=,SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME=,SPEEDTEST_TRACKER_LOCAL_DATABASE_USER=,SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "SpeedtestTrackerVPN" "SPEEDTEST_TRACKER_VPN_INIT_ENV=false,SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME=,SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS=,SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD=,SPEEDTEST_TRACKER_VPN_DATABASE_NAME=,SPEEDTEST_TRACKER_VPN_DATABASE_USER=,SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Change Detection" "CHANGEDETECTION_INIT_ENV=false,CHANGEDETECTION_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Script-server" "SCRIPTSERVER_INIT_ENV=false,SCRIPTSERVER_LOCALHOST_PORT=8008,SCRIPTSERVER_ADMIN_USERNAME=,SCRIPTSERVER_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Huginn" "HUGINN_INIT_ENV=false,HUGINN_APP_SECRET_TOKEN=,HUGINN_ADMIN_USERNAME=,HUGINN_ADMIN_EMAIL_ADDRESS=,HUGINN_ADMIN_PASSWORD=,HUGINN_DATABASE_NAME=,HUGINN_DATABASE_USER=,HUGINN_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Coturn" "COTURN_STATIC_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Piped" "PIPED_DATABASE_NAME=,PIPED_DATABASE_USER=,PIPED_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "GrampsWeb" "GRAMPSWEB_INIT_ENV=false,GRAMPSWEB_ADMIN_USERNAME=,GRAMPSWEB_ADMIN_PASSWORD=,GRAMPSWEB_ADMIN_EMAIL_ADDRESS=,GRAMPSWEB_SECRET_KEY=,GRAMPSWEB_REDIS_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Penpot" "PENPOT_INIT_ENV=false,PENPOT_REDIS_PASSWORD=,PENPOT_DATABASE_NAME=,PENPOT_DATABASE_USER=,PENPOT_DATABASE_USER_PASSWORD=,PENPOT_SECRET_KEY=" $CONFIG_FILE false checkAddServiceToConfig "EspoCRM" "ESPOCRM_INIT_ENV=false,ESPOCRM_ADMIN_USERNAME=,ESPOCRM_ADMIN_PASSWORD=,ESPOCRM_DATABASE_NAME=,ESPOCRM_DATABASE_ROOT_PASSWORD=,ESPOCRM_DATABASE_USER=,ESPOCRM_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Immich" "IMMICH_INIT_ENV=false,IMMICH_ADMIN_USERNAME=,IMMICH_ADMIN_PASSWORD=,IMMICH_ADMIN_EMAIL_ADDRESS=,IMMICH_DATABASE_NAME=,IMMICH_DATABASE_USER=,IMMICH_DATABASE_USER_PASSWORD=,IMMICH_REDIS_PASSWORD=,IMMICH_OIDC_CLIENT_SECRET=,IMMICH_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Homarr" "HOMARR_INIT_ENV=false,HOMARR_ADMIN_USERNAME=,HOMARR_ADMIN_EMAIL_ADDRESS=,HOMARR_ADMIN_PASSWORD=,HOMARR_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Matomo" "MATOMO_INIT_ENV=false,MATOMO_ADMIN_USERNAME=,MATOMO_ADMIN_PASSWORD=,MATOMO_ADMIN_EMAIL_ADDRESS=,MATOMO_DATABASE_NAME=,MATOMO_DATABASE_ROOT_PASSWORD=,MATOMO_DATABASE_USER=,MATOMO_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Pastefy" "PASTEFY_INIT_ENV=false,PASTEFY_ADMIN_USERNAME=,PASTEFY_ADMIN_PASSWORD=,PASTEFY_ADMIN_EMAIL_ADDRESS=,PASTEFY_DATABASE_NAME=,PASTEFY_DATABASE_ROOT_PASSWORD=,PASTEFY_DATABASE_USER=,PASTEFY_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "AIStack" "AISTACK_INIT_ENV=false,AISTACK_MINDSDB_ADMIN_USERNAME=,AISTACK_MINDSDB_ADMIN_PASSWORD=,AISTACK_MINDSDB_ADMIN_EMAIL_ADDRESS=,AISTACK_MINDSDB_DATABASE_NAME=,AISTACK_MINDSDB_DATABASE_USER=,AISTACK_MINDSDB_DATABASE_USER_PASSWORD=,AISTACK_LANGFUSE_ADMIN_USERNAME=,AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS=,AISTACK_LANGFUSE_ADMIN_PASSWORD=,AISTACK_LANGFUSE_DATABASE_NAME=,AISTACK_OPENWEBUI_ADMIN_USERNAME=,AISTACK_OPENWEBUI_ADMIN_EMAIL_ADDRESS=,AISTACK_OPENWEBUI_ADMIN_PASSWORD=,AISTACK_OPENWEBUI_OIDC_CLIENT_ID=,AISTACK_OPENWEBUI_OIDC_CLIENT_SECRET=,AISTACK_REDIS_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Pixelfed" "PIXELFED_INIT_ENV=false,PIXELFED_DATABASE_NAME=,PIXELFED_DATABASE_ROOT_PASSWORD=,PIXELFED_DATABASE_USER=,PIXELFED_DATABASE_USER_PASSWORD=,PIXELFED_REDIS_PASSWORD=,PIXELFED_APP_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Yamtrack" "YAMTRACK_INIT_ENV=false,YAMTRACK_ADMIN_USERNAME=,YAMTRACK_ADMIN_PASSWORD=,YAMTRACK_REDIS_PASSWORD=,YAMTRACK_DATABASE_NAME=,YAMTRACK_DATABASE_USER=,YAMTRACK_DATABASE_USER_PASSWORD=,YAMTRACK_SECRET_KEY=,YAMTRACK_OIDC_CLIENT_ID=,YAMTRACK_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Servarr" "SERVARR_INIT_ENV=false,SONARR_ADMIN_USERNAME=,SONARR_ADMIN_PASSWORD=,RADARR_ADMIN_USERNAME=,RADARR_ADMIN_PASSWORD=,LIDARR_ADMIN_USERNAME=,LIDARR_ADMIN_PASSWORD=,READARR_ADMIN_USERNAME=,READARR_ADMIN_PASSWORD=,BAZARR_ADMIN_USERNAME=,BAZARR_ADMIN_PASSWORD=,MYLAR3_ADMIN_USERNAME=,MYLAR3_ADMIN_PASSWORD=,PROWLARR_ADMIN_USERNAME=,PROWLARR_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "SABnzbd" "SABNZBD_INIT_ENV=false,SABNZBD_ADMIN_USERNAME=,SABNZBD_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "qBittorrent" "QBITTORRENT_INIT_ENV=false,QBITTORRENT_ADMIN_USERNAME=,QBITTORRENT_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Ombi" "OMBI_INIT_ENV=false,OMBI_ADMIN_USERNAME=,OMBI_ADMIN_PASSWORD=,OMBI_ADMIN_EMAIL_ADDRESS=,OMBI_DATABASE_NAME=,OMBI_DATABASE_ROOT_PASSWORD=,OMBI_DATABASE_USER=,OMBI_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "MeshCentral" "MESHCENTRAL_INIT_ENV=false,MESHCENTRAL_DATABASE_NAME=,MESHCENTRAL_DATABASE_ROOT_PASSWORD=,MESHCENTRAL_DATABASE_USER=,MESHCENTRAL_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Navidrome" "NAVIDROME_INIT_ENV=false,NAVIDROME_ADMIN_USERNAME=,NAVIDROME_ADMIN_PASSWORD=,NAVIDROME_ADMIN_EMAIL_ADDRESS=" $CONFIG_FILE false checkAddServiceToConfig "Adminer" "ADMINER_INIT_ENV=false,ADMINER_DATABASE_NAME=,ADMINER_DATABASE_ROOT_PASSWORD=,ADMINER_DATABASE_USER=,ADMINER_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Budibase" "BUDIBASE_INIT_ENV=false,BUDIBASE_API_ENCRYPTION_KEY=,BUDIBASE_INTERNAL_API_KEY=,BUDIBASE_JWT_SECRET=,BUDIBASE_ADMIN_EMAIL_ADDRESS=,BUDIBASE_ADMIN_PASSWORD=,BUDIBASE_COUCHDB_USER=,BUDIBASE_COUCHDB_PASSWORD=,BUDIBASE_MINIO_ACCESS_KEY=,BUDIBASE_MINIO_SECRET_KEY=,BUDIBASE_REDIS_PASSWORD=,BUDIBASE_DATABASE_NAME=,BUDIBASE_DATABASE_ROOT_PASSWORD=,BUDIBASE_DATABASE_USER=,BUDIBASE_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "AudioBookshelf" "AUDIOBOOKSHELF_INIT_ENV=false,AUDIOBOOKSHELF_ADMIN_USERNAME=,AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS=,AUDIOBOOKSHELF_ADMIN_PASSWORD=,AUDIOBOOKSHELF_JWT_SECRET=,AUDIOBOOKSHELF_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "StandardNotes" "STANDARDNOTES_REDIS_PASSWORD=,STANDARDNOTES_DATABASE_NAME=,STANDARDNOTES_DATABASE_ROOT_PASSWORD=,STANDARDNOTES_DATABASE_USER=,STANDARDNOTES_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Metabase" "METABASE_INIT_ENV=false,METABASE_ADMIN_USERNAME=,METABASE_ADMIN_EMAIL_ADDRESS=,METABASE_ADMIN_PASSWORD=,METABASE_DATABASE_NAME=,METABASE_DATABASE_USER=,METABASE_DATABASE_USER_PASSWORD=,METABASE_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Kanboard" "KANBOARD_INIT_ENV=false,KANBOARD_ADMIN_USERNAME=,KANBOARD_ADMIN_EMAIL_ADDRESS=,KANBOARD_ADMIN_PASSWORD=,KANBOARD_DATABASE_NAME=,KANBOARD_DATABASE_USER=,KANBOARD_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Wekan" "WEKAN_OIDC_CLIENT_SECRET=,WEKAN_DATABASE_NAME=,WEKAN_DATABASE_USER=,WEKAN_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Revolt" "REVOLT_DATABASE_NAME=,REVOLT_DATABASE_USER=,REVOLT_DATABASE_USER_PASSWORD=,REVOLT_REDIS_PASSWORD=,REVOLT_RABBITMQ_USERNAME=,REVOLT_RABBITMQ_PASSWORD=,REVOLT_MINIO_KEY=,REVOLT_MINIO_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "FrappeHR" "FRAPPE_HR_INIT_ENV=false,FRAPPE_HR_ADMIN_USERNAME=,FRAPPE_HR_ADMIN_EMAIL_ADDRESS=,FRAPPE_HR_ADMIN_PASSWORD=,FRAPPE_HR_DATABASE_NAME=,FRAPPE_HR_DATABASE_ROOT_PASSWORD=,FRAPPE_HR_DATABASE_USER=,FRAPPE_HR_DATABASE_USER_PASSWORD=,FRAPPE_HR_REDIS_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "MintHCM" "MINTHCM_INIT_ENV=false,MINTHCM_ADMIN_USERNAME=,MINTHCM_ADMIN_PASSWORD=,MINTHCM_ADMIN_EMAIL_ADDRESS=,MINTHCM_DATABASE_NAME=,MINTHCM_DATABASE_ROOT_PASSWORD=,MINTHCM_DATABASE_USER=,MINTHCM_DATABASE_USER_PASSWORD=,MINTHCM_ES_USER=,MINTHCM_ES_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "CloudBeaver" "CLOUDBEAVER_INIT_ENV=false,CLOUDBEAVER_ADMIN_USERNAME=,CLOUDBEAVER_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "DbGate" "DBGATE_INIT_ENV=false,DBGATE_ADMIN_USERNAME=,DBGATE_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Twenty" "TWENTY_INIT_ENV=false,TWENTY_ADMIN_USERNAME=,TWENTY_ADMIN_EMAIL_ADDRESS=,TWENTY_ADMIN_PASSWORD=,TWENTY_DATABASE_NAME=,TWENTY_DATABASE_USER=,TWENTY_DATABASE_USER_PASSWORD=,TWENTY_REDIS_PASSWORD=,TWENTY_MINIO_KEY=,TWENTY_MINIO_SECRET=,TWENTY_APP_SECRET=,TWENTY_ENCRYPTION_KEY=,TWENTY_ADMIN_API_KEY=,TWENTY_AKB_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Odoo" "ODOO_INIT_ENV=false,ODOO_ADMIN_USERNAME=,ODOO_ADMIN_EMAIL_ADDRESS=,ODOO_ADMIN_PASSWORD=,ODOO_DATABASE_NAME=,ODOO_DATABASE_USER=,ODOO_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Calcom" "CALCOM_INIT_ENV=false,CALCOM_ADMIN_USERNAME=,CALCOM_ADMIN_EMAIL_ADDRESS=,CALCOM_ADMIN_PASSWORD=,CALCOM_DATABASE_NAME=,CALCOM_DATABASE_USER=,CALCOM_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Rallly" "RALLLY_INIT_ENV=false,RALLLY_ADMIN_USERNAME=,RALLLY_ADMIN_EMAIL_ADDRESS=,RALLLY_ADMIN_PASSWORD=,RALLLY_DATABASE_NAME=,RALLLY_DATABASE_USER=,RALLLY_DATABASE_USER_PASSWORD=,RALLLY_MINIO_KEY=,RALLLY_MINIO_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "EasyAppointments" "EASYAPPOINTMENTS_INIT_ENV=false,EASYAPPOINTMENTS_ADMIN_USERNAME=,EASYAPPOINTMENTS_ADMIN_EMAIL_ADDRESS=,EASYAPPOINTMENTS_ADMIN_PASSWORD=,EASYAPPOINTMENTS_DATABASE_NAME=,EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD=,EASYAPPOINTMENTS_DATABASE_USER=,EASYAPPOINTMENTS_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "OpenProject" "OPENPROJECT_INIT_ENV=false,OPENPROJECT_ADMIN_USERNAME=,OPENPROJECT_ADMIN_EMAIL_ADDRESS=,OPENPROJECT_ADMIN_PASSWORD=,OPENPROJECT_DATABASE_NAME=,OPENPROJECT_DATABASE_USER=,OPENPROJECT_DATABASE_USER_PASSWORD=,OPENPROJECT_MINIO_KEY=,OPENPROJECT_MINIO_SECRET=,OPENPROJECT_SECRET_KEY_BASE=" $CONFIG_FILE false checkAddServiceToConfig "Zammad" "ZAMMAD_INIT_ENV=false,ZAMMAD_ADMIN_USERNAME=,ZAMMAD_ADMIN_EMAIL_ADDRESS=,ZAMMAD_ADMIN_PASSWORD=,ZAMMAD_DATABASE_NAME=,ZAMMAD_DATABASE_ROOT_PASSWORD=,ZAMMAD_DATABASE_USER=,ZAMMAD_DATABASE_USER_PASSWORD=,ZAMMAD_REDIS_PASSWORD=,ZAMMAD_ES_USERNAME=,ZAMMAD_ES_PASSWORD=,ZAMMAD_MINIO_KEY=,ZAMMAD_MINIO_SECRET=,ZAMMAD_DEDICATED_EMAIL_USERNAME=,ZAMMAD_DEDICATED_EMAIL_ADDRESS=,ZAMMAD_DEDICATED_EMAIL_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Zulip" "ZULIP_INIT_ENV=false,ZULIP_ADMIN_USERNAME=,ZULIP_ADMIN_EMAIL_ADDRESS=,ZULIP_ADMIN_PASSWORD=,ZULIP_DATABASE_NAME=,ZULIP_DATABASE_USER=,ZULIP_DATABASE_USER_PASSWORD=,ZULIP_REDIS_PASSWORD=,ZULIP_RABBITMQ_USERNAME=,ZULIP_RABBITMQ_PASSWORD=,ZULIP_MEMCACHE_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "KillBill" "KILLBILL_INIT_ENV=false,KILLBILL_ADMIN_USERNAME=,KILLBILL_ADMIN_EMAIL_ADDRESS=,KILLBILL_ADMIN_PASSWORD=,KILLBILL_DATABASE_USER=,KILLBILL_DATABASE_USER_PASSWORD=,KILLBILL_KB_DATABASE_NAME=,KILLBILL_KAUI_DATABASE_NAME=,KILLBILL_REDIS_PASSWORD=,KILLBILL_API_KEY=,KILLBILL_API_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "InvoiceShelf" "INVOICESHELF_INIT_ENV=false,INVOICESHELF_ADMIN_USERNAME=,INVOICESHELF_ADMIN_EMAIL_ADDRESS=,INVOICESHELF_ADMIN_PASSWORD=,INVOICESHELF_ADMIN_API_KEY=,INVOICESHELF_DATABASE_NAME=,INVOICESHELF_DATABASE_USER=,INVOICESHELF_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "InvoiceNinja" "INVOICENINJA_INIT_ENV=false,INVOICENINJA_ADMIN_USERNAME=,INVOICENINJA_ADMIN_EMAIL_ADDRESS=,INVOICENINJA_ADMIN_PASSWORD=,INVOICENINJA_DATABASE_NAME=,INVOICENINJA_DATABASE_ROOT_PASSWORD=,INVOICENINJA_DATABASE_USER=,INVOICENINJA_DATABASE_USER_PASSWORD=,INVOICENINJA_REDIS_PASSWORD=,INVOICENINJA_MINIO_KEY=,INVOICENINJA_MINIO_SECRET=,INVOICENINJA_API_SECRET=,INVOICENINJA_UPDATE_SECRET=,INVOICENINJA_WEBCRON_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Dolibarr" "DOLIBARR_INIT_ENV=false,DOLIBARR_ADMIN_USERNAME=,DOLIBARR_ADMIN_EMAIL_ADDRESS=,DOLIBARR_ADMIN_PASSWORD=,DOLIBARR_DATABASE_NAME=,DOLIBARR_DATABASE_ROOT_PASSWORD=,DOLIBARR_DATABASE_USER=,DOLIBARR_DATABASE_USER_PASSWORD=,DOLIBARR_REDIS_PASSWORD=,DOLIBARR_CRON_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "n8n" "N8N_INIT_ENV=false,N8N_ADMIN_USERNAME=,N8N_ADMIN_EMAIL_ADDRESS=,N8N_ADMIN_PASSWORD=,N8N_DATABASE_NAME=,N8N_DATABASE_USER=,N8N_DATABASE_USER_PASSWORD=,N8N_REDIS_PASSWORD=,N8N_ENCRYPTION_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Automatisch" "AUTOMATISCH_INIT_ENV=false,AUTOMATISCH_ADMIN_USERNAME=,AUTOMATISCH_ADMIN_EMAIL_ADDRESS=,AUTOMATISCH_ADMIN_PASSWORD=,AUTOMATISCH_DATABASE_NAME=,AUTOMATISCH_DATABASE_USER=,AUTOMATISCH_DATABASE_USER_PASSWORD=,AUTOMATISCH_REDIS_PASSWORD=,AUTOMATISCH_ENCRYPTION_KEY=,AUTOMATISCH_WEBHOOK_SECRET_KEY=,AUTOMATISCH_APP_SECRET_KEY=" $CONFIG_FILE false checkAddServiceToConfig "ActivePieces" "ACTIVEPIECES_INIT_ENV=false,ACTIVEPIECES_ADMIN_USERNAME=,ACTIVEPIECES_ADMIN_EMAIL_ADDRESS=,ACTIVEPIECES_ADMIN_PASSWORD=,ACTIVEPIECES_DATABASE_NAME=,ACTIVEPIECES_DATABASE_USER=,ACTIVEPIECES_DATABASE_USER_PASSWORD=,ACTIVEPIECES_REDIS_PASSWORD=,ACTIVEPIECES_ENCRYPTION_KEY=,ACTIVEPIECES_API_KEY=,ACTIVEPIECES_JWT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Beszel" "BESZEL_INIT_ENV=false,BESZEL_ADMIN_USERNAME=,BESZEL_ADMIN_EMAIL_ADDRESS=,BESZEL_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Taiga" "TAIGA_INIT_ENV=false,TAIGA_ADMIN_USERNAME=,TAIGA_ADMIN_EMAIL_ADDRESS=,TAIGA_ADMIN_PASSWORD=,TAIGA_DATABASE_NAME=,TAIGA_DATABASE_USER=,TAIGA_DATABASE_USER_PASSWORD=,TAIGA_SECRET_KEY=,TAIGA_RABBITMQ_USERNAME=,TAIGA_RABBITMQ_PASSWORD=,TAIGA_RABBITMQ_ERLANG_COOKIE=" $CONFIG_FILE false checkAddServiceToConfig "OpenSign" "OPENSIGN_INIT_ENV=false,OPENSIGN_ADMIN_USERNAME=,OPENSIGN_ADMIN_EMAIL_ADDRESS=,OPENSIGN_ADMIN_PASSWORD=,OPENSIGN_DATABASE_NAME=,OPENSIGN_DATABASE_USER=,OPENSIGN_DATABASE_USER_PASSWORD=,OPENSIGN_MASTER_KEY=,OPENSIGN_CERTIFICATE_PASS_PHRASE=" $CONFIG_FILE false checkAddServiceToConfig "DocuSeal" "DOCUSEAL_INIT_ENV=false,DOCUSEAL_ADMIN_USERNAME=,DOCUSEAL_ADMIN_EMAIL_ADDRESS=,DOCUSEAL_ADMIN_PASSWORD=,DOCUSEAL_DATABASE_NAME=,DOCUSEAL_DATABASE_USER=,DOCUSEAL_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "ControlR" "CONTROLR_INIT_ENV=false,CONTROLR_ADMIN_USERNAME=,CONTROLR_ADMIN_EMAIL_ADDRESS=,CONTROLR_ADMIN_PASSWORD=,CONTROLR_DATABASE_NAME=,CONTROLR_DATABASE_USER=,CONTROLR_DATABASE_USER_PASSWORD=,CONTROLR_ASPIRE_TOKEN=,CONTROLR_ASPIRE_APIKEY=" $CONFIG_FILE false checkAddServiceToConfig "Akaunting" "AKAUNTING_INIT_ENV=false,AKAUNTING_ADMIN_USERNAME=,AKAUNTING_ADMIN_EMAIL_ADDRESS=,AKAUNTING_ADMIN_PASSWORD=,AKAUNTING_DATABASE_NAME=,AKAUNTING_DATABASE_ROOT_PASSWORD=,AKAUNTING_DATABASE_USER=,AKAUNTING_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Axelor" "AXELOR_INIT_ENV=false,AXELOR_ADMIN_USERNAME=,AXELOR_ADMIN_EMAIL_ADDRESS=,AXELOR_ADMIN_PASSWORD=,AXELOR_DATABASE_NAME=,AXELOR_DATABASE_USER=,AXELOR_DATABASE_USER_PASSWORD=,AXELOR_NEXTAUTH_SECRET=,AXELOR_GOOVEE_ADMIN_USERNAME=,AXELOR_GOOVEE_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "ConvertX" "CONVERTX_INIT_ENV=false,CONVERTX_ADMIN_USERNAME=,CONVERTX_ADMIN_EMAIL_ADDRESS=,CONVERTX_ADMIN_PASSWORD=,CONVERTX_JWT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Kopia" "KOPIA_INIT_ENV=false,KOPIA_ADMIN_USERNAME=,KOPIA_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Ollama" "OLLAMA_INIT_ENV=false,OLLAMA_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "LocalAI" "LOCALAI_INIT_ENV=false,LOCALAI_ADMIN_USERNAME=,LOCALAI_ADMIN_EMAIL_ADDRESS=,LOCALAI_ADMIN_PASSWORD=,LOCALAI_API_KEY=,LOCALAI_SSHBOX_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "ComfyUI" "COMFYUI_INIT_ENV=false,COMFYUI_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Langflow" "LANGFLOW_INIT_ENV=false,LANGFLOW_ADMIN_USERNAME=,LANGFLOW_ADMIN_EMAIL_ADDRESS=,LANGFLOW_ADMIN_PASSWORD=,LANGFLOW_DATABASE_NAME=,LANGFLOW_DATABASE_USER=,LANGFLOW_DATABASE_USER_PASSWORD=,LANGFLOW_API_KEY=,LANGFLOW_SECRET_KEY=,LANGFLOW_LANGFUSE_PUBLIC_KEY=,LANGFLOW_LANGFUSE_SECRET_KEY=" $CONFIG_FILE false checkAddServiceToConfig "AnythingLLM" "ANYTHINGLLM_INIT_ENV=false,ANYTHINGLLM_ADMIN_USERNAME=,ANYTHINGLLM_ADMIN_EMAIL_ADDRESS=,ANYTHINGLLM_ADMIN_PASSWORD=,ANYTHINGLLM_SIG_KEY=,ANYTHINGLLM_SIG_SALT=,ANYTHINGLLM_JWT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Firecrawl" "FIRECRAWL_INIT_ENV=false,FIRECRAWL_DATABASE_NAME=,FIRECRAWL_DATABASE_USER=,FIRECRAWL_DATABASE_USER_PASSWORD=,FIRECRAWL_REDIS_PASSWORD=,FIRECRAWL_BULL_AUTH_KEY=,FIRECRAWL_API_KEY=,FIRECRAWL_RABBITMQ_USERNAME=,FIRECRAWL_RABBITMQ_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "LibreChat" "LIBRECHAT_INIT_ENV=false,LIBRECHAT_ADMIN_USERNAME=,LIBRECHAT_ADMIN_EMAIL_ADDRESS=,LIBRECHAT_ADMIN_PASSWORD=,LIBRECHAT_DATABASE_NAME=,LIBRECHAT_DATABASE_USER=,LIBRECHAT_DATABASE_USER_PASSWORD=,LIBRECHAT_REDIS_PASSWORD=,LIBRECHAT_MONGODB_DATABASE=,LIBRECHAT_MONGODB_USER=,LIBRECHAT_MONGODB_USER_PASSWORD=,LIBRECHAT_CREDS_KEY=,LIBRECHAT_CREDS_IV=,LIBRECHAT_JWT_SECRET=,LIBRECHAT_JWT_REFRESH_SECRET=,LIBRECHAT_MEILI_MASTER_KEY=,LIBRECHAT_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "OpenWebUI" "OPENWEBUI_INIT_ENV=false,OPENWEBUI_ADMIN_USERNAME=,OPENWEBUI_ADMIN_EMAIL_ADDRESS=,OPENWEBUI_ADMIN_PASSWORD=,OPENWEBUI_DATABASE_NAME=,OPENWEBUI_DATABASE_USER=,OPENWEBUI_DATABASE_USER_PASSWORD=,OPENWEBUI_REDIS_PASSWORD=,OPENWEBUI_OIDC_CLIENT_ID=,OPENWEBUI_OIDC_CLIENT_SECRET=,OPENWEBUI_SECRET_KEY=,OPENWEBUI_ADMIN_API_KEY=,OPENWEBUI_QDRANT_API_KEY=,OPENWEBUI_OPENTERMINAL_API_KEY=,OPENWEBUI_MCPO_API_KEY=,OPENWEBUI_PIPELINES_API_KEY=,OPENWEBUI_ADMIN_UUID=,OPENWEBUI_PRIMARYUSERS_UUID=" $CONFIG_FILE false checkAddServiceToConfig "Khoj" "KHOJ_INIT_ENV=false,KHOJ_ADMIN_USERNAME=,KHOJ_ADMIN_EMAIL_ADDRESS=,KHOJ_ADMIN_PASSWORD=,KHOJ_DATABASE_NAME=,KHOJ_DATABASE_USER=,KHOJ_DATABASE_USER_PASSWORD=,KHOJ_DJANGO_SECRET_KEY=" $CONFIG_FILE false checkAddServiceToConfig "LobeChat" "LOBECHAT_INIT_ENV=false,LOBECHAT_ADMIN_USERNAME=,LOBECHAT_ADMIN_EMAIL_ADDRESS=,LOBECHAT_ADMIN_PASSWORD=,LOBECHAT_DATABASE_NAME=,LOBECHAT_DATABASE_USER=,LOBECHAT_DATABASE_USER_PASSWORD=,LOBECHAT_REDIS_PASSWORD=,LOBECHAT_NEXTAUTH_SECRET=,LOBECHAT_KEYVAULTS_SECRET=,LOBECHAT_MINIO_KEY=,LOBECHAT_MINIO_SECRET=,LOBECHAT_OIDC_CLIENT_ID=,LOBECHAT_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "RAGFlow" "RAGFLOW_INIT_ENV=false,RAGFLOW_ADMIN_USERNAME=,RAGFLOW_ADMIN_EMAIL_ADDRESS=,RAGFLOW_ADMIN_PASSWORD=,RAGFLOW_ADMIN_API_KEY=,RAGFLOW_DATABASE_NAME=,RAGFLOW_DATABASE_USER=,RAGFLOW_DATABASE_USER_PASSWORD=,RAGFLOW_DATABASE_ROOT_PASSWORD=,RAGFLOW_REDIS_PASSWORD=,RAGFLOW_MINIO_KEY=,RAGFLOW_MINIO_SECRET=,RAGFLOW_SECRET_KEY=,RAGFLOW_OIDC_CLIENT_ID=,RAGFLOW_OIDC_CLIENT_SECRET=,RAGFLOW_MCPSERVER_API_KEY=,RAGFLOW_SANDBOX_EXECUTOR_MANAGER_API_TOKEN=,RAGFLOW_TEAM_API_KEY=,RAGFLOW_TEAM_TENANT_ID=" $CONFIG_FILE false checkAddServiceToConfig "TabbyML" "TABBYML_INIT_ENV=false,TABBYML_ADMIN_USERNAME=,TABBYML_ADMIN_EMAIL_ADDRESS=,TABBYML_ADMIN_PASSWORD=,TABBYML_JWT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "DeepWikiOpen" "DEEPWIKI_OPEN_INIT_ENV=false,DEEPWIKI_OPEN_AUTH_CODE=" $CONFIG_FILE false checkAddServiceToConfig "Docling" "DOCLING_INIT_ENV=false,DOCLING_REDIS_PASSWORD=,DOCLING_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Dify" "DIFY_INIT_ENV=false,DIFY_ADMIN_USERNAME=,DIFY_ADMIN_EMAIL_ADDRESS=,DIFY_ADMIN_PASSWORD=,DIFY_DATABASE_NAME=,DIFY_DATABASE_USER=,DIFY_DATABASE_USER_PASSWORD=,DIFY_REDIS_PASSWORD=,DIFY_SECRET_KEY=,DIFY_MINIO_KEY=,DIFY_MINIO_SECRET=,DIFY_QDRANT_API_KEY=,DIFY_CODE_EXECUTION_API_KEY=,DIFY_SANDBOX_API_KEY=,DIFY_PLUGIN_DAEMON_KEY=,DIFY_PLUGIN_DIFY_INNER_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "MindsDB" "MINDSDB_INIT_ENV=false,MINDSDB_ADMIN_USERNAME=,MINDSDB_ADMIN_EMAIL_ADDRESS=,MINDSDB_ADMIN_PASSWORD=,MINDSDB_DATABASE_NAME=,MINDSDB_DATABASE_USER=,MINDSDB_DATABASE_USER_PASSWORD=,MINDSDB_DATABASE_READONLYUSER=,MINDSDB_DATABASE_READONLYUSER_PASSWORD=,MINDSDB_REDIS_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "WaterCrawl" "WATERCRAWL_INIT_ENV=false,WATERCRAWL_ADMIN_USERNAME=,WATERCRAWL_ADMIN_EMAIL_ADDRESS=,WATERCRAWL_ADMIN_PASSWORD=,WATERCRAWL_DATABASE_NAME=,WATERCRAWL_DATABASE_USER=,WATERCRAWL_DATABASE_USER_PASSWORD=,WATERCRAWL_DATABASE_READONLYUSER=,WATERCRAWL_DATABASE_READONLYUSER_PASSWORD=,WATERCRAWL_REDIS_PASSWORD=,WATERCRAWL_SECRET_KEY=,WATERCRAWL_API_ENCRYPTION_KEY=,WATERCRAWL_PLAYWRIGHT_API_KEY=,WATERCRAWL_MINIO_KEY=,WATERCRAWL_MINIO_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Flowise" "FLOWISE_INIT_ENV=false,FLOWISE_ADMIN_USERNAME=,FLOWISE_ADMIN_EMAIL_ADDRESS=,FLOWISE_ADMIN_PASSWORD=,FLOWISE_DATABASE_NAME=,FLOWISE_DATABASE_USER=,FLOWISE_DATABASE_USER_PASSWORD=,FLOWISE_DATABASE_READONLYUSER=,FLOWISE_DATABASE_READONLYUSER_PASSWORD=,FLOWISE_REDIS_PASSWORD=,FLOWISE_JWT_SECRET=,FLOWISE_JWT_REFRESH_SECRET=,FLOWISE_EXPRESS_SESSION_SECRET=,FLOWISE_TOKEN_HASH_SECRET=,FLOWISE_MINIO_KEY=,FLOWISE_MINIO_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "SurfSense" "SURFSENSE_INIT_ENV=false,SURFSENSE_ADMIN_USERNAME=,SURFSENSE_ADMIN_EMAIL_ADDRESS=,SURFSENSE_ADMIN_PASSWORD=,SURFSENSE_SECRET_KEY=" $CONFIG_FILE false checkAddServiceToConfig "NocoDB" "NOCODB_INIT_ENV=false,NOCODB_ADMIN_USERNAME=,NOCODB_ADMIN_EMAIL_ADDRESS=,NOCODB_ADMIN_PASSWORD=,NOCODB_DATABASE_NAME=,NOCODB_DATABASE_USER=,NOCODB_DATABASE_USER_PASSWORD=,NOCODB_DATABASE_READONLYUSER=,NOCODB_DATABASE_READONLYUSER_PASSWORD=,NOCODB_REDIS_PASSWORD=,NOCODB_AUTH_JWT_SECRET=,NOCODB_MINIO_KEY=,NOCODB_MINIO_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Ente" "ENTE_INIT_ENV=false,ENTE_ADMIN_USERNAME=,ENTE_ADMIN_EMAIL_ADDRESS=,ENTE_ADMIN_PASSWORD=,ENTE_DATABASE_NAME=,ENTE_DATABASE_USER=,ENTE_DATABASE_USER_PASSWORD=,ENTE_DATABASE_READONLYUSER=,ENTE_DATABASE_READONLYUSER_PASSWORD=,ENTE_MINIO_KEY=,ENTE_MINIO_SECRET=,ENTE_MUSEUM_KEY=,ENTE_MUSEUM_HASH=,ENTE_MUSEUM_JWT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Morphic" "MORPHIC_INIT_ENV=false,MORPHIC_ADMIN_USERNAME=,MORPHIC_ADMIN_EMAIL_ADDRESS=,MORPHIC_ADMIN_PASSWORD=,MORPHIC_DATABASE_NAME=,MORPHIC_DATABASE_USER=,MORPHIC_DATABASE_USER_PASSWORD=,MORPHIC_DATABASE_READONLYUSER=,MORPHIC_DATABASE_READONLYUSER_PASSWORD=,MORPHIC_REDIS_PASSWORD=,MORPHIC_SUPABASE_DASHBOARD_USERNAME=,MORPHIC_SUPABASE_DASHBOARD_PASSWORD=,MORPHIC_SUPABASE_ANON_KEY=,MORPHIC_SUPABASE_JWT_SECRET=,MORPHIC_SUPABASE_DB_ENC_KEY=,MORPHIC_SUPABASE_PG_META_CRYPTO_KEY=,MORPHIC_SUPABASE_SECRET_KEY_BASE=,MORPHIC_SUPABASE_SERVICE_ROLE_KEY=,MORPHIC_SUPABASE_VAULT_ENC_KEY=,MORPHIC_SUPABASE_LOGFLARE_PRIVATE_ACCESS_TOKEN=,MORPHIC_SUPABASE_LOGFLARE_PUBLIC_ACCESS_TOKEN=" $CONFIG_FILE false checkAddServiceToConfig "OpenNotebook" "OPENNOTEBOOK_INIT_ENV=false,OPENNOTEBOOK_ADMIN_USERNAME=,OPENNOTEBOOK_ADMIN_EMAIL_ADDRESS=,OPENNOTEBOOK_ADMIN_PASSWORD=,OPENNOTEBOOK_SURREALDB_NAME=,OPENNOTEBOOK_SURREALDB_USER=,OPENNOTEBOOK_SURREALDB_PASSWORD=,OPENNOTEBOOK_ENCRYPTION_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Appsmith" "APPSMITH_INIT_ENV=false,APPSMITH_ADMIN_USERNAME=,APPSMITH_ADMIN_EMAIL_ADDRESS=,APPSMITH_ADMIN_PASSWORD=,APPSMITH_SUPERVISOR_USER=,APPSMITH_SUPERVISOR_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Trilium" "TRILIUM_INIT_ENV=false,TRILIUM_ADMIN_USERNAME=,TRILIUM_ADMIN_PASSWORD=,TRILIUM_OIDC_CLIENT_ID=,TRILIUM_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "DocsGPT" "DOCSGPT_INIT_ENV=false,DOCSGPT_ADMIN_USERNAME=,DOCSGPT_ADMIN_EMAIL_ADDRESS=,DOCSGPT_ADMIN_PASSWORD=,DOCSGPT_DATABASE_NAME=,DOCSGPT_DATABASE_USER=,DOCSGPT_DATABASE_USER_PASSWORD=,DOCSGPT_DATABASE_READONLYUSER=,DOCSGPT_DATABASE_READONLYUSER_PASSWORD=,DOCSGPT_REDIS_PASSWORD=,DOCSGPT_API_KEY=,DOCSGPT_JWT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Memos" "MEMOS_INIT_ENV=false,MEMOS_ADMIN_USERNAME=,MEMOS_ADMIN_EMAIL_ADDRESS=,MEMOS_ADMIN_PASSWORD=,MEMOS_DATABASE_NAME=,MEMOS_DATABASE_USER=,MEMOS_DATABASE_USER_PASSWORD=,MEMOS_DATABASE_READONLYUSER=,MEMOS_DATABASE_READONLYUSER_PASSWORD=,MEMOS_OIDC_CLIENT_ID=,MEMOS_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "SillyTavern" "SILLYTAVERN_INIT_ENV=false" $CONFIG_FILE false checkAddServiceToConfig "Lemonade" "LEMONADE_INIT_ENV=false,LEMONADE_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Speakr" "SPEAKR_INIT_ENV=false,SPEAKR_ADMIN_USERNAME=,SPEAKR_ADMIN_EMAIL_ADDRESS=,SPEAKR_ADMIN_PASSWORD=,SPEAKR_DATABASE_NAME=,SPEAKR_DATABASE_USER=,SPEAKR_DATABASE_USER_PASSWORD=,SPEAKR_DATABASE_READONLYUSER=,SPEAKR_DATABASE_READONLYUSER_PASSWORD=,SPEAKR_OIDC_CLIENT_ID=,SPEAKR_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "InsanelyFastWhisper" "INSANELYFASTWHISPER_INIT_ENV=false,INSANELYFASTWHISPER_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "IVBox" "IVBOX_INIT_ENV=false" $CONFIG_FILE false checkAddServiceToConfig "Monica" "MONICA_INIT_ENV=false,MONICA_ADMIN_USERNAME=,MONICA_ADMIN_EMAIL_ADDRESS=,MONICA_ADMIN_PASSWORD=,MONICA_DATABASE_NAME=,MONICA_DATABASE_ROOT_PASSWORD=,MONICA_DATABASE_USER=,MONICA_DATABASE_USER_PASSWORD=,MONICA_DATABASE_READONLYUSER=,MONICA_DATABASE_READONLYUSER_PASSWORD=,MONICA_REDIS_PASSWORD=,MONICA_APP_KEY=,MONICA_MEILISEARCH_KEY=,MONICA_MEMCACHE_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "AFFiNE" "AFFINE_INIT_ENV=false,AFFINE_ADMIN_USERNAME=,AFFINE_ADMIN_EMAIL_ADDRESS=,AFFINE_ADMIN_PASSWORD=,AFFINE_DATABASE_NAME=,AFFINE_DATABASE_USER=,AFFINE_DATABASE_USER_PASSWORD=,AFFINE_DATABASE_READONLYUSER=,AFFINE_DATABASE_READONLYUSER_PASSWORD=,AFFINE_REDIS_PASSWORD=,AFFINE_OIDC_CLIENT_ID=,AFFINE_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Joplin" "JOPLIN_INIT_ENV=false,JOPLIN_ADMIN_USERNAME=,JOPLIN_ADMIN_EMAIL_ADDRESS=,JOPLIN_ADMIN_PASSWORD=,JOPLIN_DATABASE_NAME=,JOPLIN_DATABASE_USER=,JOPLIN_DATABASE_USER_PASSWORD=,JOPLIN_DATABASE_READONLYUSER=,JOPLIN_DATABASE_READONLYUSER_PASSWORD=,JOPLIN_TRANSCRIBE_DATABASE_NAME=,JOPLIN_TRANSCRIBE_DATABASE_USER=,JOPLIN_TRANSCRIBE_DATABASE_USER_PASSWORD=,JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER=,JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER_PASSWORD=,JOPLIN_TRANSCRIBE_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Superset" "SUPERSET_INIT_ENV=false,SUPERSET_ADMIN_USERNAME=,SUPERSET_ADMIN_EMAIL_ADDRESS=,SUPERSET_ADMIN_PASSWORD=,SUPERSET_DATABASE_NAME=,SUPERSET_DATABASE_USER=,SUPERSET_DATABASE_USER_PASSWORD=,SUPERSET_DATABASE_READONLYUSER=,SUPERSET_DATABASE_READONLYUSER_PASSWORD=,SUPERSET_SECRET_KEY=,SUPERSET_JWT_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "Kokoro" "KOKORO_INIT_ENV=false,KOKORO_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Chatterbox" "CHATTERBOX_INIT_ENV=false,CHATTERBOX_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "LiteLLM" "LITELLM_INIT_ENV=false,LITELLM_ADMIN_USERNAME=,LITELLM_ADMIN_EMAIL_ADDRESS=,LITELLM_ADMIN_PASSWORD=,LITELLM_DATABASE_NAME=,LITELLM_DATABASE_USER=,LITELLM_DATABASE_USER_PASSWORD=,LITELLM_DATABASE_READONLYUSER=,LITELLM_DATABASE_READONLYUSER_PASSWORD=,LITELLM_REDIS_PASSWORD=,LITELLM_MASTER_KEY=,LITELLM_OIDC_CLIENT_ID=,LITELLM_OIDC_CLIENT_SECRET=,LITELLM_SALT_KEY=,LITELLM_AUTH_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Langfuse" "LANGFUSE_INIT_ENV=false,LANGFUSE_ADMIN_USERNAME=,LANGFUSE_ADMIN_EMAIL_ADDRESS=,LANGFUSE_ADMIN_PASSWORD=,LANGFUSE_DATABASE_NAME=,LANGFUSE_DATABASE_USER=,LANGFUSE_DATABASE_USER_PASSWORD=,LANGFUSE_DATABASE_READONLYUSER=,LANGFUSE_DATABASE_READONLYUSER_PASSWORD=,LANGFUSE_REDIS_PASSWORD=,LANGFUSE_PUBLIC_KEY=,LANGFUSE_SECRET_KEY=,LANGFUSE_INIT_PROJECT_ID=,LANGFUSE_MINIO_KEY=,LANGFUSE_MINIO_SECRET=,LANGFUSE_OIDC_CLIENT_ID=,LANGFUSE_OIDC_CLIENT_SECRET=,LANGFUSE_SALT=,LANGFUSE_ENCRYPTION_KEY=,LANGFUSE_NEXTAUTH_SECRET=,LANGFUSE_CLICKHOUSE_DB=,LANGFUSE_CLICKHOUSE_USER=,LANGFUSE_CLICKHOUSE_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Skyvern" "SKYVERN_INIT_ENV=false,SKYVERN_ADMIN_USERNAME=,SKYVERN_ADMIN_EMAIL_ADDRESS=,SKYVERN_ADMIN_PASSWORD=,SKYVERN_DATABASE_NAME=,SKYVERN_DATABASE_USER=,SKYVERN_DATABASE_USER_PASSWORD=,SKYVERN_DATABASE_READONLYUSER=,SKYVERN_DATABASE_READONLYUSER_PASSWORD=,SKYVERN_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Wger" "WGER_INIT_ENV=false,WGER_ADMIN_USERNAME=,WGER_ADMIN_EMAIL_ADDRESS=,WGER_ADMIN_PASSWORD=,WGER_DATABASE_NAME=,WGER_DATABASE_USER=,WGER_DATABASE_USER_PASSWORD=,WGER_DATABASE_READONLYUSER=,WGER_DATABASE_READONLYUSER_PASSWORD=,WGER_REDIS_PASSWORD=,WGER_SECRET_KEY=,WGER_SIGNING_KEY=,WGER_CELERY_FLOWER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "WorkoutCool" "WORKOUTCOOL_INIT_ENV=false,WORKOUTCOOL_ADMIN_USERNAME=,WORKOUTCOOL_ADMIN_EMAIL_ADDRESS=,WORKOUTCOOL_ADMIN_PASSWORD=,WORKOUTCOOL_DATABASE_NAME=,WORKOUTCOOL_DATABASE_USER=,WORKOUTCOOL_DATABASE_USER_PASSWORD=,WORKOUTCOOL_DATABASE_READONLYUSER=,WORKOUTCOOL_DATABASE_READONLYUSER_PASSWORD=,WORKOUTCOOL_BETTER_AUTH_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "OpenRag" "OPENRAG_INIT_ENV=false,OPENRAG_ADMIN_USERNAME=,OPENRAG_ADMIN_EMAIL_ADDRESS=,OPENRAG_ADMIN_PASSWORD=,OPENRAG_OPENSEARCH_USERNAME=,OPENRAG_OPENSEARCH_PASSWORD=,OPENRAG_LANGFLOW_ADMIN_USERNAME=,OPENRAG_LANGFLOW_ADMIN_PASSWORD=,OPENRAG_LANGFLOW_API_KEY=,OPENRAG_LANGFLOW_SECRET_KEY=,OPENRAG_LANGFUSE_SECRET_KEY=,OPENRAG_LANGFUSE_PUBLIC_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Voicebox" "VOICEBOX_INIT_ENV=false,VOICEBOX_ADMIN_USERNAME=,VOICEBOX_ADMIN_EMAIL_ADDRESS=,VOICEBOX_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "OpenCode" "OPENCODE_INIT_ENV=false,OPENCODE_ADMIN_USERNAME=,OPENCODE_ADMIN_PASSWORD=,OPENCODE_CODESERVER_USERNAME=,OPENCODE_CODESERVER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "OpenSkills" "OPENSKILLS_INIT_ENV=false,OPENSKILLS_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "EmailClassifierAI" "EMAILCLASSIFIERAI_INIT_ENV=false,EMAILCLASSIFIERAI_ADMIN_USERNAME=,EMAILCLASSIFIERAI_ADMIN_PASSWORD=,EMAILCLASSIFIERAI_REDIS_PASSWORD=,EMAILCLASSIFIERAI_FLASK_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "HermesAgent" "HERMES_AGENT_INIT_ENV=false,HERMES_AGENT_ADMIN_USERNAME=,HERMES_AGENT_ADMIN_PASSWORD=,HERMES_AGENT_SUDO_PASSWORD=,HERMES_AGENT_API_KEY=,HERMES_AGENT_WEBUI_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "AutoKB" "AUTOKB_INIT_ENV=false,AUTOKB_ADMIN_USERNAME=,AUTOKB_ADMIN_PASSWORD=,AUTOKB_DATABASE_NAME=,AUTOKB_DATABASE_USER=,AUTOKB_DATABASE_USER_PASSWORD=,AUTOKB_DATABASE_READONLYUSER=,AUTOKB_DATABASE_READONLYUSER_PASSWORD=,AUTOKB_REDIS_PASSWORD=,AUTOKB_API_KEY=,AUTOKB_BACKEND_API_KEY=,AUTOKB_WEBHOOK_API_KEY=,AUTOKB_ENCRYPTION_KEY=,AUTOKB_ENCRYPTION_SALT=,AUTOKB_SHARED_OWUI_TARGET_ID=,AUTOKB_SHARED_RAGFLOW_TARGET_ID=" $CONFIG_FILE false checkAddServiceToConfig "SuiteCRM" "SUITECRM_INIT_ENV=false,SUITECRM_ADMIN_USERNAME=,SUITECRM_ADMIN_EMAIL_ADDRESS=,SUITECRM_ADMIN_PASSWORD=,SUITECRM_DATABASE_NAME=,SUITECRM_DATABASE_ROOT_PASSWORD=,SUITECRM_DATABASE_USER=,SUITECRM_DATABASE_USER_PASSWORD=,SUITECRM_DATABASE_READONLYUSER=,SUITECRM_DATABASE_READONLYUSER_PASSWORD=,SUITECRM_REDIS_PASSWORD=,SUITECRM_APP_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "HedgeDoc" "HEDGEDOC_INIT_ENV=false,HEDGEDOC_ADMIN_USERNAME=,HEDGEDOC_ADMIN_EMAIL_ADDRESS=,HEDGEDOC_ADMIN_PASSWORD=,HEDGEDOC_DATABASE_NAME=,HEDGEDOC_DATABASE_USER=,HEDGEDOC_DATABASE_USER_PASSWORD=,HEDGEDOC_DATABASE_READONLYUSER=,HEDGEDOC_DATABASE_READONLYUSER_PASSWORD=,HEDGEDOC_SESSION_SECRET=,HEDGEDOC_ADMIN_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "Presenton" "PRESENTON_INIT_ENV=false,PRESENTON_ADMIN_USERNAME=,PRESENTON_ADMIN_EMAIL_ADDRESS=,PRESENTON_ADMIN_PASSWORD=,PRESENTON_ADMIN_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "BasicMemory" "BASICMEMORY_INIT_ENV=false,BASICMEMORY_ADMIN_USERNAME=,BASICMEMORY_ADMIN_EMAIL_ADDRESS=,BASICMEMORY_ADMIN_PASSWORD=,BASICMEMORY_DATABASE_NAME=,BASICMEMORY_DATABASE_USER=,BASICMEMORY_DATABASE_USER_PASSWORD=,BASICMEMORY_DATABASE_READONLYUSER=,BASICMEMORY_DATABASE_READONLYUSER_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "Cognee" "COGNEE_INIT_ENV=false,COGNEE_ADMIN_USERNAME=,COGNEE_ADMIN_EMAIL_ADDRESS=,COGNEE_ADMIN_PASSWORD=,COGNEE_DATABASE_NAME=,COGNEE_DATABASE_USER=,COGNEE_DATABASE_USER_PASSWORD=,COGNEE_DATABASE_READONLYUSER=,COGNEE_DATABASE_READONLYUSER_PASSWORD=,COGNEE_REDIS_PASSWORD=,COGNEE_JWT_SECRET=,COGNEE_VERIFICATION_TOKEN_SECRET=,COGNEE_RESET_PASSWORD_TOKEN_SECRET=" $CONFIG_FILE false checkAddServiceToConfig "LightRAG" "LIGHTRAG_INIT_ENV=false,LIGHTRAG_ADMIN_USERNAME=,LIGHTRAG_ADMIN_EMAIL_ADDRESS=,LIGHTRAG_ADMIN_PASSWORD=,LIGHTRAG_DATABASE_NAME=,LIGHTRAG_DATABASE_USER=,LIGHTRAG_DATABASE_USER_PASSWORD=,LIGHTRAG_DATABASE_READONLYUSER=,LIGHTRAG_DATABASE_READONLYUSER_PASSWORD=,LIGHTRAG_TOKEN_SECRET=,LIGHTRAG_API_KEY=,LIGHTRAG_QDRANT_API_KEY=,LIGHTRAG_NEO4J_DATABASE=,LIGHTRAG_NEO4J_USER=,LIGHTRAG_NEO4J_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "OpenSERP" "OPENSERP_INIT_ENV=false" $CONFIG_FILE false checkAddServiceToConfig "Suricata" "SURICATA_INIT_ENV=false,SURICATA_ADMIN_USERNAME=,SURICATA_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "CrowdSec" "CROWDSEC_INIT_ENV=false,CROWDSEC_ADMIN_USERNAME=,CROWDSEC_ADMIN_PASSWORD=,CROWDSEC_LAPI_USERNAME=,CROWDSEC_LAPI_PASSWORD=,CROWDSEC_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "fastCRW" "FASTCRW_INIT_ENV=false,FASTCRW_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "WhiteboardRT" "WHITEBOARDRT_INIT_ENV=false,WHITEBOARDRT_REDIS_PASSWORD=" $CONFIG_FILE false checkAddServiceToConfig "mdCollab" "MDCOLLAB_INIT_ENV=false,MDCOLLAB_ADMIN_USERNAME=,MDCOLLAB_ADMIN_EMAIL_ADDRESS=,MDCOLLAB_ADMIN_PASSWORD=,MDCOLLAB_DATABASE_NAME=,MDCOLLAB_DATABASE_USER=,MDCOLLAB_DATABASE_USER_PASSWORD=,MDCOLLAB_DATABASE_READONLYUSER=,MDCOLLAB_DATABASE_READONLYUSER_PASSWORD=,MDCOLLAB_SESSION_SECRET=,MDCOLLAB_ADMIN_API_KEY=" $CONFIG_FILE false checkAddServiceToConfig "HaloSpeak" "HALOSPEAK_INIT_ENV=false,HALOSPEAK_API_KEY=" $CONFIG_FILE false #ADD_NEW_ADD_SVC_CONFIG_HERE checkAddVarsToServiceConfig "Mailu" "MAILU_API_TOKEN=" $CONFIG_FILE false checkAddVarsToServiceConfig "PhotoPrism" "PHOTOPRISM_INIT_ENV=false" $CONFIG_FILE false checkAddVarsToServiceConfig "PhotoPrism" "PHOTOPRISM_ADMIN_USERNAME=" $CONFIG_FILE false checkAddVarsToServiceConfig "PhotoPrism" "PHOTOPRISM_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mastodon" "MASTODON_ARE_DETERMINISTIC_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mastodon" "MASTODON_ARE_KEY_DERIVATION_SALT=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mastodon" "MASTODON_ARE_PRIMARY_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "HomeAssistant" "HOMEASSISTANT_CONFIGURATOR_USER=,HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mealie" "MEALIE_DATABASE_NAME=,MEALIE_DATABASE_USER=,MEALIE_DATABASE_USER_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Remotely" "REMOTELY_INIT_ENV=false" $CONFIG_FILE false checkAddVarsToServiceConfig "Calibre" "CALIBRE_WEB_INIT_ENV=false" $CONFIG_FILE false checkAddVarsToServiceConfig "FreshRSS" "FRESHRSS_INIT_ENV=false" $CONFIG_FILE false checkAddVarsToServiceConfig "Keila" "KEILA_INIT_ENV=false" $CONFIG_FILE false checkAddVarsToServiceConfig "Wazuh" "WAZUH_MANAGER_AUTH_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Paperless" "PAPERLESS_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddVarsToServiceConfig "Linkwarden" "LINKWARDEN_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddVarsToServiceConfig "FreshRSS" "FRESHRSS_OIDC_CLIENT_SECRET=" $CONFIG_FILE false checkAddVarsToServiceConfig "Duplicati" "DUPLICATI_SETTINGS_ENCRYPTION_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "FireflyIII" "FIREFLY_STATIC_CRON_TOKEN=" $CONFIG_FILE false checkAddVarsToServiceConfig "Homarr" "HOMARR_ADMIN_EMAIL_ADDRESS=" $CONFIG_FILE false checkAddVarsToServiceConfig "Nextcloud" "NEXTCLOUD_TALKHPB_SIGNALING_SECRET=,NEXTCLOUD_TALKHPB_INTERNAL_SECRET=,NEXTCLOUD_TALKHPB_RECORDING_SECRET=" $CONFIG_FILE false checkAddVarsToServiceConfig "Portainer" "PORTAINER_ENDPOINT_ID=1" $HSHQ_PLAINTEXT_ROOT_CONFIG true checkAddVarsToServiceConfig "Nextcloud" "NEXTCLOUD_HARP_PORT=8780,NEXTCLOUD_HARP_SHARED_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Caddy" "CADDY_SNIPPET_SAFEHEADERCORSPREFLIGHT=safe-header-cors-preflight" $CONFIG_FILE false checkAddVarsToServiceConfig "Wordpress" "WORDPRESS_INIT_ENV=false,WORDPRESS_ADMIN_USERNAME=,WORDPRESS_ADMIN_PASSWORD=,WORDPRESS_ADMIN_EMAIL_ADDRESS=" $CONFIG_FILE false checkAddVarsToServiceConfig "ComfyUI" "COMFYUI_API_KEY=" $CONFIG_FILE true checkAddVarsToServiceConfig "Metabase" "METABASE_API_KEY=" $CONFIG_FILE true checkAddVarsToServiceConfig "OpenNotebook" "OPENNOTEBOOK_ENCRYPTION_KEY=" $CONFIG_FILE true checkAddVarsToServiceConfig "Langflow" "LANGFLOW_API_KEY=,LANGFLOW_SECRET_KEY=,LANGFLOW_LANGFUSE_SECRET_KEY=,LANGFLOW_LANGFUSE_PUBLIC_KEY=" $CONFIG_FILE true checkAddVarsToServiceConfig "RAGFlow" "RAGFLOW_SECRET_KEY=" $CONFIG_FILE true checkAddVarsToServiceConfig "Paperless" "PAPERLESS_AI_ADMIN_USERNAME=,PAPERLESS_AI_ADMIN_PASSWORD=,PAPERLESS_AI_API_KEY=,PAPERLESS_AI_JWT_SECRET=" $CONFIG_FILE true checkAddVarsToServiceConfig "Paperless" "PAPERLESS_GPT_ADMIN_USERNAME=,PAPERLESS_GPT_ADMIN_PASSWORD=,PAPERLESS_API_TOKEN=" $CONFIG_FILE true checkAddVarsToServiceConfig "OpenWebUI" "OPENWEBUI_ADMIN_API_KEY=,OPENWEBUI_QDRANT_API_KEY=,OPENWEBUI_OPENTERMINAL_API_KEY=,OPENWEBUI_MCPO_API_KEY=,OPENWEBUI_PIPELINES_API_KEY=" $CONFIG_FILE true checkAddVarsToServiceConfig "Bar Assistant" "BARASSISTANT_API_KEY=,BARASSISTANT_MCP_API_KEY=" $CONFIG_FILE true checkAddVarsToServiceConfig "Dolibarr" "DOLIBARR_INSTANCE_UNIQUE_ID=,DOLIBARR_API_KEY=,DOLIBARR_MCP_API_KEY=,DOLIBARR_MCP_REDIS_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Dolibarr" "DOLIBARR_INSTANCE_UNIQUE_ID=,DOLIBARR_API_KEY=,DOLIBARR_MCP_API_KEY=,DOLIBARR_MCP_REDIS_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "Nextcloud" "NEXTCLOUD_TOKEN_ENCRYPTION_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Immich" "IMMICH_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Caddy" "CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED=safe-header-cors-automated,CADDY_SNIPPET_BASEHEADER=base-header,CADDY_SNIPPET_DEFAULTCSP=default-csp,CADDY_SNIPPET_RELAXEDCSP=relaxed-csp,CADDY_SNIPPET_LOG_TRUE=log_true,CADDY_SNIPPET_LOG_FALSE=log_false,CADDY_SNIPPET_INTERNAL_GUARD=internal_guard,CROWDSEC_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "OpenProject" "OPENPROJECT_SECRET_KEY_BASE=" $CONFIG_FILE false checkAddVarsToServiceConfig "Twenty" "TWENTY_APP_SECRET=,TWENTY_ENCRYPTION_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Paperless" "PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME=,PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID=,PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME=,PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID=,PAPERLESS_KNOWLEDGEBASE_TAG_NAME=,PAPERLESS_KNOWLEDGEBASE_TAG_ID=,PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME=,PAPERLESS_TRANSCRIPTION_DOCTYPE_ID=,PAPERLESS_AKB_WORKFLOW_ID=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mailu" "EMAIL_JOINT_USERNAME=,EMAIL_JOINT_PASSWORD=,EMAIL_JOINT_EMAIL_ADDRESS=" $CONFIG_FILE false checkAddVarsToServiceConfig "OpenWebUI" "OPENWEBUI_ADMIN_UUID=,OPENWEBUI_PRIMARYUSERS_UUID=" $CONFIG_FILE false checkAddVarsToServiceConfig "AutoKB" "AUTOKB_SHARED_OWUI_TARGET_ID=,AUTOKB_SHARED_RAGFLOW_TARGET_ID=" $CONFIG_FILE false checkAddVarsToServiceConfig "Wordpress" "WORDPRESS_APP_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "InvoiceShelf" "INVOICESHELF_ADMIN_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Keila" "KEILA_ADMIN_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Wikijs" "WIKIJS_ADMIN_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Twenty" "TWENTY_ADMIN_API_KEY=,TWENTY_AKB_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "HedgeDoc" "HEDGEDOC_ADMIN_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Mealie" "MEALIE_ADMIN_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "Presenton" "PRESENTON_ADMIN_API_KEY=" $CONFIG_FILE false checkAddVarsToServiceConfig "RAGFlow" "RAGFLOW_DATABASE_ROOT_PASSWORD=,RAGFLOW_ADMIN_API_KEY=,RAGFLOW_SANDBOX_EXECUTOR_MANAGER_API_TOKEN=,RAGFLOW_TEAM_API_KEY=,RAGFLOW_TEAM_TENANT_ID=" $CONFIG_FILE false checkAddVarsToServiceConfig "AutoKB" "AUTOKB_ENCRYPTION_SALT=" $CONFIG_FILE false checkAddVarsToServiceConfig "SysUtils" "ALLOY_ADMIN_USERNAME=,ALLOY_ADMIN_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "LightRAG" "LIGHTRAG_NEO4J_DATABASE=,LIGHTRAG_NEO4J_USER=,LIGHTRAG_NEO4J_PASSWORD=" $CONFIG_FILE false checkAddVarsToServiceConfig "LiteLLM" "LITELLM_AUTH_KEY=" $CONFIG_FILE false initServicesCredentials } function importDBs() { # This function is just here as a reminder to add any # DB connection info to SQLPad and/or the AIStack return } function getHomeServerPortsList() { portsList="$ADGUARD_DNS_PORT,$CADDY_HTTP_PORT,$CADDY_HTTPS_PORT,$COTURN_PRIMARY_PORT,$COTURN_SECONDARY_PORT,$COTURN_COMMS_MIN_PORT:$COTURN_COMMS_MAX_PORT,$JELLYFIN_PORT,$JITSI_COLIBRI_PORT,$JITSI_JVB_PORT,$JITSI_MEET_PORT,$KHOJ_VNC_PORT,$MAILU_PORT_1,$MAILU_PORT_2,$MAILU_PORT_3,$MAILU_PORT_4,$MAILU_PORT_5,$MAILU_PORT_6,$MAILU_PORT_7,$PEERTUBE_RDP_PORT,$QBITTORRENT_PORT,$SYNCTHING_DISC_PORT,$SYNCTHING_SYNC_PORT,$UPNP_PORT,$VNC_SERVER_PORT,$WAZUH_PORT_1,$WAZUH_PORT_2,$WAZUH_PORT_3,$WAZUH_PORT_4,$WAZUH_PORT_5" echo "$portsList" } # Custom images function buildOrPullImage() { curImg="$1" maxTries="$2" if [ -z "$curImg" ]; then echo "Empty image!!!" return 1 fi if [ "$(isImageInRepo $curImg)" = "true" ]; then echo "Image ($curImg) is already in local registry..." return fi curMax=$MAX_DOCKER_PULL_TRIES if ! [ -z "$maxTries" ] && [ $maxTries -gt 0 ]; then MAX_DOCKER_PULL_TRIES=$maxTries fi case "$curImg" in "filedrop/filedrop:1") buildImageFileDropV1 ;; "hshq/mindsdb:v1") buildImageMindsDBV1 ;; "hshq/mindsdb:v2") buildImageMindsDBV2 ;; "hshq/pixelfed:v1") buildImagePixelfedV1 ;; "hshq/sshbox:v1") buildImageLocalAISSHBoxV1 ;; "hshq/firecrawl-db:v1") buildImageFirecrawlDBV1 ;; "hshq/docling:v1") buildImageDoclingV1 ;; "hshq/docling-serve:v1.31.0") buildImageDoclingV2 ;; "hshq/docling-serve:v1.38.1") buildImageDoclingV3 ;; "hshq/docling-serve:v1.38.1.1") buildImageDoclingV4 ;; "hshq/ente-server:v1") buildImageEnteServerV1 ;; "hshq/docsgpt-backend:v1"|"hshq/docsgpt-frontend:v1") buildImageDocsGPTV1 ;; "hshq/insanelyfastwhisper:v1") buildImageInsanelyFastWhisperV1 ;; "hshq/insanelyfastwhisper:v2") buildImageInsanelyFastWhisperV2 ;; "hshq/insanelyfastwhisper:v3") buildImageInsanelyFastWhisperV3 ;; "hshq/ivbox:v1") buildImageIVBoxV1 ;; "hshq/monica-app:v1") buildImageMonicaV1 ;; "hshq/monica-web:v1") buildImageMonicaV1 ;; "hshq/kokoro-tts:v1") buildImageKokoroV1 ;; "hshq/chatterbox:v1") buildImageChatterboxV1 ;; "hshq/voicebox:v1") buildImageVoiceboxV1 ;; "hshq/openskills:v1") buildImageOpenSkillsV1 ;; "hshq/openwebui-unstupid:v1") buildImageOpenWebUIAppV1 ;; "hshq/openwebui-app:v2") buildImageOpenWebUIAppV2 ;; "hshq/openwebui-app:v3") buildImageOpenWebUIAppV3 ;; "hshq/openwebui-mcp:v1") buildImageOpenWebUIMCPV1 ;; "hshq/barassistant-mcp:v1") buildImageBarAssistantMCPV1 ;; "hshq/dolibarr-mcp:v1") buildImageDolibarrMCPV1 ;; "hshq/paperless-mcp:v1") buildImagePaperlessMCPV1 ;; "hshq/immich-mcp:v1") buildImageImmichMCPV1 ;; "hshq/excalidraw-web:v1") buildImageExcalidrawWebV1 ;; "hshq/excalidraw-ai:v1"|"hshq/excalidraw-mcp") buildImageExcalidrawAIV1 ;; "hshq/emailclassifierai:v1") buildImageEmailClassifierAIV1 ;; "hshq/linkwarden-mcp:v1") buildImageLinkwardenMCPV1 ;; "hshq/searxng-mcp:v1") buildImageSearxNGMCPV1 ;; "hshq/hermes-agent:v1") buildImageHermesAgentV1 ;; "hshq/hermes-terminal:v1") buildImageHermesTerminalV1 ;; "hshq/paperless-ai-next:v1") buildImagePaperlessAINextV1 ;; *) pullImage "$curImg" ;; esac bop_rtVal=$? # The above command catches return value for all case statements MAX_DOCKER_PULL_TRIES=$curMax return $bop_rtVal } function checkIsCustomImage() { curImg="$1" case "$curImg" in "filedrop/filedrop:1") echo "true" ;; "hshq/mindsdb:v1") echo "true" ;; "hshq/mindsdb:v2") echo "true" ;; "hshq/pixelfed:v1") echo "true" ;; "hshq/sshbox:v1") echo "true" ;; "hshq/firecrawl-db:v1") echo "true" ;; "hshq/docling:v1") echo "true" ;; "hshq/ente-server:v1") echo "true" ;; "hshq/docsgpt-backend:v1"|"hshq/docsgpt-frontend:v1") echo "true" ;; "hshq/insanelyfastwhisper:v1") echo "true" ;; "hshq/insanelyfastwhisper:v2") echo "true" ;; "hshq/insanelyfastwhisper:v3") echo "true" ;; "hshq/ivbox:v1") echo "true" ;; "hshq/monica-app:v1") echo "true" ;; "hshq/monica-web:v1") echo "true" ;; "hshq/kokoro-tts:v1") echo "true" ;; "hshq/chatterbox:v1") echo "true" ;; "hshq/voicebox:v1") echo "true" ;; "hshq/openskills:v1") echo "true" ;; "hshq/openwebui-unstupid:v1") echo "true" ;; "hshq/openwebui-app:v2") echo "true" ;; "hshq/openwebui-app:v3") echo "true" ;; "hshq/openwebui-mcp:v1") echo "true" ;; "hshq/barassistant-mcp:v1") echo "true" ;; "hshq/dolibarr-mcp:v1") echo "true" ;; "hshq/paperless-mcp:v1") echo "true" ;; "hshq/immich-mcp:v1") echo "true" ;; "hshq/excalidraw-web:v1") echo "true" ;; "hshq/excalidraw-ai:v1"|"hshq/excalidraw-mcp") echo "true" ;; "hshq/emailclassifierai:v1") echo "true" ;; "hshq/linkwarden-mcp:v1") echo "true" ;; "hshq/searxng-mcp:v1") echo "true" ;; "hshq/hermes-agent:v1") echo "true" ;; "hshq/hermes-terminal:v1") echo "true" ;; "hshq/paperless-ai-next:v1") echo "true" ;; *) echo "false" ;; esac } # Stacks Installation/Update Functions # ===================================== # Portainer function installPortainer() { set +e is_containers=$(docker ps -q) if [ ${#is_containers} -gt 0 ]; then showMessageBox "Docker Containers Running" "There are docker containers running. Please stop these containers before continuing. Exiting..." exit 1 fi # Don't install if directory exists checkDeleteStackAndDirectory portainer "Portainer" false cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi set -e createDockerNetworks mkdir $HSHQ_STACKS_DIR/portainer mkdir $HSHQ_STACKS_DIR/portainer/certs initServicesCredentials outputConfigPortainer generateCert portainer portainer "127.0.0.1,$HOMESERVER_HOST_PRIMARY_INTERFACE_IP" if [ -z "$PORTAINER_DB_KEY" ]; then PORTAINER_DB_KEY=$(pwgen -c -n 64 1) updateConfigVar PORTAINER_DB_KEY $PORTAINER_DB_KEY rm -f $HSHQ_SECRETS_DIR/portainer_key.txt echo $PORTAINER_DB_KEY > $HSHQ_SECRETS_DIR/portainer_key.txt chmod 0400 $HSHQ_SECRETS_DIR/portainer_key.txt elif ! [ -f $HSHQ_SECRETS_DIR/portainer_key.txt ]; then echo $PORTAINER_DB_KEY > $HSHQ_SECRETS_DIR/portainer_key.txt chmod 0400 $HSHQ_SECRETS_DIR/portainer_key.txt fi # Version 74 Update - see note at very top inside of init() function. cd ~ sudo docker compose -f $HSHQ_STACKS_DIR/portainer/docker-compose.yml up -d search="starting HTTPS server" isFound=false i=0 set +e while [ $i -le 300 ] do findtext=$(docker logs portainer 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 1 second, total wait=$i seconds..." sleep 1 i=$((i+1)) done set -e if ! [ "$isFound" = "true" ]; then echo "Portainer did not start up correctly..." exit 1 fi echo "Portainer has been loaded and started..." echo "Sleeping 10 seconds to ensure Portainer has loaded completely." sleep 10 PORTAINER_ADMIN_USERID=$(http --verify=no --print="b" POST https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/users/admin/init Username="$PORTAINER_ADMIN_USERNAME" Password="$PORTAINER_ADMIN_PASSWORD" | jq -r .Id) updateConfigVar PORTAINER_ADMIN_USERID $PORTAINER_ADMIN_USERID PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" http -f --verify=no --timeout=300 --print="b" POST https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/endpoints "Authorization: Bearer $PORTAINER_TOKEN" Name="$HOMESERVER_NAME" EndpointCreationType=1 >/dev/null # Enable dark mode because it looks better echo "{\"theme\":{\"color\":\"dark\"}}" > $HOME/portainer-json.tmp http --verify=no --timeout=300 PUT https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/users/$PORTAINER_ADMIN_USERID "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/portainer-json.tmp > /dev/null rm $HOME/portainer-json.tmp # Add gcr registry echo "{\"Name\": \"GCR\",\"Type\": 3,\"URL\": \"mirror.gcr.io\"}" > $HOME/portainer-json.tmp http --verify=no --timeout=300 POST https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/registries "Authorization: Bearer $PORTAINER_TOKEN" @$HOME/portainer-json.tmp > /dev/null rm $HOME/portainer-json.tmp inner_block="" inner_block=$inner_block">>https://$SUB_PORTAINER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://portainer:9443 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PORTAINER $MANAGETLS_PORTAINER "$is_integrate_hshq" $NETDEFAULT_PORTAINER "$inner_block" } function outputConfigPortainer() { DOCKER_SOCKET="/var/run/docker.sock" if ! [ "$(checkIsIPPrivate $HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" = "true" ]; then pdocknet=dock-ext else pdocknet=dock-privateip fi cat < $HSHQ_STACKS_DIR/portainer/docker-compose.yml $STACK_VERSION_PREFIX portainer $(getScriptStackVersion portainer) services: portainer: image: $(getScriptImageByContainerName portainer) container_name: portainer hostname: portainer restart: unless-stopped security_opt: - no-new-privileges:true env_file: portainer.env networks: - dock-proxy-net - ${pdocknet}-net ports: - "$PORTAINER_LOCAL_HTTPS_PORT:9443" command: --tlscert /data/certs/portainer.crt --tlskey /data/certs/portainer.key --http-disabled --no-setup-token volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${DOCKER_SOCKET}:/var/run/docker.sock - ${HSHQ_STACKS_DIR}/portainer:/data - ${HSHQ_SSL_DIR}/portainer.crt:/data/certs/portainer.crt:ro - ${HSHQ_SSL_DIR}/portainer.key:/data/certs/portainer.key:ro - ${HSHQ_SECRETS_DIR}/portainer_key.txt:/run/portainer/portainer - ${HSHQ_SECRETS_DIR}/portainer_key.txt:/run/secrets/portainer networks: dock-proxy-net: name: dock-proxy external: true ${pdocknet}-net: name: ${pdocknet} external: true EOFPC outputEnvPortainer } function outputEnvPortainer() { rm -f $HSHQ_STACKS_DIR/portainer/portainer.env tee $HSHQ_STACKS_DIR/portainer/portainer.env >/dev/null </dev/null ALL_INTERFACE_IPS="$(getAllInterfaceIPs)" ifListDBID=($(sqlite3 $HSHQ_DB "select ID from connections where NetworkType = 'home_network';")) for curID in "${ifListDBID[@]}" do curIF_Interface=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID = $curID;") curIF_IP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID = $curID;") curIF_Subnet=$(sqlite3 $HSHQ_DB "select Network_Subnet from connections where ID = $curID;") curIF_IPVar=$(getHSHostIPVarName $curIF_Interface) curIF_SubnetVar=$(getHSHostSubnetVarName $curIF_Interface) if ! [ -z "$curIF_IP" ] && [ "$(checkValidIPAddress $curIF_IP)" = "true" ] && ! [ "$curIF_IP" = "$DEFAULT_UNFOUND_IP_ADDRESS" ]; then echo "${curIF_IPVar}=${curIF_IP}" | tee -a $HSHQ_STACKS_DIR/portainer/portainer.env >/dev/null echo "${curIF_SubnetVar}=${curIF_Subnet}" | tee -a $HSHQ_STACKS_DIR/portainer/portainer.env >/dev/null fi done echo "PORTAINER_ALL_INTERFACE_IPS=${ALL_INTERFACE_IPS}" | tee -a $HSHQ_STACKS_DIR/portainer/portainer.env >/dev/null chown $USERID:$GROUPID $HSHQ_STACKS_DIR/portainer/portainer.env } function getAllInterfaceIPs() { all_iface_ips="" ifListDBID=($(sqlite3 $HSHQ_DB "select ID from connections where NetworkType = 'home_network';")) for curID in "${ifListDBID[@]}" do curIF_IP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ID = $curID;") if ! [ -z "$curIF_IP" ] && [ "$(checkValidIPAddress $curIF_IP)" = "true" ] && ! [ "$curIF_IP" = "$DEFAULT_UNFOUND_IP_ADDRESS" ]; then if ! [ "$curIF_IP" = "127.0.0.1" ]; then if [ -z "$all_iface_ips" ]; then all_iface_ips="${curIF_IP}" else all_iface_ips="$all_iface_ips","${curIF_IP}" fi fi fi done ipListArr=($(sqlite3 $HSHQ_DB "select IPAddress from connections where ConnectionType='homeserver_vpn' and NetworkType in ('primary','other');")) for curIP in "${ipListArr[@]}" do if [ -z "$all_iface_ips" ]; then all_iface_ips="${curIP}" else all_iface_ips="$all_iface_ips,${curIP}" fi done echo "$all_iface_ips" } function performUpdatePortainer() { # This is a special case and has not been tested. Ensure to test on next image release. perform_stack_name=portainer # This function modifies the variable perform_update_report # with the results of the update process. It is up to the # caller to do something with it. perform_update_report="" perform_compose=$HSHQ_STACKS_DIR/portainer/docker-compose.yml perform_stack_firstline=$(sudo sed -n 1p $perform_compose) perform_stack_ver=$(getVersionFromComposeLine "$perform_stack_firstline") unset image_update_map image_update_map="" oldVer=v"$perform_stack_ver" # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=portainer/portainer-ce:2.19.3-alpine image_update_map[0]="portainer/portainer-ce:2.19.3-alpine,portainer/portainer-ce:2.21.4-alpine" ;; 2) newVer=v3 curImageList=portainer/portainer-ce:2.19.4-alpine image_update_map[0]="portainer/portainer-ce:2.19.4-alpine,portainer/portainer-ce:2.21.4-alpine" ;; 3) newVer=v4 curImageList=portainer/portainer-ce:2.21.4-alpine image_update_map[0]="portainer/portainer-ce:2.21.4-alpine,mirror.gcr.io/portainer/portainer-ce:2.33.1-alpine" ;; 4) newVer=v5 curImageList=mirror.gcr.io/portainer/portainer-ce:2.33.1-alpine image_update_map[0]="mirror.gcr.io/portainer/portainer-ce:2.33.1-alpine,mirror.gcr.io/portainer/portainer-ce:2.33.5-alpine" stopPortainer outputConfigPortainer startPortainer sudo sed -i "1s|.*|$STACK_VERSION_PREFIX $perform_stack_name $newVer|" $perform_compose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v6 curImageList=mirror.gcr.io/portainer/portainer-ce:2.33.5-alpine image_update_map[0]="mirror.gcr.io/portainer/portainer-ce:2.33.5-alpine,mirror.gcr.io/portainer/portainer-ce:2.39.5-alpine" ;; 6) newVer=v6 curImageList=mirror.gcr.io/portainer/portainer-ce:2.39.5-alpine image_update_map[0]="mirror.gcr.io/portainer/portainer-ce:2.39.5-alpine,mirror.gcr.io/portainer/portainer-ce:2.39.5-alpine" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "0" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function restartPortainer() { stopPortainer sleep 3 startPortainer if ! [ -z "$PORTAINER_ADMIN_USERNAME" ] && ! [ -z "$PORTAINER_ADMIN_PASSWORD" ]; then PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" fi } function stopPortainer() { cd ~ docker compose -f $HSHQ_STACKS_DIR/portainer/docker-compose.yml down > /dev/null 2>&1 PORTAINER_TOKEN="" } function startPortainer() { stpo_curE=${-//[^e]/} set +e cd ~ docker compose -f $HSHQ_STACKS_DIR/portainer/docker-compose.yml up -d > /dev/null 2>&1 search="starting HTTPS server" isFound="F" i=0 set +e while [ $i -le 300 ] do findtext=$(docker logs portainer 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound="T" break fi echo "Container not ready, sleeping 1 second, total wait=$i seconds..." sleep 1 i=$((i+1)) done if [ "$isFound" = "F" ]; then return 1 fi PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)" if ! [ -z "$stpo_curE" ]; then set -e fi sleep 3 } function updatePortainerEnv() { stopPortainer outputEnvPortainer startPortainer } # Adguard function installAdGuard() { set +e is_integrate_hshq=$1 # Remove directory if it exists checkDeleteStackAndDirectory adguard "Adguard" cdRes=$? if [ $cdRes -ne 0 ]; then echo "ERROR: AdGuard directory exists" exit 1 fi pullImage $(getScriptImageByContainerName adguard) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain AdGuard docker image" exit 1 fi set -e mkdir $HSHQ_STACKS_DIR/adguard mkdir $HSHQ_STACKS_DIR/adguard/conf mkdir $HSHQ_NONBACKUP_DIR/adguard mkdir $HSHQ_NONBACKUP_DIR/adguard/work initServicesCredentials outputConfigAdGuard generateCert adguard adguard prepAdguardInstallation installStack adguard adguard "entering listener loop proto=tls" $HOME/adguard.env retval=$? if [ $retval -ne 0 ]; then echo "ERROR: There was a problem installing AdGuard" exit $retval fi sleep 5 isSuccess=false i=0 set +e while [ $i -le 300 ] do checkDNS1=$(dig +short api.ipify.org | grep '^[.0-9]*$' | tail -n 1) checkDNS2=$(dig +short registry-1.docker.io | grep '^[.0-9]*$' | tail -n 1) checkDNS3=$(dig +short mirror.gcr.io | grep '^[.0-9]*$' | tail -n 1) if ! [ -z "$checkDNS1" ] && [ "$(checkValidIPAddress $checkDNS1)" = "true" ] && ! [ -z "$checkDNS2" ] && [ "$(checkValidIPAddress $checkDNS2)" = "true" ] && ! [ -z "$checkDNS3" ] && [ "$(checkValidIPAddress $checkDNS3)" = "true" ]; then isSuccess=true break fi echo "Adguard not ready, sleeping 5 seconds, total wait=$i seconds..." sleep 5 i=$((i+5)) done if ! [ "$isSuccess" = "true" ]; then echo "Adguard did not install correctly, exiting..." exit 1 fi inner_block="" inner_block=$inner_block">>https://$SUB_ADGUARD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://adguard {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ADGUARD $MANAGETLS_ADGUARD "$is_integrate_hshq" $NETDEFAULT_ADGUARD "$inner_block" } function outputConfigAdGuard() { ADGUARD_ADMIN_PASSWORD_HASH=$(htpasswd -B -n -b $ADGUARD_ADMIN_USERNAME $ADGUARD_ADMIN_PASSWORD | cut -d":" -f2-) cat < $HOME/adguard-compose.yml $STACK_VERSION_PREFIX adguard $(getScriptStackVersion adguard) services: adguard: image: $(getScriptImageByContainerName adguard) container_name: adguard hostname: adguard restart: unless-stopped env_file: stack.env user: "\${PORTAINER_UID}:\${PORTAINER_GID}" security_opt: - no-new-privileges:true networks: dock-proxy-net: dock-ext-net: ipv4_address: \${NET_EXTERNAL_LAST_IP} ports: - "$ADGUARD_DNS_PORT:${ADGUARD_DNS_PORT}/tcp" - "$ADGUARD_DNS_PORT:${ADGUARD_DNS_PORT}/udp" - "127.0.0.1:$ADGUARD_LOCALHOST_PORT:443" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/adguard/conf:/opt/adguardhome/conf - \${PORTAINER_HSHQ_NONBACKUP_DIR}/adguard/work:/opt/adguardhome/work - \${PORTAINER_HSHQ_SSL_DIR}/adguard.crt:/opt/adguardhome/conf/cert.pem - \${PORTAINER_HSHQ_SSL_DIR}/adguard.key:/opt/adguardhome/conf/key.pem networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFAC cat < $HOME/adguard.env NET_EXTERNAL_LAST_IP=$(sipcalc $NET_EXTERNAL_SUBNET | grep "^Usable range" | rev | cut -d " " -f1 | cut -d"." -f2- | rev).253 GODEBUG=tlskyber=0 EOFAD cat < $HSHQ_STACKS_DIR/adguard/conf/AdGuardHome.yaml http: pprof: port: 6060 enabled: false address: 0.0.0.0:3000 session_ttl: 720h users: - name: $ADGUARD_ADMIN_USERNAME password: $ADGUARD_ADMIN_PASSWORD_HASH auth_attempts: 5 block_auth_min: 15 http_proxy: "" language: en theme: dark dns: bind_hosts: - 0.0.0.0 port: $ADGUARD_DNS_PORT anonymize_client_ip: false ratelimit: 0 ratelimit_subnet_len_ipv4: 24 ratelimit_subnet_len_ipv6: 56 ratelimit_whitelist: [] refuse_any: true upstream_dns: - https://dns.quad9.net/dns-query - https://anycast.dns.nextdns.io/dns-query - quic://dns-unfiltered.adguard.com:784 - https://dns.nextdns.io/dns-query upstream_dns_file: "" bootstrap_dns: - 9.9.9.9 - 149.112.112.112 fallback_dns: - 9.9.9.10 - 149.112.112.10 - 94.140.14.14 - 94.140.15.15 upstream_mode: parallel fastest_timeout: 1s allowed_clients: - 127.0.0.0/8 - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 disallowed_clients: [] blocked_hosts: - version.bind - id.server - hostname.bind trusted_proxies: - 127.0.0.0/8 - ::1/128 cache_size: 41943040 cache_ttl_min: 0 cache_ttl_max: 0 cache_optimistic: true bogus_nxdomain: [] aaaa_disabled: true enable_dnssec: true edns_client_subnet: custom_ip: "" enabled: false use_custom: false max_goroutines: 300 handle_ddr: true ipset: [] ipset_file: "" bootstrap_prefer_ipv6: false upstream_timeout: 3s private_networks: [] use_private_ptr_resolvers: true local_ptr_upstreams: [] use_dns64: false dns64_prefixes: [] serve_http3: false use_http3_upstreams: false serve_plain_dns: true hostsfile_enabled: true tls: enabled: true server_name: adguard force_https: true port_https: 443 port_dns_over_tls: 853 port_dns_over_quic: 853 port_dnscrypt: 0 dnscrypt_config_file: "" allow_unencrypted_doh: false certificate_chain: "" private_key: "" certificate_path: /opt/adguardhome/conf/cert.pem private_key_path: /opt/adguardhome/conf/key.pem strict_sni_check: false querylog: dir_path: "" ignored: - $HOMESERVER_DOMAIN - '*.$HOMESERVER_DOMAIN' interval: 720h size_memory: 1000 enabled: true file_enabled: true statistics: dir_path: "" ignored: - $HOMESERVER_DOMAIN - '*.$HOMESERVER_DOMAIN' interval: 720h enabled: true filters: - enabled: true url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt name: AdGuard DNS filter id: 1 - enabled: true url: https://adaway.org/hosts.txt name: AdAway Default Blocklist id: 2 - enabled: false url: https://someonewhocares.org/hosts/zero/hosts name: Dan Pollock's List id: 1657273139 - enabled: false url: https://raw.githubusercontent.com/Perflyst/PiHoleBlocklist/master/SmartTV-AGH.txt name: Perflyst and Dandelion Sprout's Smart-TV Blocklist id: 1657273140 - enabled: false url: https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt name: WindowsSpyBlocker - Hosts spy rules id: 1657273141 - enabled: false url: https://raw.githubusercontent.com/DandelionSprout/adfilt/master/Alternate%20versions%20Anti-Malware%20List/AntiMalwareAdGuardHome.txt name: Dandelion Sprout's Anti-Malware List id: 1657273142 - enabled: false url: https://raw.githubusercontent.com/hoshsadiq/adblock-nocoin-list/master/hosts.txt name: NoCoin Filter List id: 1657273143 - enabled: false url: https://raw.githubusercontent.com/durablenapkin/scamblocklist/master/adguard.txt name: Scam Blocklist by DurableNapkin id: 1657273144 - enabled: false url: https://raw.githubusercontent.com/mitchellkrogza/The-Big-List-of-Hacked-Malware-Web-Sites/master/hosts name: The Big List of Hacked Malware Web Sites id: 1657273145 - enabled: false url: https://malware-filter.gitlab.io/malware-filter/urlhaus-filter-agh-online.txt name: Online Malicious URL Blocklist id: 1657273146 - enabled: true url: https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adblockplus&showintro=1&mimetype=plaintext name: Peter Lowe's List id: 1657273147 whitelist_filters: [] user_rules: - '@@||api.ipify.org^' - '@@||$HOMESERVER_DOMAIN^' dhcp: enabled: false interface_name: "" local_domain_name: lan dhcpv4: gateway_ip: "" subnet_mask: "" range_start: "" range_end: "" lease_duration: 86400 icmp_timeout_msec: 1000 options: [] dhcpv6: range_start: "" lease_duration: 86400 ra_slaac_only: false ra_allow_slaac: false filtering: blocking_ipv4: "" blocking_ipv6: "" blocked_services: schedule: time_zone: Local ids: [] protection_disabled_until: null safe_search: enabled: false bing: true duckduckgo: true ecosia: true google: true pixabay: true yandex: true youtube: true blocking_mode: default parental_block_host: family-block.dns.adguard.com safebrowsing_block_host: standard-block.dns.adguard.com rewrites: - domain: '$HOMESERVER_DOMAIN' answer: $HOMESERVER_HOST_PRIMARY_INTERFACE_IP - domain: '*.$HOMESERVER_DOMAIN' answer: $HOMESERVER_HOST_PRIMARY_INTERFACE_IP - domain: '*.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN' answer: $RELAYSERVER_SERVER_IP safe_fs_patterns: - /opt/adguardhome/work/data/userfilters/* safebrowsing_cache_size: 10485760 safesearch_cache_size: 10485760 parental_cache_size: 10485760 cache_time: 30 filters_update_interval: 24 blocked_response_ttl: 10 filtering_enabled: true parental_enabled: false safebrowsing_enabled: false protection_enabled: true clients: runtime_sources: whois: true arp: true rdns: true dhcp: true hosts: true persistent: [] log: enabled: true file: "" max_backups: 0 max_size: 100 max_age: 3 compress: false local_time: false verbose: false os: group: "" user: "" rlimit_nofile: 0 schema_version: 29 EOFAD } function modFunAdguardFixMSS() { set +e grep "GODEBUG=tlskyber" $HOME/adguard.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "GODEBUG=tlskyber=0" >> $HOME/adguard.env else return 2 fi } function performUpdateAdGuard() { perform_stack_name=adguard prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v8 curImageList=adguard/adguardhome:v0.107.41 image_update_map[0]="adguard/adguardhome:v0.107.41,mirror.gcr.io/adguard/adguardhome:v0.107.69" ;; 2) newVer=v8 curImageList=adguard/adguardhome:v0.107.43 image_update_map[0]="adguard/adguardhome:v0.107.43,mirror.gcr.io/adguard/adguardhome:v0.107.69" ;; 3) newVer=v8 curImageList=adguard/adguardhome:v0.107.45 image_update_map[0]="adguard/adguardhome:v0.107.45,mirror.gcr.io/adguard/adguardhome:v0.107.69" ;; 4) newVer=v8 curImageList=adguard/adguardhome:v0.107.52 image_update_map[0]="adguard/adguardhome:v0.107.52,mirror.gcr.io/adguard/adguardhome:v0.107.69" ;; 5) newVer=v8 curImageList=adguard/adguardhome:v0.107.53 image_update_map[0]="adguard/adguardhome:v0.107.53,mirror.gcr.io/adguard/adguardhome:v0.107.69" ;; 6) newVer=v8 curImageList=adguard/adguardhome:v0.107.59 image_update_map[0]="adguard/adguardhome:v0.107.59,mirror.gcr.io/adguard/adguardhome:v0.107.69" ;; 7) newVer=v8 curImageList=mirror.gcr.io/adguard/adguardhome:v0.107.64 image_update_map[0]="mirror.gcr.io/adguard/adguardhome:v0.107.64,mirror.gcr.io/adguard/adguardhome:v0.107.69" ;; 8) newVer=v9 curImageList=mirror.gcr.io/adguard/adguardhome:v0.107.69 image_update_map[0]="mirror.gcr.io/adguard/adguardhome:v0.107.69,mirror.gcr.io/adguard/adguardhome:v0.107.78" ;; 9) newVer=v9 curImageList=mirror.gcr.io/adguard/adguardhome:v0.107.78 image_update_map[0]="mirror.gcr.io/adguard/adguardhome:v0.107.78,mirror.gcr.io/adguard/adguardhome:v0.107.78" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function prepAdguardInstallation() { pai_curE=${-//[^e]/} set +e sudo systemctl stop systemd-resolved > /dev/null 2>&1 sudo systemctl disable systemd-resolved > /dev/null 2>&1 sudo systemctl mask systemd-resolved > /dev/null 2>&1 if [ -f /etc/dhcp/dhclient.conf ]; then sudo sed -i '/supersede domain-name-servers/d' /etc/dhcp/dhclient.conf sudo sed -i -e '$a\' /etc/dhcp/dhclient.conf echo "supersede domain-name-servers 127.0.0.1;" | sudo tee -a /etc/dhcp/dhclient.conf > /dev/null 2>&1 fi sudo rm -f /etc/resolv.conf > /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null < /dev/null 2>&1 sudo tee /etc/resolv.conf >/dev/null <&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 5 seconds, total wait=$i seconds..." sleep 5 i=$((i+5)) done if ! [ "$isFound" = "true" ]; then echo "System Utils did not start up correctly..." cd ~ docker compose -f $HOME/sysutils-compose-tmp.yml down -v return 1 fi search="service=tcp-listener transport=https" isFound=false i=0 while [ $i -le 60 ] do findtext=$(docker logs influxdb 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 1 second, total wait=$i seconds..." sleep 1 i=$((i+1)) done if ! [ "$isFound" = "true" ]; then echo "System Utils did not start up correctly..." cd ~ docker compose -f $HOME/sysutils-compose-tmp.yml down -v return 1 fi sleep 5 import_gf_ds() { num_tries=1 total_tries=5 isSuccess=false while [ "$isSuccess" = "false" ] && [ $num_tries -lt $total_tries ] do echo "$1" | http POST http://$GRAFANA_ADMIN_USERNAME:$GRAFANA_ADMIN_PASSWORD@127.0.0.1:6565/api/datasources > /dev/null 2>&1 if [ $? -eq 0 ]; then isSuccess=true break fi echo "ERROR: Grafana datasource import failed, retrying in 5 seconds..." sleep 5 ((num_tries++)) done } prometheus_json=$(jq -n --arg gfid "$gf_dataset_uid" '{name: "Prometheus", uid: $gfid, type: "prometheus", url: "http://prometheus:9090", access: "proxy", basicAuth: false, isDefault: true}') import_gf_ds "$prometheus_json" loki_json=$(jq -n --arg gfid "$gf_loki_uid" '{name: "Loki", uid: $gfid, type: "loki", url: "http://loki:3100", access: "proxy", basicAuth: false, isDefault: false, jsonData: {maxLines: 1000}}') import_gf_ds "$loki_json" if [ "$isSuccess" = "false" ]; then echo "ERROR: Could not import datasource into Grafana." else sleep 3 isSuccess=false num_tries=1 while [ "$isSuccess" = "false" ] && [ $num_tries -lt $total_tries ] do cat $HOME/gfdashboard.json | http POST http://$GRAFANA_ADMIN_USERNAME:$GRAFANA_ADMIN_PASSWORD@127.0.0.1:6565/api/dashboards/db > /dev/null 2>&1 if [ $? -eq 0 ]; then isSuccess=true break fi echo "ERROR: Grafana dashboard import failed, retrying in 5 seconds..." sleep 5 ((num_tries++)) done if [ "$isSuccess" = "false" ]; then echo "ERROR: Could not import dashboard into Grafana." fi fi set -e pref_string=$(jq -n --arg gfid "$gf_dashboard_uid" --arg tz "$TZ" '{theme: "dark", homeDashboardUID: $gfid, timezone: $tz}') echo $pref_string | http PATCH http://$GRAFANA_ADMIN_USERNAME:$GRAFANA_ADMIN_PASSWORD@127.0.0.1:6565/api/org/preferences > /dev/null 2>&1 sleep 2 cd ~ docker compose -f $HOME/sysutils-compose-tmp.yml down -v sleep 2 installStack sysutils grafana "HTTP Server Listen" $HOME/sysutils.env retval=$? if [ $retval -ne 0 ]; then return $retval fi rm -f $HOME/sysutils-compose-tmp.yml rm -f $HOME/gfdashboard.json inner_block="" inner_block=$inner_block">>https://$SUB_GRAFANA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://grafana:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_GRAFANA $MANAGETLS_GRAFANA "$is_integrate_hshq" $NETDEFAULT_GRAFANA "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_PROMETHEUS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://prometheus:9090 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PROMETHEUS $MANAGETLS_PROMETHEUS "$is_integrate_hshq" $NETDEFAULT_PROMETHEUS "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_INFLUXDB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://influxdb:8086 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_INFLUXDB $MANAGETLS_INFLUXDB "$is_integrate_hshq" $NETDEFAULT_INFLUXDB "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_ALLOY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>forward_auth https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>uri /api/verify?rd=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN\n" inner_block=$inner_block">>>>>>>>copy_headers Remote-User Remote-Groups Remote-Name Remote-Email\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://alloy-web:80 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ALLOY $MANAGETLS_ALLOY "$is_integrate_hshq" $NETDEFAULT_ALLOY "$inner_block" insertSubAuthelia $SUB_ALLOY.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll sysutils "$FMLNAME_GRAFANA" $USERTYPE_GRAFANA "https://$SUB_GRAFANA.$HOMESERVER_DOMAIN" "grafana.png" "$(getHeimdallOrderFromSub $SUB_GRAFANA $USERTYPE_GRAFANA)" insertEnableSvcAll sysutils "$FMLNAME_PROMETHEUS" $USERTYPE_PROMETHEUS "https://$SUB_PROMETHEUS.$HOMESERVER_DOMAIN" "prometheus.png" "$(getHeimdallOrderFromSub $SUB_PROMETHEUS $USERTYPE_PROMETHEUS)" insertEnableSvcAll sysutils "$FMLNAME_INFLUXDB" $USERTYPE_INFLUXDB "https://$SUB_INFLUXDB.$HOMESERVER_DOMAIN" "influxdb.png" "$(getHeimdallOrderFromSub $SUB_INFLUXDB $USERTYPE_INFLUXDB)" insertEnableSvcAll sysutils "$FMLNAME_ALLOY" $USERTYPE_ALLOY "https://$SUB_ALLOY.$HOMESERVER_DOMAIN" "alloy.png" "$(getHeimdallOrderFromSub $SUB_ALLOY $USERTYPE_ALLOY)" restartAllCaddyContainers fi } function outputConfigSysUtils() { cat < $HOME/sysutils-compose-tmp.yml services: grafana: image: $(getScriptImageByContainerName grafana) container_name: grafana hostname: grafana user: "$UID" restart: unless-stopped security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-ext-net ports: - "127.0.0.1:6565:3000" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-sysutils-grafana:/var/lib/grafana environment: - GF_USERS_ALLOW_SIGN_UP=false - GF_SECURITY_ADMIN_USER=$GRAFANA_ADMIN_USERNAME - GF_SECURITY_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD - GF_INSTALL_PLUGINS=grafana-piechart-panel,grafana-clock-panel,grafana-simple-json-datasource prometheus: image: $(getScriptImageByContainerName prometheus) container_name: prometheus hostname: prometheus restart: unless-stopped security_opt: - no-new-privileges:true extra_hosts: - host.docker.internal:host-gateway command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' - '--web.enable-lifecycle' networks: - int-sysutils-net - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - ${HSHQ_STACKS_DIR}/sysutils/prometheus:/etc/prometheus - v-sysutils-prometheus:/prometheus node-exporter: image: $(getScriptImageByContainerName node-exporter) container_name: node-exporter hostname: node-exporter restart: unless-stopped security_opt: - no-new-privileges:true user: "0:0" command: - '--path.procfs=/host/proc' - '--path.rootfs=/rootfs' - '--path.sysfs=/host/sys' - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc|var/lib/docker/(containers|devicemapper|volumes)/.+)(\$\$|/)' networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro influxdb: image: $(getScriptImageByContainerName influxdb) container_name: influxdb hostname: influxdb user: "$USERID" restart: unless-stopped command: - '--reporting-disabled' security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${HSHQ_SSL_DIR}/influxdb.crt:/certs/influxdb.crt - ${HSHQ_SSL_DIR}/influxdb.key:/certs/influxdb.key - ${HSHQ_STACKS_DIR}/sysutils/influxdb/etc:/etc/influxdb2 - ${HSHQ_STACKS_DIR}/sysutils/influxdb/var:/var/lib/influxdb2 environment: - DOCKER_INFLUXDB_INIT_MODE=setup - DOCKER_INFLUXDB_INIT_USERNAME=$INFLUXDB_ADMIN_USERNAME - DOCKER_INFLUXDB_INIT_PASSWORD=$INFLUXDB_ADMIN_PASSWORD - DOCKER_INFLUXDB_INIT_ORG=$INFLUXDB_ORG - DOCKER_INFLUXDB_INIT_BUCKET=$INFLUXDB_HA_BUCKET - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=$INFLUXDB_TOKEN - INFLUXD_TLS_CERT=/certs/influxdb.crt - INFLUXD_TLS_KEY=/certs/influxdb.key loki: image: $(getScriptImageByContainerName loki) container_name: loki hostname: loki restart: unless-stopped security_opt: - no-new-privileges:true command: -config.file=/etc/loki/config.yml networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${HSHQ_STACKS_DIR}/sysutils/loki/config.yml:/etc/loki/config.yml:ro - v-sysutils-loki:/loki environment: - TZ=$TZ - UID=$USERID - GID=$GROUPID - ALLOY_ADMIN_USERNAME=$ALLOY_ADMIN_USERNAME - ALLOY_ADMIN_PASSWORD=$ALLOY_ADMIN_PASSWORD alloy-app: image: $(getScriptImageByContainerName alloy-app) container_name: alloy-app hostname: alloy-app restart: unless-stopped security_opt: - no-new-privileges:true command: - run - --stability.level=experimental - --server.http.listen-addr=0.0.0.0:12345 - /etc/alloy/config.alloy networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /var/run/docker.sock:/var/run/docker.sock:ro - /var/log:/var/log:ro - ${HSHQ_STACKS_DIR}/sysutils/alloy/devices:/etc/alloy/devices:ro - ${HSHQ_STACKS_DIR}/sysutils/alloy/config.alloy:/etc/alloy/config.alloy:ro environment: - TZ=$TZ - UID=$USERID - GID=$GROUPID - ALLOY_ADMIN_USERNAME=$ALLOY_ADMIN_USERNAME - ALLOY_ADMIN_PASSWORD=$ALLOY_ADMIN_PASSWORD alloy-web: image: $(getScriptImageByContainerName alloy-web) container_name: alloy-web hostname: alloy-web restart: unless-stopped security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${HSHQ_STACKS_DIR}/sysutils/alloy/Caddyfile:/etc/caddy/Caddyfile environment: - TZ=$TZ - UID=$USERID - GID=$GROUPID - ALLOY_ADMIN_USERNAME=$ALLOY_ADMIN_USERNAME - ALLOY_ADMIN_PASSWORD=$ALLOY_ADMIN_PASSWORD volumes: v-sysutils-grafana: driver: local driver_opts: type: none o: bind device: ${HSHQ_STACKS_DIR}/sysutils/grafana v-sysutils-prometheus: driver: local driver_opts: type: none o: bind device: ${HSHQ_NONBACKUP_DIR}/sysutils/prometheus v-sysutils-loki: driver: local driver_opts: type: none o: bind device: ${HSHQ_NONBACKUP_DIR}/sysutils/loki networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-privateip-net: name: dock-privateip external: true int-sysutils-net: driver: bridge internal: true ipam: driver: default EOFGF cat < $HOME/sysutils-compose.yml $STACK_VERSION_PREFIX sysutils $(getScriptStackVersion sysutils) services: grafana: image: $(getScriptImageByContainerName grafana) container_name: grafana hostname: grafana user: "\${PORTAINER_UID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-sysutils-grafana:/var/lib/grafana prometheus: image: $(getScriptImageByContainerName prometheus) container_name: prometheus hostname: prometheus restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true extra_hosts: - host.docker.internal:host-gateway command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' - '--web.enable-lifecycle' networks: - int-sysutils-net - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/prometheus:/etc/prometheus - v-sysutils-prometheus:/prometheus node-exporter: image: $(getScriptImageByContainerName node-exporter) container_name: node-exporter hostname: node-exporter restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true user: "0:0" command: - '--path.procfs=/host/proc' - '--path.rootfs=/rootfs' - '--path.sysfs=/host/sys' - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc|var/lib/docker/(containers|devicemapper|volumes)/.+)(\$\$|/)' networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro influxdb: image: $(getScriptImageByContainerName influxdb) container_name: influxdb hostname: influxdb user: "\${PORTAINER_UID}" restart: unless-stopped env_file: stack.env command: - '--reporting-disabled' security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/influxdb.crt:/certs/influxdb.crt - \${PORTAINER_HSHQ_SSL_DIR}/influxdb.key:/certs/influxdb.key - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/influxdb/etc:/etc/influxdb2 - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/influxdb/var:/var/lib/influxdb2 loki: image: $(getScriptImageByContainerName loki) container_name: loki hostname: loki restart: unless-stopped security_opt: - no-new-privileges:true command: -config.file=/etc/loki/config.yml networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/loki/config.yml:/etc/loki/config.yml:ro - v-sysutils-loki:/loki alloy-app: image: $(getScriptImageByContainerName alloy-app) container_name: alloy-app hostname: alloy-app restart: unless-stopped security_opt: - no-new-privileges:true command: - run - --stability.level=experimental - --server.http.listen-addr=0.0.0.0:12345 - /etc/alloy/config.alloy networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /var/run/docker.sock:/var/run/docker.sock:ro - /var/log:/var/log:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/alloy/devices:/etc/alloy/devices:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/alloy/config.alloy:/etc/alloy/config.alloy:ro alloy-web: image: $(getScriptImageByContainerName alloy-web) container_name: alloy-web hostname: alloy-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/alloy/Caddyfile:/etc/caddy/Caddyfile volumes: v-sysutils-grafana: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/grafana v-sysutils-prometheus: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/sysutils/prometheus v-sysutils-loki: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/sysutils/loki networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true int-sysutils-net: driver: bridge internal: true ipam: driver: default EOFGF cat < $HOME/sysutils.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID INFLUXD_TLS_CERT=/certs/influxdb.crt INFLUXD_TLS_KEY=/certs/influxdb.key ALLOY_ADMIN_USERNAME=$ALLOY_ADMIN_USERNAME ALLOY_ADMIN_PASSWORD=$ALLOY_ADMIN_PASSWORD EOFGF cat < $HOME/gfdashboard.json { "dashboard": { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "description": "CPU/Memory/Disk IO/Network/Temperature and other monitoring indicators.", "editable": true, "fiscalYearStartMonth": 0, "gnetId": 9893, "graphTooltip": 0, "id": null, "iteration": 1645129641574, "links": [], "liveNow": false, "panels": [ { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 5, "w": 2, "x": 0, "y": 1 }, "hideTimeOverride": true, "id": 15, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "time() - node_boot_time_seconds{instance=~\"\$node\"}", "format": "time_series", "hide": false, "instant": true, "intervalFactor": 2, "refId": "A", "step": 40 } ], "title": "Uptime", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 2, "y": 1 }, "id": 14, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "count(count(node_cpu_seconds_total{instance=~\"\$node\", mode='system'}) by (cpu))", "format": "time_series", "instant": true, "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 20 } ], "title": "CPU Count", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 2, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 50 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 80 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 5, "w": 3, "x": 4, "y": 1 }, "id": 167, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "8.3.4", "targets": [ { "expr": "100 - (avg(irate(node_cpu_seconds_total{instance=~\"\$node\",mode=\"idle\"}[5m])) * 100)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 20 } ], "title": "CPU Usage(5m)", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 2, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 20 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 50 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 5, "w": 3, "x": 7, "y": 1 }, "id": 20, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "8.3.4", "targets": [ { "expr": "avg(irate(node_cpu_seconds_total{instance=~\"\$node\",mode=\"iowait\"}[5m])) * 100", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 20 } ], "title": "CPU IO Wait(5m)", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 0, "mappings": [], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 80 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 90 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 5, "w": 3, "x": 10, "y": 1 }, "hideTimeOverride": false, "id": 172, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "8.3.4", "targets": [ { "expr": "(1 - (node_memory_MemAvailable_bytes{instance=~\"\$node\"} / (node_memory_MemTotal_bytes{instance=~\"\$node\"})))* 100", "format": "time_series", "hide": false, "interval": "10s", "intervalFactor": 1, "refId": "A", "step": 20 } ], "title": "Memory Usage", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 2, "mappings": [], "min": 0, "max": 100000, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 60000 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 80000 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 5, "w": 3, "x": 13, "y": 1 }, "hideTimeOverride": false, "id": 16, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "8.3.4", "targets": [ { "expr": "node_filefd_allocated{instance=~\"\$node\"}", "format": "time_series", "instant": false, "interval": "10s", "intervalFactor": 1, "refId": "B" } ], "title": "Open File Descriptors", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 70 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 90 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 5, "w": 4, "x": 16, "y": 1 }, "id": 166, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "8.3.4", "repeatDirection": "h", "targets": [ { "expr": "100 - ((node_filesystem_avail_bytes{instance=~\"\$node\",mountpoint=\"/\",fstype=~\"ext4|xfs\"} * 100) / node_filesystem_size_bytes {instance=~\"\$node\",mountpoint=\"/\",fstype=~\"ext4|xfs\"})", "format": "time_series", "interval": "10s", "intervalFactor": 1, "refId": "A", "step": 20 } ], "title": "Root Partition Usage", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "Get the largest partition by the variable maxmount. ", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 70 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 90 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 5, "w": 4, "x": 20, "y": 1 }, "id": 154, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "8.3.4", "repeatDirection": "h", "targets": [ { "expr": "100 - ((node_filesystem_avail_bytes{instance=~\"\$node\",mountpoint=\"\$maxmount\",fstype=~\"ext4|xfs\"} * 100) / node_filesystem_size_bytes {instance=~\"\$node\",mountpoint=\"\$maxmount\",fstype=~\"ext4|xfs\"})", "format": "time_series", "interval": "10s", "intervalFactor": 1, "refId": "A", "step": 20 } ], "title": "Maximum Partition (\$maxmount) Usage", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 3, "w": 2, "x": 2, "y": 3 }, "id": 75, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "8.3.4", "targets": [ { "expr": "node_memory_MemTotal_bytes{instance=~\"\$node\"}", "format": "time_series", "instant": true, "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A", "step": 20 } ], "title": "Total RAM", "type": "stat" }, { "aliasColors": { "1 minute": "#BF1B00", "15 minutes": "#6ED0E0", "5 minutes": "#CCA300" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "editable": true, "error": false, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 6, "w": 11, "x": 0, "y": 6 }, "height": "300", "hiddenSeries": false, "id": 13, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "maxPerRow": 6, "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_load1{instance=~\"\$node\"}", "format": "time_series", "instant": false, "interval": "10s", "intervalFactor": 2, "legendFormat": "1m", "metric": "", "refId": "A", "step": 20, "target": "" }, { "expr": "node_load5{instance=~\"\$node\"}", "format": "time_series", "instant": false, "interval": "10s", "intervalFactor": 2, "legendFormat": "5m", "refId": "B", "step": 20 }, { "expr": "node_load15{instance=~\"\$node\"}", "format": "time_series", "instant": false, "interval": "10s", "intervalFactor": 2, "legendFormat": "15m", "refId": "C", "step": 20 } ], "thresholds": [], "timeRegions": [], "title": "Average System Load", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "aliasColors": { "/": "#eab839", "/boot": "#bf1b00", "/data": "#1f78c1" }, "breakPoint": "100%", "combine": { "label": "Others", "threshold": "" }, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "decimals": 1, "fontSize": "50%", "format": "bytes", "gridPos": { "h": 6, "w": 5, "x": 11, "y": 6 }, "hideTimeOverride": false, "id": 171, "legend": { "header": "", "percentage": false, "percentageDecimals": 0, "show": true, "sideWidth": 142, "values": true }, "legendType": "Right side", "links": [], "maxDataPoints": 3, "nullPointMode": "connected", "pieType": "pie", "strokeWidth": "2", "targets": [ { "expr": "node_filesystem_size_bytes {instance=~\"\$node\",fstype=~\"ext4|xfs\"}", "format": "time_series", "instant": true, "interval": "10s", "intervalFactor": 1, "legendFormat": "{{mountpoint}}", "refId": "A" } ], "title": "Total Disk Space", "type": "grafana-piechart-panel", "valueName": "current" }, { "columns": [], "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "fontSize": "120%", "gridPos": { "h": 6, "w": 8, "x": 16, "y": 6 }, "id": 164, "links": [], "scroll": true, "showHeader": true, "sort": { "col": 11, "desc": true }, "styles": [ { "alias": "Time", "align": "auto", "dateFormat": "YYYY-MM-DD HH:mm:ss", "pattern": "Time", "type": "hidden" }, { "alias": "Partition", "align": "auto", "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "mountpoint", "thresholds": [ "" ], "type": "string", "unit": "bytes" }, { "alias": "Available space", "align": "auto", "colorMode": "value", "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "Value #A", "thresholds": [ "10000000000", "20000000000" ], "type": "number", "unit": "bytes" }, { "alias": "Usage rate", "align": "auto", "colorMode": "cell", "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "Value #B", "thresholds": [ "70", "90" ], "type": "number", "unit": "percentunit" }, { "alias": "Total space", "align": "auto", "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 1, "link": false, "mappingType": 1, "pattern": "Value #C", "thresholds": [], "type": "number", "unit": "bytes" }, { "alias": "File system", "align": "auto", "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "link": false, "mappingType": 1, "pattern": "fstype", "thresholds": [], "type": "number", "unit": "short" }, { "alias": "", "align": "auto", "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "decimals": 2, "pattern": "/.*/", "preserveFormat": true, "sanitize": false, "thresholds": [], "type": "hidden", "unit": "short" } ], "targets": [ { "expr": "node_filesystem_size_bytes{instance=~'\$node',fstype=~\"ext4|xfs\"}", "format": "table", "hide": true, "instant": true, "intervalFactor": 1, "legendFormat": "", "refId": "C" }, { "expr": "node_filesystem_avail_bytes {instance=~'\$node',fstype=~\"ext4|xfs\"}", "format": "table", "hide": false, "instant": true, "interval": "10s", "intervalFactor": 1, "legendFormat": "", "refId": "A" }, { "expr": "1-(node_filesystem_free_bytes{instance=~'\$node',fstype=~\"ext4|xfs\"} / node_filesystem_size_bytes{instance=~'\$node',fstype=~\"ext4|xfs\"})", "format": "table", "hide": false, "instant": true, "intervalFactor": 1, "legendFormat": "", "refId": "B" }, { "expr": "", "format": "table", "interval": "10s", "intervalFactor": 1, "legendFormat": "", "refId": "D" } ], "title": "Free Space by Partition", "transform": "table", "type": "table-old" }, { "aliasColors": { "Disk consumption in I/O operations": "#ba43a9", "Idle - Waiting for something to happen": "#052B51", "guest": "#9AC48A", "idle": "#052B51", "iowait": "#EAB839", "irq": "#BF1B00", "nice": "#C15C17", "sdb_% I/O operations per second": "#d683ce", "softirq": "#E24D42", "steal": "#FCE2DE", "system": "#508642", "user": "#5195CE" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "decimals": 2, "description": "node_disk_io_time_seconds_total:\nThe number of milliseconds the disk spends on input/output operations. This value is the accumulated value.(Milliseconds Spent Doing I/Os)\n\nirate(node_disk_io_time_seconds_total[1m]):\nCalculate the rate per second: (last value - last previous value) / timestamp difference, that is, the percentage of time that the disk spends on I/O operations in 1 second.", "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 16, "x": 0, "y": 12 }, "hiddenSeries": false, "id": 7, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "maxPerRow": 6, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "avg(irate(node_cpu_seconds_total{instance=~\"\$node\",mode=\"system\"}[1m]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "System", "refId": "A", "step": 20 }, { "expr": "avg(irate(node_cpu_seconds_total{instance=~\"\$node\",mode=\"user\"}[1m]))", "format": "time_series", "intervalFactor": 2, "legendFormat": "User", "refId": "B", "step": 240 }, { "expr": "avg(irate(node_cpu_seconds_total{instance=~\"\$node\",mode=\"idle\"}[1m]))", "format": "time_series", "hide": true, "intervalFactor": 2, "legendFormat": "Idle", "refId": "F", "step": 240 }, { "expr": "avg(irate(node_cpu_seconds_total{instance=~\"\$node\",mode=\"iowait\"}[1m]))", "format": "time_series", "intervalFactor": 2, "legendFormat": "Iowait", "refId": "D", "step": 240 }, { "expr": "irate(node_disk_io_time_seconds_total{instance=~\"\$node\"}[1m])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}}_% of I/O operations per second", "refId": "C" } ], "thresholds": [], "timeRegions": [], "title": "CPU Usage, Disk I/O Operations Per Second (%)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "percentunit", "label": "", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": false } ], "yaxis": { "align": false } }, { "aliasColors": { "Available": "#9ac48a", "Memory_Avaliable": "#6ED0E0", "Memory_Cached": "#EF843C", "Memory_Free": "#629E51", "Memory_Total": "#6d1f62", "Memory_Used": "#eab839", "Total memory": "#bf1b00" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "decimals": 2, "fill": 6, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 12 }, "height": "300", "hiddenSeries": false, "id": 156, "legend": { "alignAsTable": false, "avg": false, "current": true, "max": false, "min": false, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 3, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_memory_MemTotal_bytes{instance=~\"\$node\"}", "format": "time_series", "hide": false, "instant": false, "intervalFactor": 2, "legendFormat": "Total memory", "refId": "A", "step": 4 }, { "expr": "node_memory_MemTotal_bytes{instance=~\"\$node\"} - node_memory_MemAvailable_bytes{instance=~\"\$node\"}", "format": "time_series", "hide": false, "intervalFactor": 2, "legendFormat": "used", "refId": "B", "step": 4 }, { "expr": "node_memory_MemFree_bytes{instance=~\"\$node\"}", "format": "time_series", "hide": true, "intervalFactor": 2, "legendFormat": "Memory_Free", "refId": "C", "step": 4 }, { "expr": "node_memory_Buffers_bytes{instance=~\"\$node\"}", "format": "time_series", "hide": true, "intervalFactor": 2, "legendFormat": "Memory_Buffers", "refId": "D", "step": 4 }, { "expr": "node_memory_Cached_bytes{instance=~\"\$node\"}", "format": "time_series", "hide": true, "intervalFactor": 2, "legendFormat": "Memory_Cached", "refId": "E", "step": 4 }, { "expr": "node_memory_MemAvailable_bytes{instance=~\"\$node\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "Available", "refId": "F", "step": 4 }, { "expr": "node_memory_MemTotal_bytes{instance=~\"\$node\"} - (node_memory_Cached_bytes{instance=~\"\$node\"} + node_memory_Buffers_bytes{instance=~\"\$node\"} + node_memory_MemFree_bytes{instance=~\"\$node\"})", "format": "time_series", "hide": true, "intervalFactor": 1, "refId": "G" } ], "thresholds": [], "timeRegions": [], "title": "Memory information", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "aliasColors": { "vda_write": "#6ED0E0" }, "bars": true, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "Reads completed: Reads per second per disk partition\n\nWrites completed: Number of writes per second per disk partition\n\nIO now Number of input/output requests being processed per disk partition per second", "fill": 2, "fillGradient": 0, "gridPos": { "h": 8, "w": 8, "x": 0, "y": 19 }, "height": "300", "hiddenSeries": false, "id": 161, "legend": { "alignAsTable": false, "avg": false, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": false, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/.*_read \$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(node_disk_reads_completed_total{instance=~\"\$node\"}[1m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "{{device}}_read", "refId": "A", "step": 10 }, { "expr": "irate(node_disk_writes_completed_total{instance=~\"\$node\"}[1m])", "format": "time_series", "hide": false, "intervalFactor": 2, "legendFormat": "{{device}}_write", "refId": "B", "step": 10 }, { "expr": "node_disk_io_now{instance=~\"\$node\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}", "refId": "C" } ], "thresholds": [], "timeRegions": [], "title": "Disk Read and Write Rate (IOPS)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "iops", "label": "Read (-) / write(+)I/O ops/sec", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "aliasColors": { "vda_write": "#6ED0E0" }, "bars": true, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "Read bytes The number of bits read per second per disk partition\nWritten bytes The number of bits written per second per disk partition", "fill": 2, "fillGradient": 0, "gridPos": { "h": 8, "w": 8, "x": 8, "y": 19 }, "height": "300", "hiddenSeries": false, "id": 168, "legend": { "alignAsTable": false, "avg": false, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": false, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/.*_read \$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(node_disk_read_bytes_total{instance=~\"\$node\"}[1m])", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{device}}_read", "refId": "A", "step": 10 }, { "expr": "irate(node_disk_written_bytes_total{instance=~\"\$node\"}[1m])", "format": "time_series", "hide": false, "intervalFactor": 2, "legendFormat": "{{device}}_write", "refId": "B", "step": 10 } ], "thresholds": [], "timeRegions": [], "title": "Disk Read and Write Capacity", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": "Read (-) / write (+)", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": false } ], "yaxis": { "align": false } }, { "aliasColors": { "vda": "#6ED0E0" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "Read time ms Number of seconds per disk partition read operation\n\nWrite time ms Number of seconds spent per disk partition write operation\n\nIO time ms Number of seconds spent per disk partition input/output operation\n\n IO time weighted Weighted seconds spent per disk partition input/output operation", "fill": 3, "fillGradient": 0, "gridPos": { "h": 8, "w": 8, "x": 16, "y": 19 }, "height": "300", "hiddenSeries": false, "id": 160, "legend": { "alignAsTable": false, "avg": false, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/,*_read \$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(node_disk_io_time_seconds_total{instance=~\"\$node\"}[1m])", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 2, "legendFormat": "{{device}}", "refId": "A", "step": 10 }, { "expr": "irate(node_disk_io_time_weighted_seconds_total{instance=~\"\$node\"}[1m])", "format": "time_series", "hide": true, "intervalFactor": 1, "legendFormat": "{{device}}_weighted", "refId": "D" }, { "expr": "irate(node_disk_read_time_seconds_total{instance=~\"\$node\"}[1m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_read", "refId": "B" }, { "expr": "irate(node_disk_write_time_seconds_total{instance=~\"\$node\"}[1m])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{device}}_write", "refId": "C" } ], "thresholds": [], "timeRegions": [], "title": "Disk IO Read and Write Time", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "s", "label": "Read (-) / write (+) ", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": false } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 27 }, "height": "300", "id": 157, "legend": { "alignAsTable": false, "avg": false, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/.*_out upload \$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(node_network_receive_bytes_total{instance=~'\$node',device!~'tap.*'}[5m])*8", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{device}}_in download", "refId": "A", "step": 4 }, { "expr": "irate(node_network_transmit_bytes_total{instance=~'\$node',device!~'tap.*'}[5m])*8", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{device}}_out upload", "refId": "B", "step": 4 } ], "thresholds": [], "timeRegions": [], "title": "Network Traffic", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "bps", "label": "Upload (-) / Download(+)", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": false } ], "yaxis": { "align": false } }, { "aliasColors": { "TCP": "#6ED0E0" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "description": "CurrEstab - TCP connections with current state ESTABLISHED or CLOSE-WAIT\n\nActiveOpens - TCP average connections that have been converted from CLOSED state to SYN-SENT state (within 1 minute)\n\nPassiveOpens - Directly from LISTEN state The average number of TCP connections to SYN-RCVD state (within 1 minute)\n\nTCP_all oc - the number of TCP sockets allocated (established, applied to sk_buff)\n\nTCP_inuse - in use (listening The number of TCP sockets \n\nTCP_tw - the number of TCP connections waiting to be closed", "fill": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 27 }, "height": "300", "id": 158, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": true, "min": false, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pluginVersion": "8.3.4", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_netstat_Tcp_CurrEstab{instance=~'\$node'}", "format": "time_series", "hide": false, "interval": "10s", "intervalFactor": 1, "legendFormat": "ESTABLISHED", "refId": "A", "step": 20 }, { "expr": "node_sockstat_TCP_tw{instance=~'\$node'}", "format": "time_series", "intervalFactor": 1, "legendFormat": "TCP_tw", "refId": "D" }, { "expr": "irate(node_netstat_Tcp_ActiveOpens{instance=~'\$node'}[1m])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "ActiveOpens", "refId": "B" }, { "expr": "irate(node_netstat_Tcp_PassiveOpens{instance=~'\$node'}[1m])", "format": "time_series", "intervalFactor": 1, "legendFormat": "PassiveOpens", "refId": "C" }, { "expr": "node_sockstat_TCP_alloc{instance=~'\$node'}", "format": "time_series", "intervalFactor": 1, "legendFormat": "TCP_alloc", "refId": "E" }, { "expr": "node_sockstat_TCP_inuse{instance=~'\$node'}", "format": "time_series", "intervalFactor": 1, "legendFormat": "TCP_inuse", "refId": "F" } ], "thresholds": [], "timeRegions": [], "title": "TCP Connections", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "fill": 0, "gridPos": { "h": 10, "w": 24, "x": 0, "y": 35 }, "id": 169, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null as zero", "percentage": false, "pluginVersion": "8.3.4", "pointradius": 0.5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_hwmon_temp_celsius{instance=\"\$node\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{chip}} {{sensor}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Hardware Temperature", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "celsius", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } } ], "refresh": "5s", "schemaVersion": 34, "style": "dark", "tags": [ "StarsL", "Prometheus" ], "templating": { "list": [ { "auto": true, "auto_count": 30, "auto_min": "10s", "current": { "selected": false, "text": "auto", "value": "\$__auto_interval_interval" }, "hide": 0, "label": "interval", "name": "interval", "options": [ { "selected": true, "text": "auto", "value": "\$__auto_interval_interval" }, { "selected": false, "text": "1m", "value": "1m" }, { "selected": false, "text": "10m", "value": "10m" }, { "selected": false, "text": "30m", "value": "30m" }, { "selected": false, "text": "1h", "value": "1h" }, { "selected": false, "text": "6h", "value": "6h" }, { "selected": false, "text": "12h", "value": "12h" }, { "selected": false, "text": "1d", "value": "1d" }, { "selected": false, "text": "7d", "value": "7d" }, { "selected": false, "text": "14d", "value": "14d" }, { "selected": false, "text": "30d", "value": "30d" } ], "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", "refresh": 2, "skipUrlSync": false, "type": "interval" }, { "allFormat": "glob", "current": {}, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "definition": "", "hide": 0, "includeAll": false, "label": "surroundings", "multi": false, "multiFormat": "regex values", "name": "env", "options": [], "query": { "query": "label_values(node_exporter_build_info,env)", "refId": "Prometheus-env-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "current": {}, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "definition": "", "hide": 0, "includeAll": false, "label": "CPU name", "multi": false, "multiFormat": "regex values", "name": "name", "options": [], "query": { "query": "label_values(node_exporter_build_info{env='\$env'},name)", "refId": "Prometheus-name-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "current": {}, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "definition": "", "hide": 0, "includeAll": false, "label": "node", "multi": false, "multiFormat": "regex values", "name": "node", "options": [], "query": { "query": "label_values(node_exporter_build_info{name='\$name'},instance)", "refId": "Prometheus-node-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": {}, "datasource": { "type": "prometheus", "uid": "$gf_dataset_uid" }, "definition": "", "hide": 2, "includeAll": false, "label": "", "multi": false, "name": "maxmount", "options": [], "query": { "query": "query_result(topk(1,sort_desc (max(node_filesystem_size_bytes{instance=~'\$node',fstype=~\"ext4|xfs\"}) by (mountpoint))))", "refId": "Prometheus-maxmount-Variable-Query" }, "refresh": 1, "regex": "/.*\\\\\\"(.*)\\\\\\".*/", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": { "now": true, "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "browser", "title": "System Metrics", "uid": "$gf_dashboard_uid", "version": 1, "weekStart": "" } } EOFJS outputLokiAlloyConfig } function outputLokiAlloyConfig() { cat < $HSHQ_STACKS_DIR/sysutils/loki/config.yml auth_enabled: false server: http_listen_port: 3100 common: path_prefix: /loki storage: filesystem: chunks_directory: /loki/chunks rules_directory: /loki/rules replication_factor: 1 ring: kvstore: store: inmemory schema_config: configs: - from: 2024-01-01 store: tsdb object_store: filesystem schema: v13 index: prefix: index_ period: 24h limits_config: volume_enabled: true max_query_lookback: 168h retention_period: 744h discover_log_levels: true discover_service_name: - service_name - job - container - service - host pattern_ingester: enabled: true EOFPM cat < $HSHQ_STACKS_DIR/sysutils/alloy/config.alloy // ──────────────────────────────────────────────────────────────────── // Docker containers → syslog files in /var/log/docker // Docker socket is used for METADATA ONLY // Just add loki.ship: "true" label to any container to ingest logs // ──────────────────────────────────────────────────────────────────── discovery.docker "engine" { host = "unix:///var/run/docker.sock" } discovery.relabel "containers" { targets = discovery.docker.engine.targets rule { source_labels = ["__meta_docker_container_label_loki_ship"] action = "keep" regex = "true" } rule { source_labels = ["__meta_docker_container_name"] regex = "/(.*)" target_label = "__path__" replacement = "/var/log/docker/\$1.log" } rule { source_labels = ["__meta_docker_container_name"] regex = "/(.*)" target_label = "container" } rule { source_labels = ["__meta_docker_container_label_com_docker_compose_service"] target_label = "service" } rule { source_labels = ["__meta_docker_container_label_com_docker_compose_project"] target_label = "project" } rule { target_label = "log_source" replacement = "docker" } } loki.source.file "docker_logs" { targets = discovery.relabel.containers.output forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Caddy access logs → /var/log/caddy/caddy-*-access.log (JSON lines) // Set ENABLE_ACCESS_LOGGING=log_true in caddy stack env to ingest access logs // ──────────────────────────────────────────────────────────────────── loki.source.file "caddy" { targets = [ { __path__ = "/var/log/caddy/caddy-*-access.log" }, ] forward_to = [loki.relabel.caddy_instance.receiver] file_match { enabled = true sync_period = "10s" } } local.file_match "crowdsec_logs" { path_targets = [ { "__path__" = "/var/log/crowdsec/crowdsec.log", "job" = "crowdsec", "type" = "engine" }, { "__path__" = "/var/log/crowdsec/crowdsec_alerts.json", "job" = "crowdsec", "type" = "alerts" }, ] } loki.relabel "caddy_instance" { forward_to = [loki.process.caddy_parse.receiver] rule { source_labels = ["filename"] regex = ".*/(caddy-.+)-access\\\\.log" replacement = "\$1-access" target_label = "job" } rule { target_label = "log_source" replacement = "caddy_access" } } loki.source.file "crowdsec" { targets = local.file_match.crowdsec_logs.targets forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Device-name enrichment (Caddy access logs only) // Automatic - no action needed // ──────────────────────────────────────────────────────────────────── discovery.file "devices" { files = ["/etc/alloy/devices/devices.json"] refresh_interval = "30s" } loki.process "caddy_parse" { stage.json { expressions = { client_ip = "request.client_ip", } } stage.labels { values = { client_ip = "" } } forward_to = [loki.enrich.devices.receiver] } loki.enrich "devices" { targets = discovery.file.devices.targets target_match_label = "__address__" logs_match_label = "client_ip" labels_to_copy = ["device_name"] forward_to = [loki.process.caddy_default.receiver] } loki.process "caddy_default" { stage.match { selector = \`{device_name=""}\` stage.static_labels { values = { device_name = "UNKNOWN" } } } forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Host logs → /var/log/* (one file = one service) // Read via the full /var/log mount (subsumes /var/log/docker). // Either uncomment existing, or add other paths in /var/log to ingest // ──────────────────────────────────────────────────────────────────── loki.source.file "host" { targets = [ // { __path__ = "/var/log/syslog" }, // { __path__ = "/var/log/auth.log" }, // { __path__ = "/var/log/hshq.log" }, ] forward_to = [loki.relabel.host_instance.receiver] } loki.relabel "host_instance" { forward_to = [loki.write.local.receiver] rule { source_labels = ["filename"] regex = ".*/(.+)" replacement = "\$1" target_label = "job" } rule { target_label = "log_source" replacement = "host" } } // ───────────────────────────────────────────────────────────── // LiteLLM request/response records → /loki/api/v1/raw (NDJSON) // ───────────────────────────────────────────────────────────── // Uncomment the following block to ingest from LiteLLM /* loki.source.api "litellm" { http { listen_address = "0.0.0.0" listen_port = 3101 } forward_to = [loki.process.llm.receiver] } */ loki.process "llm" { stage.static_labels { values = { job = "litellm", log_source = "litellm", } } forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Output // ──────────────────────────────────────────────────────────────────── loki.write "local" { endpoint { url = "http://loki:3100/loki/api/v1/push" } } EOFPM outputDeviceIPMapping $HSHQ_STACKS_DIR/sysutils/alloy/devices/devices.json cat < $HSHQ_STACKS_DIR/sysutils/alloy/Caddyfile :80 { basic_auth { $ALLOY_ADMIN_USERNAME $(htpasswd -nbBC 12 $ALLOY_ADMIN_USERNAME $ALLOY_ADMIN_PASSWORD | cut -d: -f2) } reverse_proxy alloy-app:12345 } :3101 { @unauth not header Authorization "Bearer {\$ALLOY_ADMIN_PASSWORD}" respond @unauth 401 { close } @badmethod not method POST @badpath not path /loki/api/v1/raw respond @badmethod 405 { close } respond @badpath 404 { close } reverse_proxy alloy-app:3101 } EOFPM cat < $HSHQ_STACKS_DIR/sysutils/prometheus/prometheus.yml global: scrape_interval: 15s scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'node-exporter' static_configs: - targets: ['node-exporter:9100'] - job_name: 'docker' static_configs: - targets: ["host.docker.internal:$DOCKER_METRICS_PORT"] - job_name: 'litellm' metrics_path: '/metrics/' static_configs: - targets: ['litellm-proxy:4000'] authorization: type: Bearer credentials: "$LITELLM_AUTH_KEY" - job_name: 'crowdsec' static_configs: - targets: ['crowdsec-app:6060'] EOFPM # Temporary cat < $HSHQ_STACKS_DIR/sysutils/alloy/config.alloy // ===================================================================== // HSHQ Alloy configuration — PHASE 1 (baseline data gathering) // // Destination in container: // /etc/alloy/config.alloy // // Streams shipped to Loki: // * Docker container logs (containers labelled loki.ship=true) // * Caddy access logs -> /var/log/caddy/caddy-*-access.log {log_source="caddy_access"} // * CrowdSec AppSec alerts -> /var/log/crowdsec/baseline-alerts.json {log_source="crowdsec_alerts"} // * Suricata EVE JSON -> /var/log/suricata/eve.json {log_source="suricata"} // * Host logs -> /var/log/{syslog,auth.log,hshq.log} {log_source="host"} // * LiteLLM -> HTTP API on :3101 {log_source="litellm"} // // NOTE: this config reads the Caddy logs from /var/log/caddy (the directory // Caddy now writes to). Alloy mounts the host /var/log, so no separate // /caddylogs mount is required. If you prefer a dedicated mount, change the // \`loki.source.file "caddy"\` path accordingly. // // Cardinality rules: // * low-cardinality values -> stage.labels // * high-cardinality values -> stage.structured_metadata // ===================================================================== // ──────────────────────────────────────────────────────────────────── // Docker containers → syslog files in /var/log/docker // Docker socket is used for METADATA ONLY // Just add loki.ship: "true" label to any container to ingest logs // ──────────────────────────────────────────────────────────────────── discovery.docker "engine" { host = "unix:///var/run/docker.sock" } discovery.relabel "containers" { targets = discovery.docker.engine.targets rule { source_labels = ["__meta_docker_container_label_loki_ship"] action = "keep" regex = "true" } rule { source_labels = ["__meta_docker_container_name"] regex = "/(.*)" target_label = "__path__" replacement = "/var/log/docker/\$1.log" } rule { source_labels = ["__meta_docker_container_name"] regex = "/(.*)" target_label = "container" } rule { source_labels = ["__meta_docker_container_label_com_docker_compose_service"] target_label = "service" } rule { source_labels = ["__meta_docker_container_label_com_docker_compose_project"] target_label = "project" } rule { target_label = "log_source" replacement = "docker" } } loki.source.file "docker_logs" { targets = discovery.relabel.containers.output forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Caddy access logs → /var/log/caddy/caddy-*-access.log (JSON lines) // ──────────────────────────────────────────────────────────────────── loki.source.file "caddy" { targets = [ { __path__ = "/var/log/caddy/caddy-*-access.log" }, ] forward_to = [loki.relabel.caddy_instance.receiver] file_match { enabled = true sync_period = "10s" } } loki.relabel "caddy_instance" { forward_to = [loki.process.caddy_parse.receiver] rule { source_labels = ["filename"] regex = ".*/(caddy-.+)-access\\\\.log" replacement = "\$1-access" target_label = "job" } rule { target_label = "log_source" replacement = "caddy_access" } } // ──────────────────────────────────────────────────────────────────── // Device-name enrichment (Caddy access logs only) // ──────────────────────────────────────────────────────────────────── discovery.file "devices" { files = ["/etc/alloy/devices/devices.json"] refresh_interval = "30s" } // PHASE 1: extract host/method/status (labels) and // uri/content_type/user_agent/accept (structured metadata) from the // Caddy JSON access line, in addition to the client_ip label. // // NOTE: Caddy header keys contain a hyphen. If the expression parser // rejects \`request.headers.Content-Type[0]\`, use the quoted form: // content_type = "request.headers.\"Content-Type\"[0]" loki.process "caddy_parse" { stage.json { expressions = { host = "request.host", method = "request.method", status = "status", uri = "request.uri", content_type = "request.headers.Content-Type[0]", user_agent = "request.headers.User-Agent[0]", accept = "request.headers.Accept[0]", client_ip = "request.client_ip", } } stage.labels { values = { client_ip = "", host = "", method = "", status = "" } } stage.structured_metadata { values = { uri = "", content_type = "", user_agent = "", accept = "" } } forward_to = [loki.enrich.devices.receiver] } loki.enrich "devices" { targets = discovery.file.devices.targets target_match_label = "__address__" logs_match_label = "client_ip" labels_to_copy = ["device_name"] forward_to = [loki.process.caddy_default.receiver] } loki.process "caddy_default" { stage.match { selector = \`{device_name=""}\` stage.static_labels { values = { device_name = "UNKNOWN" } } } forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // CrowdSec AppSec baseline alerts // // The \`notification-file\` plugin inside crowdsec-app appends one JSON // object per alert to /var/log/crowdsec/baseline-alerts.json. Because // crowdsec-app bind-mounts /var/log/crowdsec to the host, and Alloy // mounts the host /var/log, this file is visible here. // // Line shape (one JSON object per line): // {"t":"...","src":"1.2.3.4","uuid":"...","alert":{}} // ──────────────────────────────────────────────────────────────────── loki.source.file "crowdsec_alerts" { targets = [ { __path__ = "/var/log/crowdsec/baseline-alerts.json" }, ] forward_to = [loki.process.crowdsec_alerts.receiver] file_match { enabled = true sync_period = "10s" } } loki.process "crowdsec_alerts" { stage.static_labels { values = { job = "crowdsec_alerts", log_source = "crowdsec_alerts", } } stage.json { expressions = { host = "host", rule = "rule", zones = "zones", uri = "uri", src = "src", data = "data", t = "t", scenario = "scenario", } } stage.labels { values = { host = "" } // low cardinality -> label } stage.structured_metadata { values = { rule = "", zones = "", uri = "", src = "", scenario = "" } } forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Suricata EVE JSON → /var/log/suricata/eve.json (one JSON object/line) // // Each line has \`event_type\` (alert, anomaly, dns, http, tls, flow, // fileinfo, stats, ...). We ship ALL event types by default so you can // filter in Loki with \`{log_source="suricata", event_type="alert"}\`. // // If the volume is too high (eve.json is large and mostly flow/dns/stats), // uncomment the \`stage.match\` block below to keep only security-relevant // event types. // ──────────────────────────────────────────────────────────────────── loki.source.file "suricata" { targets = [ { __path__ = "/var/log/suricata/eve.json" }, ] forward_to = [loki.process.suricata.receiver] file_match { enabled = true sync_period = "10s" } } loki.process "suricata" { stage.static_labels { values = { job = "suricata", log_source = "suricata", } } stage.json { expressions = { event_type = "event_type", src_ip = "src_ip", dest_ip = "dest_ip", proto = "proto", app_proto = "app_proto", signature = "alert.signature", category = "alert.category", severity = "alert.severity", } } // Optional: drop the high-volume, low-value event types. // stage.match { // selector = \`{job="suricata"} | event_type!~"alert|anomaly|fileinfo"\` // action = "drop" // } stage.labels { values = { event_type = "" } // low cardinality -> label } stage.structured_metadata { values = { src_ip = "", dest_ip = "", proto = "", app_proto = "", signature = "", category = "", severity = "", } } forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Host logs → /var/log/* (one file = one service) // ──────────────────────────────────────────────────────────────────── loki.source.file "host" { targets = [ { __path__ = "/var/log/syslog" }, { __path__ = "/var/log/auth.log" }, { __path__ = "/var/log/hshq.log" }, ] forward_to = [loki.relabel.host_instance.receiver] } loki.relabel "host_instance" { forward_to = [loki.write.local.receiver] rule { source_labels = ["filename"] regex = ".*/(.+)" replacement = "\$1" target_label = "job" } rule { target_label = "log_source" replacement = "host" } } // ──────────────────────────────────────────────────────────────────── // LiteLLM request/response records → /loki/api/v1/raw (NDJSON) // ──────────────────────────────────────────────────────────────────── loki.source.api "litellm" { http { listen_address = "0.0.0.0" listen_port = 3101 } forward_to = [loki.process.llm.receiver] } loki.process "llm" { stage.static_labels { values = { job = "litellm", log_source = "litellm", } } forward_to = [loki.write.local.receiver] } // ──────────────────────────────────────────────────────────────────── // Output // ──────────────────────────────────────────────────────────────────── loki.write "local" { endpoint { url = "http://loki:3100/loki/api/v1/push" } } EOFPM cat < $HSHQ_STACKS_DIR/sysutils/loki/config.yml # ===================================================================== # HSHQ Loki configuration — PHASE 1 (baseline data gathering) # # Destination in container: # /etc/loki/config.yml # # Changes vs the stock HSHQ Loki config: # * \`max_query_lookback\` raised 168h -> 336h so the full 7-day baseline # window stays queryable at the end of collection. # * ingestion limits raised (the stock 4 MB/s was being exceeded: # "ingestion rate limit exceeded" in the Loki log), so nothing is # silently dropped while the baseline streams are high volume. # # \`retention_period: 744h\` (31d) already exceeds the collection window. # ===================================================================== auth_enabled: false server: http_listen_port: 3100 common: path_prefix: /loki storage: filesystem: chunks_directory: /loki/chunks rules_directory: /loki/rules replication_factor: 1 ring: kvstore: store: inmemory schema_config: configs: - from: 2024-01-01 store: tsdb object_store: filesystem schema: v13 index: prefix: index_ period: 24h limits_config: volume_enabled: true max_query_lookback: 336h # PHASE 1: was 168h; keeps the full baseline window queryable retention_period: 744h # PHASE 1: raise the default 4 MB/s ingestion limits (they were being exceeded). ingestion_rate_mb: 32 ingestion_burst_size_mb: 64 per_stream_rate_limit: 16MB per_stream_rate_limit_burst: 32MB max_global_streams_per_user: 0 # 0 = unlimited discover_log_levels: true discover_service_name: - service_name - job - container - service - host pattern_ingester: enabled: true EOFPM } function updateDeviceIPMappingFile() { if ! [ -d $HSHQ_STACKS_DIR/sysutils/alloy/devices ]; then return fi outputDeviceIPMapping $HSHQ_STACKS_DIR/sysutils/alloy/devices/devices.json if docker ps | grep -q alloy && docker ps | grep -q grafana; then docker exec grafana bash -c "curl -fsS -X POST http://alloy-app:12345/-/reload" > /dev/null 2>&1 fi } function performUpdateSysUtils() { perform_stack_name=sysutils prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=grafana/grafana-oss:9.5.8,prom/prometheus:v2.46.0,prom/node-exporter:v1.6.1,influxdb:2.7.1-alpine image_update_map[0]="grafana/grafana-oss:9.5.8,grafana/grafana-oss:10.3.4" image_update_map[1]="prom/prometheus:v2.46.0,prom/prometheus:v2.50.1" image_update_map[2]="prom/node-exporter:v1.6.1,prom/node-exporter:v1.7.0" image_update_map[3]="influxdb:2.7.1-alpine,influxdb:2.7.5-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfAddPrometheusWeb false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v4 curImageList=grafana/grafana-oss:9.5.15,prom/prometheus:v2.48.1,prom/node-exporter:v1.7.0,influxdb:2.7.4-alpine image_update_map[0]="grafana/grafana-oss:9.5.15,grafana/grafana-oss:10.3.4" image_update_map[1]="prom/prometheus:v2.48.1,prom/prometheus:v2.50.1" image_update_map[2]="prom/node-exporter:v1.7.0,prom/node-exporter:v1.7.0" image_update_map[3]="influxdb:2.7.4-alpine,influxdb:2.7.5-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfAddPrometheusWeb false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v4 curImageList=grafana/grafana-oss:10.3.1,prom/prometheus:v2.49.1,prom/node-exporter:v1.7.0,influxdb:2.7.5-alpine image_update_map[0]="grafana/grafana-oss:10.3.1,grafana/grafana-oss:10.3.4" image_update_map[1]="prom/prometheus:v2.49.1,prom/prometheus:v2.50.1" image_update_map[2]="prom/node-exporter:v1.7.0,prom/node-exporter:v1.7.0" image_update_map[3]="influxdb:2.7.5-alpine,influxdb:2.7.5-alpine" ;; 4) # Let's increment the upgrade to v5, i.e. v1-v3 upgrades to v4, then v4 to v5. newVer=v6 curImageList=grafana/grafana-oss:10.3.4,prom/prometheus:v2.50.1,prom/node-exporter:v1.7.0,influxdb:2.7.5-alpine image_update_map[0]="grafana/grafana-oss:10.3.4,grafana/grafana-oss:11.3.0" image_update_map[1]="prom/prometheus:v2.50.1,prom/prometheus:v2.55.0" image_update_map[2]="prom/node-exporter:v1.7.0,prom/node-exporter:v1.8.2" image_update_map[3]="influxdb:2.7.5-alpine,influxdb:2.7.10-alpine" ;; 5) newVer=v6 curImageList=grafana/grafana-oss:11.1.3,prom/prometheus:v2.53.1,prom/node-exporter:v1.8.2,influxdb:2.7.8-alpine image_update_map[0]="grafana/grafana-oss:11.1.3,grafana/grafana-oss:11.3.0" image_update_map[1]="prom/prometheus:v2.53.1,prom/prometheus:v2.55.0" image_update_map[2]="prom/node-exporter:v1.8.2,prom/node-exporter:v1.8.2" image_update_map[3]="influxdb:2.7.8-alpine,influxdb:2.7.10-alpine" ;; 6) newVer=v8 curImageList=grafana/grafana-oss:11.3.0,prom/prometheus:v2.55.0,prom/node-exporter:v1.8.2,influxdb:2.7.10-alpine image_update_map[0]="grafana/grafana-oss:11.3.0,mirror.gcr.io/grafana/grafana-oss:12.1.0" image_update_map[1]="prom/prometheus:v2.55.0,mirror.gcr.io/prom/prometheus:v3.5.0" image_update_map[2]="prom/node-exporter:v1.8.2,mirror.gcr.io/prom/node-exporter:v1.9.1" image_update_map[3]="influxdb:2.7.10-alpine,mirror.gcr.io/influxdb:2.7.12-alpine" ;; 7) newVer=v8 curImageList=grafana/grafana-oss:11.6.0,prom/prometheus:v3.2.1,prom/node-exporter:v1.9.1,influxdb:2.7.11-alpine image_update_map[0]="grafana/grafana-oss:11.6.0,mirror.gcr.io/grafana/grafana-oss:12.1.0" image_update_map[1]="prom/prometheus:v3.2.1,mirror.gcr.io/prom/prometheus:v3.5.0" image_update_map[2]="prom/node-exporter:v1.9.1,mirror.gcr.io/prom/node-exporter:v1.9.1" image_update_map[3]="influxdb:2.7.11-alpine,mirror.gcr.io/influxdb:2.7.12-alpine" ;; 8) newVer=v9 curImageList=mirror.gcr.io/grafana/grafana-oss:12.1.0,mirror.gcr.io/prom/prometheus:v3.5.0,mirror.gcr.io/prom/node-exporter:v1.9.1,mirror.gcr.io/influxdb:2.7.12-alpine image_update_map[0]="mirror.gcr.io/grafana/grafana-oss:12.1.0,mirror.gcr.io/grafana/grafana-oss:12.2.0-17142428006" image_update_map[1]="mirror.gcr.io/prom/prometheus:v3.5.0,mirror.gcr.io/prom/prometheus:v3.8.0" image_update_map[2]="mirror.gcr.io/prom/node-exporter:v1.9.1,mirror.gcr.io/prom/node-exporter:v1.10.2" image_update_map[3]="mirror.gcr.io/influxdb:2.7.12-alpine,mirror.gcr.io/influxdb:2.7.12-alpine" ;; 9) newVer=v10 curImageList=mirror.gcr.io/grafana/grafana-oss:12.2.0-17142428006,mirror.gcr.io/prom/prometheus:v3.8.0,mirror.gcr.io/prom/node-exporter:v1.10.2,mirror.gcr.io/influxdb:2.7.12-alpine image_update_map[0]="mirror.gcr.io/grafana/grafana-oss:12.2.0-17142428006,mirror.gcr.io/grafana/grafana-oss:13.0.2" image_update_map[1]="mirror.gcr.io/prom/prometheus:v3.8.0,mirror.gcr.io/prom/prometheus:v3.13.1" image_update_map[2]="mirror.gcr.io/prom/node-exporter:v1.10.2,mirror.gcr.io/prom/node-exporter:v1.12.0" image_update_map[3]="mirror.gcr.io/influxdb:2.7.12-alpine,mirror.gcr.io/influxdb:2.7.12-alpine" ;; 10) newVer=v11 curImageList=mirror.gcr.io/grafana/grafana-oss:13.0.2,mirror.gcr.io/prom/prometheus:v3.13.1,mirror.gcr.io/prom/node-exporter:v1.12.0,mirror.gcr.io/influxdb:2.7.12-alpine image_update_map[0]="mirror.gcr.io/grafana/grafana-oss:13.0.2,mirror.gcr.io/grafana/grafana-oss:13.0.2" image_update_map[1]="mirror.gcr.io/prom/prometheus:v3.13.1,mirror.gcr.io/prom/prometheus:v3.13.1" image_update_map[2]="mirror.gcr.io/prom/node-exporter:v1.12.0,mirror.gcr.io/prom/node-exporter:v1.12.0" image_update_map[3]="mirror.gcr.io/influxdb:2.7.12-alpine,mirror.gcr.io/influxdb:2.7.12-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateSysUtilsV11 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 11) newVer=v11 curImageList=mirror.gcr.io/grafana/grafana-oss:13.0.2,mirror.gcr.io/prom/prometheus:v3.13.1,mirror.gcr.io/prom/node-exporter:v1.12.0,mirror.gcr.io/influxdb:2.7.12-alpine,mirror.gcr.io/grafana/loki:3.7.7,mirror.gcr.io/grafana/alloy:v1.19.2,mirror.gcr.io/caddy:2.11.4 image_update_map[0]="mirror.gcr.io/grafana/grafana-oss:13.0.2,mirror.gcr.io/grafana/grafana-oss:13.0.2" image_update_map[1]="mirror.gcr.io/prom/prometheus:v3.13.1,mirror.gcr.io/prom/prometheus:v3.13.1" image_update_map[2]="mirror.gcr.io/prom/node-exporter:v1.12.0,mirror.gcr.io/prom/node-exporter:v1.12.0" image_update_map[3]="mirror.gcr.io/influxdb:2.7.12-alpine,mirror.gcr.io/influxdb:2.7.12-alpine" image_update_map[4]="mirror.gcr.io/grafana/loki:3.7.7,mirror.gcr.io/grafana/loki:3.7.7" image_update_map[5]="mirror.gcr.io/grafana/alloy:v1.19.2,mirror.gcr.io/grafana/alloy:v1.19.2" image_update_map[6]="mirror.gcr.io/caddy:2.11.4,mirror.gcr.io/caddy:2.11.4" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfAddPrometheusWeb() { inner_block="" inner_block=$inner_block">>https://$SUB_PROMETHEUS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://prometheus:9090 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PROMETHEUS $MANAGETLS_PROMETHEUS true $NETDEFAULT_PROMETHEUS "$inner_block" insertEnableSvcAll sysutils "$FMLNAME_PROMETHEUS" $USERTYPE_PROMETHEUS "https://$SUB_PROMETHEUS.$HOMESERVER_DOMAIN" "prometheus.png" "$(getHeimdallOrderFromSub $SUB_PROMETHEUS $USERTYPE_PROMETHEUS)" restartAllCaddyContainers grep "job_name: docker" $HSHQ_STACKS_DIR/sysutils/prometheus/prometheus.yml > /dev/null 2>&1 if [ $? -ne 0 ]; then tee -a $HSHQ_STACKS_DIR/sysutils/prometheus/prometheus.yml >/dev/null < $HOME/sysutils-compose.yml $STACK_VERSION_PREFIX sysutils $(getScriptStackVersion sysutils) services: grafana: image: $(getScriptImageByContainerName grafana) container_name: grafana hostname: grafana user: "\${PORTAINER_UID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-sysutils-grafana:/var/lib/grafana prometheus: image: $(getScriptImageByContainerName prometheus) container_name: prometheus hostname: prometheus restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true extra_hosts: - host.docker.internal:host-gateway command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' - '--web.enable-lifecycle' networks: - int-sysutils-net - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/prometheus:/etc/prometheus - v-sysutils-prometheus:/prometheus node-exporter: image: $(getScriptImageByContainerName node-exporter) container_name: node-exporter hostname: node-exporter restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true user: "0:0" command: - '--path.procfs=/host/proc' - '--path.rootfs=/rootfs' - '--path.sysfs=/host/sys' - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc|var/lib/docker/(containers|devicemapper|volumes)/.+)(\$\$|/)' networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro influxdb: image: $(getScriptImageByContainerName influxdb) container_name: influxdb hostname: influxdb user: "\${PORTAINER_UID}" restart: unless-stopped env_file: stack.env command: - '--reporting-disabled' security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/influxdb.crt:/certs/influxdb.crt - \${PORTAINER_HSHQ_SSL_DIR}/influxdb.key:/certs/influxdb.key - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/influxdb/etc:/etc/influxdb2 - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/influxdb/var:/var/lib/influxdb2 volumes: v-sysutils-grafana: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/grafana v-sysutils-prometheus: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/sysutils/prometheus networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true int-sysutils-net: driver: bridge internal: true ipam: driver: default EOFGF # Too many edits to the compose file, just replace the whole darn thing... chmod 600 $HOME/sysutils-compose.yml sudo chown root:root $HOME/sysutils-compose.yml sudo mv $HOME/sysutils-compose.yml $upgrade_compose_file } function mfUpdateSysUtilsV11() { mkdir -p $HSHQ_STACKS_DIR/sysutils/loki mkdir -p $HSHQ_STACKS_DIR/sysutils/alloy mkdir -p $HSHQ_STACKS_DIR/sysutils/alloy/devices mkdir -p $HSHQ_NONBACKUP_DIR/sysutils/loki outputLokiAlloyConfig inner_block="" inner_block=$inner_block">>https://$SUB_ALLOY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>forward_auth https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>uri /api/verify?rd=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN\n" inner_block=$inner_block">>>>>>>>copy_headers Remote-User Remote-Groups Remote-Name Remote-Email\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://alloy-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ALLOY $MANAGETLS_ALLOY "$is_integrate_hshq" $NETDEFAULT_ALLOY "$inner_block" insertSubAuthelia $SUB_ALLOY.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} insertEnableSvcAll sysutils "$FMLNAME_ALLOY" $USERTYPE_ALLOY "https://$SUB_ALLOY.$HOMESERVER_DOMAIN" "alloy.png" "$(getHeimdallOrderFromSub $SUB_ALLOY $USERTYPE_ALLOY)" cat < $HOME/sysutils-compose.yml $STACK_VERSION_PREFIX sysutils v11 services: grafana: image: mirror.gcr.io/grafana/grafana-oss:13.0.2 container_name: grafana hostname: grafana user: "\${PORTAINER_UID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-sysutils-grafana:/var/lib/grafana prometheus: image: mirror.gcr.io/prom/prometheus:v3.13.1 container_name: prometheus hostname: prometheus restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true extra_hosts: - host.docker.internal:host-gateway command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' - '--web.enable-lifecycle' networks: - int-sysutils-net - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/prometheus:/etc/prometheus - v-sysutils-prometheus:/prometheus node-exporter: image: mirror.gcr.io/prom/node-exporter:v1.12.0 container_name: node-exporter hostname: node-exporter restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true user: "0:0" command: - '--path.procfs=/host/proc' - '--path.rootfs=/rootfs' - '--path.sysfs=/host/sys' - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc|var/lib/docker/(containers|devicemapper|volumes)/.+)(\$\$|/)' networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro influxdb: image: mirror.gcr.io/influxdb:2.7.12-alpine container_name: influxdb hostname: influxdb user: "\${PORTAINER_UID}" restart: unless-stopped env_file: stack.env command: - '--reporting-disabled' security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/influxdb.crt:/certs/influxdb.crt - \${PORTAINER_HSHQ_SSL_DIR}/influxdb.key:/certs/influxdb.key - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/influxdb/etc:/etc/influxdb2 - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/influxdb/var:/var/lib/influxdb2 loki: image: mirror.gcr.io/grafana/loki:3.7.7 container_name: loki hostname: loki restart: unless-stopped security_opt: - no-new-privileges:true command: -config.file=/etc/loki/config.yml networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/loki/config.yml:/etc/loki/config.yml:ro - v-sysutils-loki:/loki alloy-app: image: mirror.gcr.io/grafana/alloy:v1.19.2 container_name: alloy-app hostname: alloy-app restart: unless-stopped security_opt: - no-new-privileges:true command: - run - --stability.level=experimental - --server.http.listen-addr=0.0.0.0:12345 - /etc/alloy/config.alloy networks: - int-sysutils-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /var/run/docker.sock:/var/run/docker.sock:ro - /var/log:/var/log:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/alloy/devices:/etc/alloy/devices:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/alloy/config.alloy:/etc/alloy/config.alloy:ro alloy-web: image: mirror.gcr.io/caddy:2.11.4 container_name: alloy-web hostname: alloy-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-sysutils-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/alloy/Caddyfile:/etc/caddy/Caddyfile volumes: v-sysutils-grafana: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/sysutils/grafana v-sysutils-prometheus: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/sysutils/prometheus v-sysutils-loki: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/sysutils/loki networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true int-sysutils-net: driver: bridge internal: true ipam: driver: default EOFGF set +e grep -q ALLOY_ADMIN_USERNAME $HOME/sysutils.env if [ $? -ne 0 ]; then echo "ALLOY_ADMIN_USERNAME=$ALLOY_ADMIN_USERNAME" >> $HOME/sysutils.env echo "ALLOY_ADMIN_PASSWORD=$ALLOY_ADMIN_PASSWORD" >> $HOME/sysutils.env sendEmail -s "Alloy Admin Login Info" -b "Alloy Admin Username: $ALLOY_ADMIN_USERNAME\nAlloy Admin Password: $ALLOY_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" fi } # OpenLDAP function installOpenLDAP() { set +e is_integrate_hshq=$1 # Only install if directory does not exist checkDeleteStackAndDirectory openldap "OpenLDAP" cdRes=$? if [ $cdRes -ne 0 ]; then echo "ERROR: OpenLDAP directory exists" exit 1 fi pullImage $(getScriptImageByContainerName ldapserver) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain OpenLDAP Server docker image" exit 1 fi pullImage $(getScriptImageByContainerName ldapphp) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain OpenLDAP Manager docker image" exit 1 fi pullImage $(getScriptImageByContainerName ldapmanager) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain OpenLDAP PHP docker image" exit 1 fi set -e mkdir $HSHQ_STACKS_DIR/openldap mkdir $HSHQ_STACKS_DIR/openldap/certs mkdir $HSHQ_STACKS_DIR/openldap/ldapserver mkdir $HSHQ_STACKS_DIR/openldap/ldapphp mkdir $HSHQ_STACKS_DIR/openldap/ldapmanager mkdir $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig initServicesCredentials LDAP_URI="ldap://ldapserver:389" updateConfigVar LDAP_URI $LDAP_URI LDAPS_URI="ldaps://ldapserver:636" updateConfigVar LDAPS_URI $LDAPS_URI rm -f $HSHQ_SECRETS_DIR/ldap_admin_bind_dn.txt echo "$LDAP_ADMIN_BIND_DN" > $HSHQ_SECRETS_DIR/ldap_admin_bind_dn.txt chmod 0400 $HSHQ_SECRETS_DIR/ldap_admin_bind_dn.txt rm -f $HSHQ_SECRETS_DIR/ldap_admin_bind_password.txt echo "$LDAP_ADMIN_BIND_PASSWORD" > $HSHQ_SECRETS_DIR/ldap_admin_bind_password.txt chmod 0400 $HSHQ_SECRETS_DIR/ldap_admin_bind_password.txt if [ -z "$LDAP_READONLY_USER_USERNAME" ]; then LDAP_READONLY_USER_USERNAME=$ADMIN_USERNAME_BASE"_openldap_readonly" updateConfigVar LDAP_READONLY_USER_USERNAME $LDAP_READONLY_USER_USERNAME fi if [ -z "$LDAP_READONLY_USER_PASSWORD" ]; then LDAP_READONLY_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LDAP_READONLY_USER_PASSWORD $LDAP_READONLY_USER_PASSWORD fi rm -f $HSHQ_SECRETS_DIR/ldap_readonly_user_password.txt echo "$LDAP_READONLY_USER_PASSWORD" > $HSHQ_SECRETS_DIR/ldap_readonly_user_password.txt chmod 0400 $HSHQ_SECRETS_DIR/ldap_readonly_user_password.txt if [ -z "$LDAP_CONFIG_PASSWORD" ]; then LDAP_CONFIG_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar LDAP_CONFIG_PASSWORD $LDAP_CONFIG_PASSWORD fi rm -f $HSHQ_SECRETS_DIR/ldap_config_password.txt echo "$LDAP_CONFIG_PASSWORD" > $HSHQ_SECRETS_DIR/ldap_config_password.txt chmod 0400 $HSHQ_SECRETS_DIR/ldap_config_password.txt LDAP_READONLY_USER_BIND_DN="cn=$LDAP_READONLY_USER_USERNAME,$LDAP_BASE_DN" updateConfigVar LDAP_READONLY_USER_BIND_DN $LDAP_READONLY_USER_BIND_DN ADMIN_PASSWORD_CRYPT=$(openssl passwd -6 $LDAP_ADMIN_USER_PASSWORD) outputConfigOpenLDAP generateCert ldapserver ldapserver generateCert ldapphp ldapphp generateCert ldapmanager ldapmanager cp $HSHQ_SSL_DIR/dhparam.pem $HSHQ_STACKS_DIR/openldap/certs/dhparam.pem cp $HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt $HSHQ_STACKS_DIR/openldap/certs/${CERTS_ROOT_CA_NAME}.crt installStack openldap ldapserver "slapd starting" $HOME/openldap.env retval=$? if [ $retval -ne 0 ]; then echo "ERROR: There was a problem installing OpenLDAP" exit $retval fi docker exec ldapserver chmod +x /tmp/initconfig/initdbscript.sh docker exec ldapserver sh /tmp/initconfig/initdbscript.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/* chmod 644 $HSHQ_SSL_DIR/dhparam.pem inner_block="" inner_block=$inner_block">>https://$SUB_OPENLDAP_PHP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://ldapphp {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENLDAP_PHP $MANAGETLS_OPENLDAP_PHP "$is_integrate_hshq" $NETDEFAULT_OPENLDAP_PHP "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://ldapmanager {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENLDAP_MANAGER $MANAGETLS_OPENLDAP_MANAGER "$is_integrate_hshq" $NETDEFAULT_OPENLDAP_MANAGER "$inner_block" } function outputConfigOpenLDAP() { cat < $HOME/openldap-compose.yml $STACK_VERSION_PREFIX openldap $(getScriptStackVersion openldap) services: ldapserver: image: $(getScriptImageByContainerName ldapserver) container_name: ldapserver hostname: ldapserver restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/ldapserver.crt:/container/service/slapd/assets/certs/ldapserver.crt - \${PORTAINER_HSHQ_SSL_DIR}/ldapserver.key:/container/service/slapd/assets/certs/ldapserver.key - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/certs/${CERTS_ROOT_CA_NAME}.crt:/container/service/slapd/assets/certs/${CERTS_ROOT_CA_NAME}.crt - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/certs/dhparam.pem:/container/service/slapd/assets/certs/dhparam.pem - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/ldapserver/initconfig:/tmp/initconfig - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/ldapserver/db:/var/lib/ldap - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/ldapserver/slapd:/etc/ldap/slapd.d secrets: - ldap_admin_bind_password - ldap_config_password - ldap_readonly_user_password ldapphp: image: $(getScriptImageByContainerName ldapphp) container_name: ldapphp hostname: ldapphp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ldap-net - dock-proxy-net depends_on: - ldapserver volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/ldapphp/www:/var/www/phpldapadmin - \${PORTAINER_HSHQ_SSL_DIR}/ldapphp.crt:/container/service/phpldapadmin/assets/apache2/certs/ldapphp.crt - \${PORTAINER_HSHQ_SSL_DIR}/ldapphp.key:/container/service/phpldapadmin/assets/apache2/certs/ldapphp.key - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/certs/${CERTS_ROOT_CA_NAME}.crt:/container/service/phpldapadmin/assets/apache2/certs/${CERTS_ROOT_CA_NAME}.crt - \${PORTAINER_HSHQ_SSL_DIR}/ldapphp.crt:/container/service/ldap-client/assets/certs/ldapphp.crt - \${PORTAINER_HSHQ_SSL_DIR}/ldapphp.key:/container/service/ldap-client/assets/certs/ldapphp.key - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/certs/${CERTS_ROOT_CA_NAME}.crt:/container/service/ldap-client/assets/certs/${CERTS_ROOT_CA_NAME}.crt environment: - "PHPLDAPADMIN_LDAP_HOSTS=#PYTHON2BASH:[{'ldapserver': [{'server': [{'tls': True}]}]}]" ldapmanager: image: $(getScriptImageByContainerName ldapmanager) container_name: ldapmanager hostname: ldapmanager restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ldap-net - dock-proxy-net - dock-internalmail-net depends_on: - ldapserver volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/ldapmanager.crt:/opt/ssl/ldapmanager.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/ldapmanager.key:/opt/ssl/ldapmanager.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/certs/${CERTS_ROOT_CA_NAME}.crt:/opt/ssl/${CERTS_ROOT_CA_NAME}.crt:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openldap/ldapmanager/ldap.conf:/etc/ldap/ldap.conf secrets: - ldap_admin_bind_dn - ldap_admin_bind_password - ldap_config_password - ldap_readonly_user_password secrets: ldap_admin_bind_dn: file: \${PORTAINER_HSHQ_SECRETS_DIR}/ldap_admin_bind_dn.txt ldap_admin_bind_password: file: \${PORTAINER_HSHQ_SECRETS_DIR}/ldap_admin_bind_password.txt ldap_config_password: file: \${PORTAINER_HSHQ_SECRETS_DIR}/ldap_config_password.txt ldap_readonly_user_password: file: \${PORTAINER_HSHQ_SECRETS_DIR}/ldap_readonly_user_password.txt networks: dock-ldap-net: name: dock-ldap external: true dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true EOFLC cat < $HOME/openldap.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID LDAP_OPENLDAP_UID=$USERID LDAP_OPENLDAP_GID=$GROUPID LDAP_ORGANISATION=$HOMESERVER_NAME LDAP_DOMAIN=$HOMESERVER_DOMAIN LDAP_BASE_DN=$LDAP_BASE_DN LDAP_READONLY_USER_USERNAME=$LDAP_READONLY_USER_USERNAME LDAP_TLS_VERIFY_CLIENT=try LDAP_READONLY_USER=true LDAP_RFC2307BIS_SCHEMA=true LDAP_TLS=true LDAP_TLS_ENFORCE=true LDAP_TLS_CRT_FILENAME=ldapserver.crt LDAP_TLS_KEY_FILENAME=ldapserver.key LDAP_TLS_CA_CRT_FILENAME=${CERTS_ROOT_CA_NAME}.crt LDAP_TLS_DH_PARAM_FILENAME=dhparam.pem LDAP_ADMIN_PASSWORD_FILE=/run/secrets/ldap_admin_bind_password LDAP_CONFIG_PASSWORD_FILE=/run/secrets/ldap_config_password LDAP_READONLY_USER_PASSWORD_FILE=/run/secrets/ldap_readonly_user_password PHPLDAPADMIN_HTTPS=true PHPLDAPADMIN_LDAP_CLIENT_TLS=true PHPLDAPADMIN_LDAP_CLIENT_TLS_REQCERT=demand PHPLDAPADMIN_HTTPS_CA_CRT_FILENAME=${CERTS_ROOT_CA_NAME}.crt PHPLDAPADMIN_HTTPS_CRT_FILENAME=ldapphp.crt PHPLDAPADMIN_HTTPS_KEY_FILENAME=ldapphp.key PHPLDAPADMIN_LDAP_CLIENT_TLS_CRT_FILENAME=ldapphp.crt PHPLDAPADMIN_LDAP_CLIENT_TLS_KEY_FILENAME=ldapphp.key PHPLDAPADMIN_LDAP_CLIENT_TLS_CA_CRT_FILENAME=${CERTS_ROOT_CA_NAME}.crt SERVER_HOSTNAME=$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN LDAP_URI=$LDAP_URI EMAIL_DOMAIN=$HOMESERVER_DOMAIN SMTP_HOSTNAME=$SMTP_HOSTNAME SMTP_HOST_PORT=$SMTP_HOSTPORT EMAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS EMAIL_FROM_NAME=$LDAP_EMAIL_FROM_NAME ACCOUNT_REQUESTS_ENABLED=$LDAP_ACCOUNT_REQUESTS_ENABLED ACCOUNT_REQUESTS_EMAIL=$LDAP_ACCOUNT_REQUESTS_EMAIL LDAP_ACCOUNT_ADDITIONAL_ATTRIBUTES=jpegphoto^:Photograph ORGANISATION_NAME=$HOMESERVER_NAME SITE_NAME=$HOMESERVER_NAME User Manager LDAP_REQUIRE_STARTTLS=true LDAP_ADMINS_GROUP=$LDAP_ADMIN_USER_GROUP_NAME NO_HTTPS=false LDAP_IGNORE_CERT_ERRORS=false DEFAULT_USER_SHELL=/sbin/nologin USERNAME_FORMAT={first_name} ENFORCE_SAFE_SYSTEM_NAMES=true SMTP_USE_TLS=true SERVER_CERT_FILENAME=ldapmanager.crt SERVER_KEY_FILENAME=ldapmanager.key CA_CERT_FILENAME=${CERTS_ROOT_CA_NAME}.crt LDAP_ADMIN_BIND_DN_FILE=/run/secrets/ldap_admin_bind_dn LDAP_ADMIN_BIND_PWD_FILE=/run/secrets/ldap_admin_bind_password SMTP_USERNAME_FILE=/run/secrets/smtp_username SMTP_PASSWORD_FILE=/run/secrets/smtp_password NEW_ACCOUNT_EMAIL_BODY=

New Account Created

You've been set up with an account for {organisation}. Your credentials are:

Login: {login}
Password: {password}

You should log into {change_password_url} and change the password as soon as possible.

EOFLD cat < $HSHQ_STACKS_DIR/openldap/ldapmanager/ldap.conf TLS_CERT /opt/ssl/ldapmanager.crt TLS_KEY /opt/ssl/ldapmanager.key TLS_CACERT /opt/ssl/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT demand TLS_REQSAN demand EOFLD cat < $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/initdbscript.sh #!/bin/sh ldapadd -f /tmp/initconfig/initdb.ldif -D $LDAP_ADMIN_BIND_DN -Z -w $LDAP_ADMIN_BIND_PASSWORD EOFLI cat < $HSHQ_STACKS_DIR/openldap/ldapserver/initconfig/initdb.ldif # People dn: ou=people,${LDAP_BASE_DN} objectClass: organizationalUnit ou: people # Groups dn: ou=groups,${LDAP_BASE_DN} objectClass: organizationalUnit ou: groups # Admin User dn: uid=${LDAP_ADMIN_USER_USERNAME},ou=people,${LDAP_BASE_DN} givenName:: $(echo -n "${HOMESERVER_ABBREV^^} " | base64) sn: Admin uid: ${LDAP_ADMIN_USER_USERNAME} mail: ${EMAIL_ADMIN_EMAIL_ADDRESS} objectClass: person objectClass: inetOrgPerson objectClass: posixAccount uidNumber: 2001 gidNumber: 2001 loginShell: /sbin/nologin homeDirectory: /home/${LDAP_ADMIN_USER_USERNAME} cn: ${HOMESERVER_ABBREV^^} Admin userPassword: {CRYPT}${ADMIN_PASSWORD_CRYPT} # Last UID dn: cn=lastUID,${LDAP_BASE_DN} objectClass: device objectClass: top description: Records the last UID used to create a Posix account. This prevents the re-use of a UID from a deleted account. cn: lastUID serialNumber: 2001 # Last GID dn: cn=lastGID,${LDAP_BASE_DN} objectClass: device objectClass: top description: Records the last GID used to create a Posix group. This prevents the re-use of a GID from a deleted group. cn: lastGID serialNumber: 2004 # Group Everybody dn: cn=everybody,ou=groups,${LDAP_BASE_DN} objectClass: top objectClass: posixGroup objectClass: groupOfUniqueNames cn: everybody uniqueMember: uid=${LDAP_ADMIN_USER_USERNAME},ou=people,${LDAP_BASE_DN} gidNumber: 2001 # Group Admins dn: cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,${LDAP_BASE_DN} objectClass: top objectClass: posixGroup objectClass: groupOfUniqueNames cn: $LDAP_ADMIN_USER_GROUP_NAME uniqueMember: uid=${LDAP_ADMIN_USER_USERNAME},ou=people,${LDAP_BASE_DN} gidNumber: 2002 # Group Primary Users dn: cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,${LDAP_BASE_DN} objectClass: top objectClass: posixGroup objectClass: groupOfUniqueNames cn: $LDAP_PRIMARY_USER_GROUP_NAME uniqueMember: uid=${LDAP_ADMIN_USER_USERNAME},ou=people,${LDAP_BASE_DN} gidNumber: 2003 # Group Basic Users dn: cn=$LDAP_BASIC_USER_GROUP_NAME,ou=groups,${LDAP_BASE_DN} objectClass: top objectClass: posixGroup objectClass: groupOfUniqueNames cn: $LDAP_BASIC_USER_GROUP_NAME uniqueMember: uid=${LDAP_ADMIN_USER_USERNAME},ou=people,${LDAP_BASE_DN} gidNumber: 2004 EOFLB } function performUpdateOpenLDAP() { perform_stack_name=openldap prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=osixia/openldap:1.5.0,osixia/phpldapadmin:stable,wheelybird/ldap-user-manager:v1.11 image_update_map[0]="osixia/openldap:1.5.0,osixia/openldap:1.5.0" image_update_map[1]="osixia/phpldapadmin:stable,osixia/phpldapadmin:stable" image_update_map[2]="wheelybird/ldap-user-manager:v1.11,mirror.gcr.io/wheelybird/luminary:v2.2.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfOpenLDAPV2Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v2 curImageList=osixia/openldap:1.5.0,osixia/phpldapadmin:stable,mirror.gcr.io/wheelybird/luminary:v2.2.1 image_update_map[0]="osixia/openldap:1.5.0,osixia/openldap:1.5.0" image_update_map[1]="osixia/phpldapadmin:stable,osixia/phpldapadmin:stable" image_update_map[2]="mirror.gcr.io/wheelybird/luminary:v2.2.1,mirror.gcr.io/wheelybird/luminary:v2.2.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfOpenLDAPV2Update() { sed -i '/^LDAP_TLS_CACERT_FILE=.*/d' $HOME/openldap.env } # Mailu function installMailu() { set +e is_integrate_hshq=$1 # Unless directory doesn't exist, don't proceed checkDeleteStackAndDirectory mailu "Mailu" cdRes=$? if [ $cdRes -ne 0 ]; then echo "ERROR: Mailu directory exists" exit 1 fi set -e mkdir $HSHQ_STACKS_DIR/mailu mkdir $HSHQ_STACKS_DIR/mailu/certs mkdir $HSHQ_STACKS_DIR/mailu/clamav mkdir $HSHQ_STACKS_DIR/mailu/initconfig sudo chown 1000:1000 $HSHQ_STACKS_DIR/mailu/clamav # Generate email certificate if not joining another VPN if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then generateCert mail "$SMTP_HOSTNAME,$SUB_POSTFIX.$HOMESERVER_DOMAIN" fi if [ -z "$SMTP_RELAY_HOST" ] || [ -z "$SMTP_RELAY_USERNAME" ]; then # Set these to something SMTP_RELAY_HOST="[$SUB_POSTFIX.internal.$HOMESERVER_DOMAIN]:$MAILU_PORT_5" SMTP_RELAY_USERNAME=$HOMESERVER_DOMAIN SMTP_RELAY_PASSWORD=$(pwgen -c -n 32 1) fi outputConfigMailu outputMailuComposeTmp installStack mailu mailu-admin "Listening at: http://0.0.0.0:8080" $HOME/mailu.env 3 retval=$? if [ $retval -ne 0 ]; then echo "ERROR: There was a problem installing Mailu" exit $retval fi echo "Installed Mailu stack, sleeping 5 seconds..." sleep 5 echo "Adding initial users..." docker exec mailu-admin flask mailu config-import /initconfig/mail-config.yaml > /dev/null 2>&1 sleep 2 createStandardMailuMailboxes "$EMAIL_ADMIN_EMAIL_ADDRESS" createStandardMailuMailboxes "$EMAIL_JOINT_EMAIL_ADDRESS" startStopStack mailu stop rm -f $HSHQ_STACKS_DIR/mailu/initconfig/mail-config.yaml sudo mv $HSHQ_STACKS_DIR/mailu/postfix-override.cf $HSHQ_STACKS_DIR/mailu/overrides/postfix/postfix.cf sudo mv $HSHQ_STACKS_DIR/mailu/dovecot-override.conf $HSHQ_STACKS_DIR/mailu/overrides/dovecot/dovecot.conf sudo mv $HSHQ_STACKS_DIR/mailu/custom.inc.php $HSHQ_STACKS_DIR/mailu/overrides/roundcube/custom.inc.php sudo mv $HSHQ_STACKS_DIR/mailu/external_relay.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ sudo mv $HSHQ_STACKS_DIR/mailu/ip_whitelist.map $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ip_whitelist.map sudo chmod 644 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ip_whitelist.map sudo mv $HSHQ_STACKS_DIR/mailu/ip_blacklist.map $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ip_blacklist.map sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ip_blacklist.map sudo chmod 644 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ip_blacklist.map sudo touch $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/domain_whitelist.map sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/domain_whitelist.map sudo chmod 644 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/domain_whitelist.map sudo touch $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/domain_blacklist.map sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/domain_blacklist.map sudo chmod 644 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/domain_blacklist.map sudo mv $HSHQ_STACKS_DIR/mailu/multimap.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/multimap.conf sudo mv $HSHQ_STACKS_DIR/mailu/groups.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override/groups.conf sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override/groups.conf sudo chmod 664 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override/groups.conf sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override/groups.conf sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override sudo rm -fr $HSHQ_STACKS_DIR/mailu/filter/* sudo rm -fr $HSHQ_STACKS_DIR/mailu/clamav/* sleep 5 echo "Restarting mailu stack..." updateStackEnv mailu modFunMailuPostInstall inner_block="" inner_block=$inner_block">>https://$SUB_MAILU.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://$SMTP_HOSTNAME {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MAILU $MANAGETLS_MAILU "$is_integrate_hshq" $NETDEFAULT_MAILU "$inner_block" } function outputConfigMailu() { is_antivirus_commented_out="" is_antivirus_env="clamav" if [ "$(isServiceDisabled clamav)" = "true" ]; then is_antivirus_commented_out="#" is_antivirus_env="none" fi set -e outputMailuCompose cat < $HOME/mailu.env TZ=\${PORTAINER_TZ} SECRET_KEY=$(pwgen -c -n 16 1) SUBNET=$NET_MAILU_EXT_SUBNET SUBNET_PREFIX=$NET_MAILU_EXT_SUBNET_PREFIX DOMAIN=$HOMESERVER_DOMAIN HOSTNAMES=$SUB_POSTFIX.$HOMESERVER_DOMAIN POSTMASTER=$EMAIL_ADMIN_USERNAME TLS_FLAVOR=cert TLS_CERT_FILENAME=mail.crt TLS_KEYPAIR_FILENAME=mail.key OUTBOUND_TLS_LEVEL=encrypt INBOUND_TLS_ENFORCE=false AUTH_RATELIMIT_IP=60/hour AUTH_RATELIMIT_USER=100/day DISABLE_STATISTICS=True ADMIN=true WEBMAIL=roundcube WEBDAV=radicale ANTIVIRUS=$is_antivirus_env SCAN_MACROS=True MESSAGE_SIZE_LIMIT=50000000 MESSAGE_RATELIMIT=200/day RELAYHOST=$SMTP_RELAY_HOST RELAYUSER=$SMTP_RELAY_USERNAME RELAYPASSWORD=$SMTP_RELAY_PASSWORD FETCHMAIL_DELAY=600 FETCHMAIL_ENABLED=true RECIPIENT_DELIMITER=+ DMARC_RUA=$EMAIL_ADMIN_USERNAME DMARC_RUF=$EMAIL_ADMIN_USERNAME WELCOME=true WELCOME_SUBJECT=Welcome to your new email account WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly! COMPRESSION=gz COMPRESSION_LEVEL=5 WEBROOT_REDIRECT=/webmail WEB_ADMIN=/admin WEB_WEBMAIL=/webmail SITENAME=Mailu WEBSITE=https://mailu.io COMPOSE_PROJECT_NAME=mailu CREDENTIAL_ROUNDS=12 REJECT_UNLISTED_RECIPIENT=yes LOG_LEVEL=INFO DB_FLAVOR=sqlite FULL_TEXT_SEARCH=en API=true WEB_API=/api API_TOKEN=$MAILU_API_TOKEN PORTS=$CADDY_HTTP_PORT,$CADDY_HTTPS_PORT,4190,$MAILU_PORT_1,$MAILU_PORT_2,$MAILU_PORT_3,$MAILU_PORT_4,$MAILU_PORT_5,$MAILU_PORT_6,$MAILU_PORT_7 FULL_TEXT_SEARCH_ATTACHMENTS=false EOFMC cat < $HSHQ_STACKS_DIR/mailu/postfix-override.cf smtp_tls_cert_file=/certs/mail.crt smtp_tls_key_file=/certs/mail.key mynetworks = 127.0.0.1/32 $NET_MAILU_EXT_SUBNET $NET_INTERNALMAIL_SUBNET EOFMO cat < $HSHQ_STACKS_DIR/mailu/dovecot-override.conf listen = * EOFMD cat < $HSHQ_STACKS_DIR/mailu/external_relay.conf enabled = true; rules { EXTERNAL_RELAY_AUTHENTICATED { strategy = "count"; count = 3; } } EOFSP cat < $HSHQ_STACKS_DIR/mailu/ip_whitelist.map $NET_INTERNALMAIL_SUBNET EOFRS touch $HSHQ_STACKS_DIR/mailu/ip_blacklist.map cat < $HSHQ_STACKS_DIR/mailu/custom.inc.php EOFRO sudo chown root:root $HSHQ_STACKS_DIR/mailu/custom.inc.php outputMailuMultimap cat < $HSHQ_STACKS_DIR/mailu/groups.conf group "dkim" { symbols = { "R_DKIM_ALLOW" = { weight = -6.0; } } } EOFRS cat < $HSHQ_STACKS_DIR/mailu/initconfig/mail-config.yaml domain: - name: $HOMESERVER_DOMAIN user: - email: $EMAIL_ADMIN_USERNAME@$HOMESERVER_DOMAIN password: '$(openssl passwd -6 $EMAIL_ADMIN_PASSWORD)' hash_password: false global_admin: true displayed_name: '${HOMESERVER_ABBREV^^} Admin' - email: $EMAIL_SMTP_USERNAME@$HOMESERVER_DOMAIN password: '$(openssl passwd -6 $EMAIL_SMTP_PASSWORD)' hash_password: false displayed_name: '${HOMESERVER_ABBREV^^} SMTP Sender' - email: $EMAIL_JOINT_USERNAME@$HOMESERVER_DOMAIN password: '$(openssl passwd -6 $EMAIL_JOINT_PASSWORD)' hash_password: false displayed_name: '${EMAIL_JOINT_USERNAME^}' EOFMC if ! [ -z "$RELAYSERVER_WGPORTAL_ADMIN_EMAIL" ]; then echo -e "alias:" >> $HSHQ_STACKS_DIR/mailu/initconfig/mail-config.yaml echo -e " - email: $RELAYSERVER_WGPORTAL_ADMIN_USERNAME@$HOMESERVER_DOMAIN" >> $HSHQ_STACKS_DIR/mailu/initconfig/mail-config.yaml echo -e " destination:" >> $HSHQ_STACKS_DIR/mailu/initconfig/mail-config.yaml echo -e " - $EMAIL_ADMIN_EMAIL_ADDRESS\n" >> $HSHQ_STACKS_DIR/mailu/initconfig/mail-config.yaml fi } function outputMailuComposeTmp() { # This compose is only different from outputMailuCompose by one # line - in the admin container, the DNS server is set to the # mailu-unbound container rather than the default (Adguard). # This bandaid fix will avoid an installation failure when # adguard is not working correctly out of the gate. rm -f $HOME/mailu-compose.yml cat < $HOME/mailu-compose.yml $STACK_VERSION_PREFIX mailu $(getScriptStackVersion mailu) services: front: image: $(getScriptImageByContainerName $SMTP_HOSTNAME) container_name: $SMTP_HOSTNAME hostname: $SMTP_HOSTNAME restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net - dock-internalmail-net - dock-proxy-net ports: - "$MAILU_PORT_1:$MAILU_PORT_1" - "$MAILU_PORT_2:$MAILU_PORT_2" - "$MAILU_PORT_3:$MAILU_PORT_3" - "$MAILU_PORT_4:$MAILU_PORT_4" - "$MAILU_PORT_5:$MAILU_PORT_5" - "$MAILU_PORT_6:$MAILU_PORT_6" - "$MAILU_PORT_7:$MAILU_PORT_7" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/certs:/certs - \${PORTAINER_HSHQ_SSL_DIR}/mail.crt:/certs/mail.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/mail.key:/certs/mail.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/nginx:/overrides:ro resolver: image: $(getScriptImageByContainerName mailu-unbound) container_name: mailu-unbound restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: dock-mailu-ext-net: ipv4_address: \${SUBNET_PREFIX}.253 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro redis: image: $(getScriptImageByContainerName mailu-redis) container_name: mailu-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --appendonly no networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro admin: image: $(getScriptImageByContainerName mailu-admin) container_name: mailu-admin restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net dns: - \${SUBNET_PREFIX}.253 depends_on: - redis volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/initconfig:/initconfig - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/dkim:/dkim imap: image: $(getScriptImageByContainerName mailu-imap) container_name: mailu-imap restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-int-net - dock-mailu-ext-net depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/mail:/mail - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/dovecot:/overrides:ro smtp: image: $(getScriptImageByContainerName mailu-smtp) container_name: mailu-smtp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/mailqueue:/queue - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/certs:/certs - \${PORTAINER_HSHQ_SSL_DIR}/mail.crt:/certs/mail.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/mail.key:/certs/mail.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/postfix:/overrides:ro oletools: image: $(getScriptImageByContainerName mailu-oletools) container_name: mailu-oletools restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-int-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro # fts_attachments: # image: $(getScriptImageByContainerName mailu-tika) # container_name: mailu-tika # hostname: tika # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # networks: # - dock-mailu-int-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro antispam: image: $(getScriptImageByContainerName mailu-antispam) container_name: mailu-antispam hostname: antispam restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-int-net - dock-mailu-ext-net - dock-proxy-net dns: - \${SUBNET_PREFIX}.253 depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/filter:/var/lib/rspamd - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/rspamd/local:/overrides - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/rspamd/override:/etc/rspamd/override.d $is_antivirus_commented_out antivirus: $is_antivirus_commented_out image: $(getScriptImageByContainerName mailu-antivirus) $is_antivirus_commented_out container_name: mailu-antivirus $is_antivirus_commented_out restart: unless-stopped $is_antivirus_commented_out env_file: stack.env $is_antivirus_commented_out security_opt: $is_antivirus_commented_out - no-new-privileges:true $is_antivirus_commented_out networks: $is_antivirus_commented_out - dock-mailu-ext-net $is_antivirus_commented_out - dock-proxy-net $is_antivirus_commented_out volumes: $is_antivirus_commented_out - /etc/localtime:/etc/localtime:ro $is_antivirus_commented_out - /etc/timezone:/etc/timezone:ro $is_antivirus_commented_out - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/clamav:/var/lib/clamav webdav: image: $(getScriptImageByContainerName mailu-webdav) container_name: mailu-webdav restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/dav:/data fetchmail: image: $(getScriptImageByContainerName mailu-fetchmail) container_name: mailu-fetchmail restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/data/fetchmail:/data webmail: image: $(getScriptImageByContainerName mailu-webmail) container_name: mailu-webmail restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net depends_on: - imap volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/webmail:/data - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/roundcube:/overrides:ro networks: dock-internalmail-net: name: dock-internalmail external: true dock-proxy-net: name: dock-proxy external: true dock-mailu-int-net: name: dock-mailu-int external: true dock-mailu-ext-net: name: dock-mailu-ext external: true EOFMC } function outputMailuCompose() { rm -f $HOME/mailu-compose.yml cat < $HOME/mailu-compose.yml $STACK_VERSION_PREFIX mailu $(getScriptStackVersion mailu) services: front: image: $(getScriptImageByContainerName mailu-front) container_name: $SMTP_HOSTNAME hostname: $SMTP_HOSTNAME restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net - dock-internalmail-net - dock-proxy-net ports: - "$MAILU_PORT_1:$MAILU_PORT_1" - "$MAILU_PORT_2:$MAILU_PORT_2" - "$MAILU_PORT_3:$MAILU_PORT_3" - "$MAILU_PORT_4:$MAILU_PORT_4" - "$MAILU_PORT_5:$MAILU_PORT_5" - "$MAILU_PORT_6:$MAILU_PORT_6" - "$MAILU_PORT_7:$MAILU_PORT_7" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/certs:/certs - \${PORTAINER_HSHQ_SSL_DIR}/mail.crt:/certs/mail.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/mail.key:/certs/mail.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/nginx:/overrides:ro resolver: image: $(getScriptImageByContainerName mailu-unbound) container_name: mailu-unbound restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: dock-mailu-ext-net: ipv4_address: \${SUBNET_PREFIX}.253 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro redis: image: $(getScriptImageByContainerName mailu-redis) container_name: mailu-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --appendonly no networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro admin: image: $(getScriptImageByContainerName mailu-admin) container_name: mailu-admin restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net depends_on: - redis volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/initconfig:/initconfig - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/dkim:/dkim imap: image: $(getScriptImageByContainerName mailu-imap) container_name: mailu-imap restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-int-net - dock-mailu-ext-net depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/mail:/mail - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/dovecot:/overrides:ro smtp: image: $(getScriptImageByContainerName mailu-smtp) container_name: mailu-smtp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/mailqueue:/queue - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/certs:/certs - \${PORTAINER_HSHQ_SSL_DIR}/mail.crt:/certs/mail.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/mail.key:/certs/mail.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/postfix:/overrides:ro oletools: image: $(getScriptImageByContainerName mailu-oletools) container_name: mailu-oletools restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-int-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro # fts_attachments: # image: $(getScriptImageByContainerName mailu-tika) # container_name: mailu-tika # hostname: tika # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # networks: # - dock-mailu-int-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro antispam: image: $(getScriptImageByContainerName mailu-antispam) container_name: mailu-antispam hostname: antispam restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-int-net - dock-mailu-ext-net - dock-proxy-net dns: - \${SUBNET_PREFIX}.253 depends_on: - front volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/filter:/var/lib/rspamd - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/rspamd/local:/overrides - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/rspamd/override:/etc/rspamd/override.d $is_antivirus_commented_out antivirus: $is_antivirus_commented_out image: $(getScriptImageByContainerName mailu-antivirus) $is_antivirus_commented_out container_name: mailu-antivirus $is_antivirus_commented_out restart: unless-stopped $is_antivirus_commented_out env_file: stack.env $is_antivirus_commented_out security_opt: $is_antivirus_commented_out - no-new-privileges:true $is_antivirus_commented_out networks: $is_antivirus_commented_out - dock-mailu-ext-net $is_antivirus_commented_out - dock-proxy-net $is_antivirus_commented_out volumes: $is_antivirus_commented_out - /etc/localtime:/etc/localtime:ro $is_antivirus_commented_out - /etc/timezone:/etc/timezone:ro $is_antivirus_commented_out - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/clamav:/var/lib/clamav webdav: image: $(getScriptImageByContainerName mailu-webdav) container_name: mailu-webdav restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/dav:/data fetchmail: image: $(getScriptImageByContainerName mailu-fetchmail) container_name: mailu-fetchmail restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/data/fetchmail:/data webmail: image: $(getScriptImageByContainerName mailu-webmail) container_name: mailu-webmail restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-mailu-ext-net depends_on: - imap volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/webmail:/data - \${PORTAINER_HSHQ_STACKS_DIR}/mailu/overrides/roundcube:/overrides:ro networks: dock-internalmail-net: name: dock-internalmail external: true dock-proxy-net: name: dock-proxy external: true dock-mailu-int-net: name: dock-mailu-int external: true dock-mailu-ext-net: name: dock-mailu-ext external: true EOFMC } function modFunMailuPostInstall() { outputMailuCompose } function outputMailuMultimap() { cat < $HSHQ_STACKS_DIR/mailu/multimap.conf IP_WHITELIST { type = "ip"; prefilter = true; map = "/overrides/ip_whitelist.map"; action = "accept"; } IP_BLACKLIST { type = "ip"; prefilter = true; map = "/overrides/ip_blacklist.map"; action = "reject"; } DOMAIN_WHITELIST { regexp = true; type = "from"; filter = "email:domain"; map = "/overrides/domain_whitelist.map"; score = -100.0; action = "accept"; } DOMAIN_BLACKLIST { regexp = true; type = "from"; filter = "email:domain"; map = "/overrides/domain_blacklist.map"; score = 100.0; action = "reject"; } EOFRS } function performUpdateMailu() { perform_stack_name=mailu prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=bitnami/redis:7.0.5,ghcr.io/mailu/admin:2.0,ghcr.io/mailu/rspamd:2.0,ghcr.io/mailu/clamav:2.0,ghcr.io/mailu/fetchmail:2.0,ghcr.io/mailu/nginx:2.0,ghcr.io/mailu/dovecot:2.0,ghcr.io/mailu/oletools:2.0,ghcr.io/mailu/postfix:2.0,ghcr.io/mailu/unbound:2.0,ghcr.io/mailu/radicale:2.0,ghcr.io/mailu/webmail:2.0 image_update_map[0]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[1]="ghcr.io/mailu/admin:2.0,ghcr.io/mailu/admin:2.0.39" image_update_map[2]="ghcr.io/mailu/rspamd:2.0,ghcr.io/mailu/rspamd:2.0.39" image_update_map[3]="ghcr.io/mailu/clamav:2.0,ghcr.io/mailu/clamav:2.0.39" image_update_map[4]="ghcr.io/mailu/fetchmail:2.0,ghcr.io/mailu/fetchmail:2.0.39" image_update_map[5]="ghcr.io/mailu/nginx:2.0,ghcr.io/mailu/nginx:2.0.39" image_update_map[6]="ghcr.io/mailu/dovecot:2.0,ghcr.io/mailu/dovecot:2.0.39" image_update_map[7]="ghcr.io/mailu/oletools:2.0,ghcr.io/mailu/oletools:2.0.39" image_update_map[8]="ghcr.io/mailu/postfix:2.0,ghcr.io/mailu/postfix:2.0.39" image_update_map[9]="ghcr.io/mailu/unbound:2.0,ghcr.io/mailu/unbound:2.0.39" image_update_map[10]="ghcr.io/mailu/radicale:2.0,ghcr.io/mailu/radicale:2.0.39" image_update_map[11]="ghcr.io/mailu/webmail:2.0,ghcr.io/mailu/webmail:2.0.39" ;; 2) newVer=v3 curImageList=bitnami/redis:7.0.5,ghcr.io/mailu/admin:2.0.37,ghcr.io/mailu/rspamd:2.0.37,ghcr.io/mailu/clamav:2.0.37,ghcr.io/mailu/fetchmail:2.0.37,ghcr.io/mailu/nginx:2.0.37,ghcr.io/mailu/dovecot:2.0.37,ghcr.io/mailu/oletools:2.0.37,ghcr.io/mailu/postfix:2.0.37,ghcr.io/mailu/unbound:2.0.37,ghcr.io/mailu/radicale:2.0.37,ghcr.io/mailu/webmail:2.0.37 image_update_map[0]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[1]="ghcr.io/mailu/admin:2.0.37,ghcr.io/mailu/admin:2.0.39" image_update_map[2]="ghcr.io/mailu/rspamd:2.0.37,ghcr.io/mailu/rspamd:2.0.39" image_update_map[3]="ghcr.io/mailu/clamav:2.0.37,ghcr.io/mailu/clamav:2.0.39" image_update_map[4]="ghcr.io/mailu/fetchmail:2.0.37,ghcr.io/mailu/fetchmail:2.0.39" image_update_map[5]="ghcr.io/mailu/nginx:2.0.37,ghcr.io/mailu/nginx:2.0.39" image_update_map[6]="ghcr.io/mailu/dovecot:2.0.37,ghcr.io/mailu/dovecot:2.0.39" image_update_map[7]="ghcr.io/mailu/oletools:2.0.37,ghcr.io/mailu/oletools:2.0.39" image_update_map[8]="ghcr.io/mailu/postfix:2.0.37,ghcr.io/mailu/postfix:2.0.39" image_update_map[9]="ghcr.io/mailu/unbound:2.0.37,ghcr.io/mailu/unbound:2.0.39" image_update_map[10]="ghcr.io/mailu/radicale:2.0.37,ghcr.io/mailu/radicale:2.0.39" image_update_map[11]="ghcr.io/mailu/webmail:2.0.37,ghcr.io/mailu/webmail:2.0.39" ;; 3) newVer=v4 curImageList=bitnami/redis:7.0.5,ghcr.io/mailu/admin:2.0.39,ghcr.io/mailu/rspamd:2.0.39,ghcr.io/mailu/clamav:2.0.39,ghcr.io/mailu/fetchmail:2.0.39,ghcr.io/mailu/nginx:2.0.39,ghcr.io/mailu/dovecot:2.0.39,ghcr.io/mailu/oletools:2.0.39,ghcr.io/mailu/postfix:2.0.39,ghcr.io/mailu/unbound:2.0.39,ghcr.io/mailu/radicale:2.0.39,ghcr.io/mailu/webmail:2.0.39 image_update_map[0]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[1]="ghcr.io/mailu/admin:2.0.39,ghcr.io/mailu/admin:2024.06.24" image_update_map[2]="ghcr.io/mailu/rspamd:2.0.39,ghcr.io/mailu/rspamd:2024.06.24" image_update_map[3]="ghcr.io/mailu/clamav:2.0.39,clamav/clamav-debian:1.2.3-45" image_update_map[4]="ghcr.io/mailu/fetchmail:2.0.39,ghcr.io/mailu/fetchmail:2024.06.24" image_update_map[5]="ghcr.io/mailu/nginx:2.0.39,ghcr.io/mailu/nginx:2024.06.24" image_update_map[6]="ghcr.io/mailu/dovecot:2.0.39,ghcr.io/mailu/dovecot:2024.06.24" image_update_map[7]="ghcr.io/mailu/oletools:2.0.39,ghcr.io/mailu/oletools:2024.06.24" image_update_map[8]="ghcr.io/mailu/postfix:2.0.39,ghcr.io/mailu/postfix:2024.06.24" image_update_map[9]="ghcr.io/mailu/unbound:2.0.39,ghcr.io/mailu/unbound:2024.06.24" image_update_map[10]="ghcr.io/mailu/radicale:2.0.39,ghcr.io/mailu/radicale:2024.06.24" image_update_map[11]="ghcr.io/mailu/webmail:2.0.39,ghcr.io/mailu/webmail:2024.06.24" image_update_map[12]="apache/tika:2.9.2.1-full,apache/tika:2.9.2.1-full" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfMailuV4Update mfMailuV4PostUpdate perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v5 curImageList=bitnami/redis:7.0.5,ghcr.io/mailu/admin:2024.06.24,ghcr.io/mailu/rspamd:2024.06.24,clamav/clamav-debian:1.2.3-45,ghcr.io/mailu/fetchmail:2024.06.24,ghcr.io/mailu/nginx:2024.06.24,ghcr.io/mailu/dovecot:2024.06.24,ghcr.io/mailu/oletools:2024.06.24,ghcr.io/mailu/postfix:2024.06.24,ghcr.io/mailu/unbound:2024.06.24,ghcr.io/mailu/radicale:2024.06.24,ghcr.io/mailu/webmail:2024.06.24,apache/tika:2.9.2.1-full image_update_map[0]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[1]="ghcr.io/mailu/admin:2024.06.24,ghcr.io/mailu/admin:2024.06.36" image_update_map[2]="ghcr.io/mailu/rspamd:2024.06.24,ghcr.io/mailu/rspamd:2024.06.36" image_update_map[3]="clamav/clamav-debian:1.2.3-45,clamav/clamav-debian:1.4.2" image_update_map[4]="ghcr.io/mailu/fetchmail:2024.06.24,ghcr.io/mailu/fetchmail:2024.06.36" image_update_map[5]="ghcr.io/mailu/nginx:2024.06.24,ghcr.io/mailu/nginx:2024.06.36" image_update_map[6]="ghcr.io/mailu/dovecot:2024.06.24,ghcr.io/mailu/dovecot:2024.06.36" image_update_map[7]="ghcr.io/mailu/oletools:2024.06.24,ghcr.io/mailu/oletools:2024.06.36" image_update_map[8]="ghcr.io/mailu/postfix:2024.06.24,ghcr.io/mailu/postfix:2024.06.36" image_update_map[9]="ghcr.io/mailu/unbound:2024.06.24,ghcr.io/mailu/unbound:2024.06.36" image_update_map[10]="ghcr.io/mailu/radicale:2024.06.24,ghcr.io/mailu/radicale:2024.06.36" image_update_map[11]="ghcr.io/mailu/webmail:2024.06.24,ghcr.io/mailu/webmail:2024.06.36" image_update_map[12]="apache/tika:2.9.2.1-full,apache/tika:3.1.0.0-full" ;; 5) newVer=v6 curImageList=bitnami/redis:7.4.2,ghcr.io/mailu/admin:2024.06.36,ghcr.io/mailu/rspamd:2024.06.36,clamav/clamav-debian:1.4.2,ghcr.io/mailu/fetchmail:2024.06.36,ghcr.io/mailu/nginx:2024.06.36,ghcr.io/mailu/dovecot:2024.06.36,ghcr.io/mailu/oletools:2024.06.36,ghcr.io/mailu/postfix:2024.06.36,ghcr.io/mailu/unbound:2024.06.36,ghcr.io/mailu/radicale:2024.06.36,ghcr.io/mailu/webmail:2024.06.36,apache/tika:3.1.0.0-full image_update_map[0]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[1]="ghcr.io/mailu/admin:2024.06.36,ghcr.io/mailu/admin:2024.06.37" image_update_map[2]="ghcr.io/mailu/rspamd:2024.06.36,ghcr.io/mailu/rspamd:2024.06.37" image_update_map[3]="clamav/clamav-debian:1.4.2,mirror.gcr.io/clamav/clamav:1.4.3" image_update_map[4]="ghcr.io/mailu/fetchmail:2024.06.36,ghcr.io/mailu/fetchmail:2024.06.37" image_update_map[5]="ghcr.io/mailu/nginx:2024.06.36,ghcr.io/mailu/nginx:2024.06.37" image_update_map[6]="ghcr.io/mailu/dovecot:2024.06.36,ghcr.io/mailu/dovecot:2024.06.37" image_update_map[7]="ghcr.io/mailu/oletools:2024.06.36,ghcr.io/mailu/oletools:2024.06.37" image_update_map[8]="ghcr.io/mailu/postfix:2024.06.36,ghcr.io/mailu/postfix:2024.06.37" image_update_map[9]="ghcr.io/mailu/unbound:2024.06.36,ghcr.io/mailu/unbound:2024.06.37" image_update_map[10]="ghcr.io/mailu/radicale:2024.06.36,ghcr.io/mailu/radicale:2024.06.37" image_update_map[11]="ghcr.io/mailu/webmail:2024.06.36,ghcr.io/mailu/webmail:2024.06.37" image_update_map[12]="apache/tika:3.1.0.0-full,mirror.gcr.io/apache/tika:3.2.2.0-full" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfMailuFixRedisCompose mfMailuV5PostUpdate perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v7 curImageList=mirror.gcr.io/redis:8.2.0-bookworm,ghcr.io/mailu/admin:2024.06.37,ghcr.io/mailu/rspamd:2024.06.37,mirror.gcr.io/clamav/clamav:1.4.3,ghcr.io/mailu/fetchmail:2024.06.37,ghcr.io/mailu/nginx:2024.06.37,ghcr.io/mailu/dovecot:2024.06.37,ghcr.io/mailu/oletools:2024.06.37,ghcr.io/mailu/postfix:2024.06.37,ghcr.io/mailu/unbound:2024.06.37,ghcr.io/mailu/radicale:2024.06.37,ghcr.io/mailu/webmail:2024.06.37,mirror.gcr.io/apache/tika:3.2.2.0-full image_update_map[0]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[1]="ghcr.io/mailu/admin:2024.06.37,ghcr.io/mailu/admin:2024.06.45" image_update_map[2]="ghcr.io/mailu/rspamd:2024.06.37,ghcr.io/mailu/rspamd:2024.06.45" image_update_map[3]="mirror.gcr.io/clamav/clamav:1.4.3,mirror.gcr.io/clamav/clamav:1.4.3-67" image_update_map[4]="ghcr.io/mailu/fetchmail:2024.06.37,ghcr.io/mailu/fetchmail:2024.06.45" image_update_map[5]="ghcr.io/mailu/nginx:2024.06.37,ghcr.io/mailu/nginx:2024.06.45" image_update_map[6]="ghcr.io/mailu/dovecot:2024.06.37,ghcr.io/mailu/dovecot:2024.06.45" image_update_map[7]="ghcr.io/mailu/oletools:2024.06.37,ghcr.io/mailu/oletools:2024.06.45" image_update_map[8]="ghcr.io/mailu/postfix:2024.06.37,ghcr.io/mailu/postfix:2024.06.45" image_update_map[9]="ghcr.io/mailu/unbound:2024.06.37,ghcr.io/mailu/unbound:2024.06.45" image_update_map[10]="ghcr.io/mailu/radicale:2024.06.37,ghcr.io/mailu/radicale:2024.06.45" image_update_map[11]="ghcr.io/mailu/webmail:2024.06.37,ghcr.io/mailu/webmail:2024.06.45" image_update_map[12]="mirror.gcr.io/apache/tika:3.2.2.0-full,mirror.gcr.io/apache/tika:3.2.3.0-full" ;; 7) newVer=v9 curImageList=mirror.gcr.io/redis:8.4.0-bookworm,ghcr.io/mailu/admin:2024.06.45,ghcr.io/mailu/rspamd:2024.06.45,mirror.gcr.io/clamav/clamav:1.4.3-67,ghcr.io/mailu/fetchmail:2024.06.45,ghcr.io/mailu/nginx:2024.06.45,ghcr.io/mailu/dovecot:2024.06.45,ghcr.io/mailu/oletools:2024.06.45,ghcr.io/mailu/postfix:2024.06.45,ghcr.io/mailu/unbound:2024.06.45,ghcr.io/mailu/radicale:2024.06.45,ghcr.io/mailu/webmail:2024.06.45,mirror.gcr.io/apache/tika:3.2.3.0-full image_update_map[0]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[1]="ghcr.io/mailu/admin:2024.06.45,ghcr.io/mailu/admin:2024.06.65" image_update_map[2]="ghcr.io/mailu/rspamd:2024.06.45,ghcr.io/mailu/rspamd:2024.06.65" image_update_map[3]="mirror.gcr.io/clamav/clamav:1.4.3-67,mirror.gcr.io/clamav/clamav:1.5.3" image_update_map[4]="ghcr.io/mailu/fetchmail:2024.06.45,ghcr.io/mailu/fetchmail:2024.06.65" image_update_map[5]="ghcr.io/mailu/nginx:2024.06.45,ghcr.io/mailu/nginx:2024.06.65" image_update_map[6]="ghcr.io/mailu/dovecot:2024.06.45,ghcr.io/mailu/dovecot:2024.06.65" image_update_map[7]="ghcr.io/mailu/oletools:2024.06.45,ghcr.io/mailu/oletools:2024.06.65" image_update_map[8]="ghcr.io/mailu/postfix:2024.06.45,ghcr.io/mailu/postfix:2024.06.65" image_update_map[9]="ghcr.io/mailu/unbound:2024.06.45,ghcr.io/mailu/unbound:2024.06.65" image_update_map[10]="ghcr.io/mailu/radicale:2024.06.45,ghcr.io/mailu/radicale:2024.06.65" image_update_map[11]="ghcr.io/mailu/webmail:2024.06.45,ghcr.io/mailu/webmail:2024.06.65" image_update_map[12]="mirror.gcr.io/apache/tika:3.2.3.0-full,mirror.gcr.io/apache/tika:3.2.3.0-full" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" mfMailuClearRedis return ;; 8) newVer=v9 curImageList=mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/mailu/admin:2024.06.54,ghcr.io/mailu/rspamd:2024.06.54,mirror.gcr.io/clamav/clamav:1.5.3,ghcr.io/mailu/fetchmail:2024.06.54,ghcr.io/mailu/nginx:2024.06.54,ghcr.io/mailu/dovecot:2024.06.54,ghcr.io/mailu/oletools:2024.06.54,ghcr.io/mailu/postfix:2024.06.54,ghcr.io/mailu/unbound:2024.06.54,ghcr.io/mailu/radicale:2024.06.54,ghcr.io/mailu/webmail:2024.06.54,mirror.gcr.io/apache/tika:3.2.3.0-full image_update_map[0]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[1]="ghcr.io/mailu/admin:2024.06.54,ghcr.io/mailu/admin:2024.06.65" image_update_map[2]="ghcr.io/mailu/rspamd:2024.06.54,ghcr.io/mailu/rspamd:2024.06.65" image_update_map[3]="mirror.gcr.io/clamav/clamav:1.5.3,mirror.gcr.io/clamav/clamav:1.5.3" image_update_map[4]="ghcr.io/mailu/fetchmail:2024.06.54,ghcr.io/mailu/fetchmail:2024.06.65" image_update_map[5]="ghcr.io/mailu/nginx:2024.06.54,ghcr.io/mailu/nginx:2024.06.65" image_update_map[6]="ghcr.io/mailu/dovecot:2024.06.54,ghcr.io/mailu/dovecot:2024.06.65" image_update_map[7]="ghcr.io/mailu/oletools:2024.06.54,ghcr.io/mailu/oletools:2024.06.65" image_update_map[8]="ghcr.io/mailu/postfix:2024.06.54,ghcr.io/mailu/postfix:2024.06.65" image_update_map[9]="ghcr.io/mailu/unbound:2024.06.54,ghcr.io/mailu/unbound:2024.06.65" image_update_map[10]="ghcr.io/mailu/radicale:2024.06.54,ghcr.io/mailu/radicale:2024.06.65" image_update_map[11]="ghcr.io/mailu/webmail:2024.06.54,ghcr.io/mailu/webmail:2024.06.65" image_update_map[12]="mirror.gcr.io/apache/tika:3.2.3.0-full,mirror.gcr.io/apache/tika:3.3.1.0-full" ;; 9) newVer=v9 curImageList=mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/mailu/admin:2024.06.65,ghcr.io/mailu/rspamd:2024.06.65,mirror.gcr.io/clamav/clamav:1.5.3,ghcr.io/mailu/fetchmail:2024.06.65,ghcr.io/mailu/nginx:2024.06.65,ghcr.io/mailu/dovecot:2024.06.65,ghcr.io/mailu/oletools:2024.06.65,ghcr.io/mailu/postfix:2024.06.65,ghcr.io/mailu/unbound:2024.06.65,ghcr.io/mailu/radicale:2024.06.65,ghcr.io/mailu/webmail:2024.06.65,mirror.gcr.io/apache/tika:3.3.1.0-full image_update_map[0]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[1]="ghcr.io/mailu/admin:2024.06.65,ghcr.io/mailu/admin:2024.06.65" image_update_map[2]="ghcr.io/mailu/rspamd:2024.06.65,ghcr.io/mailu/rspamd:2024.06.65" image_update_map[3]="mirror.gcr.io/clamav/clamav:1.5.3,mirror.gcr.io/clamav/clamav:1.5.3" image_update_map[4]="ghcr.io/mailu/fetchmail:2024.06.65,ghcr.io/mailu/fetchmail:2024.06.65" image_update_map[5]="ghcr.io/mailu/nginx:2024.06.65,ghcr.io/mailu/nginx:2024.06.65" image_update_map[6]="ghcr.io/mailu/dovecot:2024.06.65,ghcr.io/mailu/dovecot:2024.06.65" image_update_map[7]="ghcr.io/mailu/oletools:2024.06.65,ghcr.io/mailu/oletools:2024.06.65" image_update_map[8]="ghcr.io/mailu/postfix:2024.06.65,ghcr.io/mailu/postfix:2024.06.65" image_update_map[9]="ghcr.io/mailu/unbound:2024.06.65,ghcr.io/mailu/unbound:2024.06.65" image_update_map[10]="ghcr.io/mailu/radicale:2024.06.65,ghcr.io/mailu/radicale:2024.06.65" image_update_map[11]="ghcr.io/mailu/webmail:2024.06.65,ghcr.io/mailu/webmail:2024.06.65" image_update_map[12]="mirror.gcr.io/apache/tika:3.3.1.0-full,mirror.gcr.io/apache/tika:3.3.1.0-full" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function addUserMailu() { set +e add_type=$1 username=$2 domain=$3 password=$4 total_tries=5 num_tries=1 is_added=false while [ $num_tries -le $total_tries ] do if [ "$add_type" = "user" ]; then docker exec mailu-admin flask mailu config-export | yq -r '.user' | grep -q ${username}@${domain} if [ $? -eq 0 ]; then return 0 fi elif [ "$add_type" = "alias" ]; then docker exec mailu-admin flask mailu config-export | yq -r '.alias' | grep -q ${username}@${domain} if [ $? -eq 0 ]; then return 0 fi fi docker exec mailu-admin flask mailu $add_type $username $domain $password if [ $? -eq 0 ]; then is_added=true echo "[Mailu] Added $username" break fi sleep 1 echo "[Mailu] Error Adding $username, retry $num_tries of $total_tries" ((num_tries++)) done if [ "$is_added" = "false" ]; then echo "[Mailu] Error adding $username, exiting..." # Sleep here to ensure error gets logged sleep 3 exit 1 fi sleep 5 set -e } function createStandardMailuMailboxes() { csmm_email_address="$1" docker exec mailu-imap bash -c "doveadm mailbox create -u $csmm_email_address \"Consume\"" docker exec mailu-imap bash -c "doveadm mailbox subscribe -u $csmm_email_address \"Consume\"" docker exec mailu-imap bash -c "doveadm mailbox create -u $csmm_email_address \"Processed\"" docker exec mailu-imap bash -c "doveadm mailbox subscribe -u $csmm_email_address \"Processed\"" docker exec mailu-imap bash -c "doveadm mailbox create -u $csmm_email_address \"Processed.Personal\"" docker exec mailu-imap bash -c "doveadm mailbox subscribe -u $csmm_email_address \"Processed.Personal\"" docker exec mailu-imap bash -c "doveadm mailbox create -u $csmm_email_address \"Processed.Work\"" docker exec mailu-imap bash -c "doveadm mailbox subscribe -u $csmm_email_address \"Processed.Work\"" docker exec mailu-imap bash -c "doveadm mailbox create -u $csmm_email_address \"Processed.Uncategorized\"" docker exec mailu-imap bash -c "doveadm mailbox subscribe -u $csmm_email_address \"Processed.Uncategorized\"" docker exec mailu-imap bash -c "doveadm mailbox create -u $csmm_email_address \"Ignore\"" docker exec mailu-imap bash -c "doveadm mailbox subscribe -u $csmm_email_address \"Ignore\"" } function mfMailuV4Update() { rm -f $HOME/mailu-compose.yml outputMailuCompose outputMailuMultimap mkdir $HSHQ_STACKS_DIR/mailu/tmpdir sudo mv $HSHQ_STACKS_DIR/mailu/overrides/rspamd/* $HSHQ_STACKS_DIR/mailu/tmpdir/ sudo mkdir -p $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local sudo mkdir -p $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local sudo chown 101:101 $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override sudo rm -f $HSHQ_STACKS_DIR/mailu/tmpdir/multimap.conf sudo mv $HSHQ_STACKS_DIR/mailu/tmpdir/groups.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/override/ sudo mv $HSHQ_STACKS_DIR/mailu/tmpdir/* $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/ sudo rm -fr $HSHQ_STACKS_DIR/mailu/tmpdir sudo mv $HSHQ_STACKS_DIR/mailu/multimap.conf $HSHQ_STACKS_DIR/mailu/overrides/rspamd/local/multimap.conf sudo rm -f $HSHQ_STACKS_DIR/mailu/filter/bytecode.cld $HSHQ_STACKS_DIR/mailu/filter/bytecode.cvd $HSHQ_STACKS_DIR/mailu/filter/daily.cld $HSHQ_STACKS_DIR/mailu/filter/freshclam.dat $HSHQ_STACKS_DIR/mailu/filter/main.cvd mkdir $HSHQ_STACKS_DIR/mailu/clamav sudo chown 1000:1000 $HSHQ_STACKS_DIR/mailu/clamav sed -i "s|^LOG_LEVEL=.*|LOG_LEVEL=INFO|g" $HOME/mailu.env echo "FULL_TEXT_SEARCH=en" >> $HOME/mailu.env echo "API=true" >> $HOME/mailu.env echo "WEB_API=/api" >> $HOME/mailu.env echo "API_TOKEN=$MAILU_API_TOKEN" >> $HOME/mailu.env echo "PORTS=$CADDY_HTTP_PORT,$CADDY_HTTPS_PORT,4190,$MAILU_PORT_1,$MAILU_PORT_2,$MAILU_PORT_3,$MAILU_PORT_4,$MAILU_PORT_5,$MAILU_PORT_6,$MAILU_PORT_7" >> $HOME/mailu.env echo "FULL_TEXT_SEARCH_ATTACHMENTS=false" >> $HOME/mailu.env } function mfMailuV4PostUpdate() { waitForStack "Listening at: http://0.0.0.0:8080" mailu-admin docker exec mailu-imap doveadm fts rescan -A > /dev/null 2>&1 docker exec mailu-imap doveadm user '*'|while read u; do echo "re-indexing $u";docker exec mailu-imap doveadm index -u $u '*'; done > /dev/null 2>&1 } function mailuV111Bugfix() { # See https://github.com/Mailu/Mailu/issues/3673 if sudo test -d ${HSHQ_STACKS_DIR}/mailu/clamav; then return fi mailu_stack_id=$(getStackID mailu) mailu_compose=$HSHQ_STACKS_DIR/portainer/compose/$mailu_stack_id/docker-compose.yml mailu_stack_firstline=$(sudo sed -n 1p $mailu_compose) mailu_stack_ver=$(getVersionFromComposeLine "$mailu_stack_firstline") if [ "$mailu_stack_ver" = "1" ] || [ "$mailu_stack_ver" = "2" ] || [ "$mailu_stack_ver" = "3" ]; then return fi startStopStack mailu stop updateStackEnv mailu mfMailuV111Bugfix } function mfMailuV111Bugfix() { rm -f $HOME/mailu-compose.yml outputMailuCompose if sudo test -d $HSHQ_STACKS_DIR/mailu/filter/clamav; then sudo mv $HSHQ_STACKS_DIR/mailu/filter/clamav $HSHQ_STACKS_DIR/mailu/clamav else mkdir $HSHQ_STACKS_DIR/mailu/clamav fi sudo chown -R 1000:1000 $HSHQ_STACKS_DIR/mailu/clamav sudo rm -fr $HSHQ_STACKS_DIR/mailu/clamav/tmp* } function mfMailuFixRedisCompose() { lineStart=" redis:" lineEnd=" - ALLOW_EMPTY_PASSWORD=yes" replaceText="" replaceText=$replaceText">>redis:\n" replaceText=$replaceText">>>>image: mirror.gcr.io/redis:8.2.0-bookworm\n" replaceText=$replaceText">>>>container_name: mailu-redis\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--appendonly no\n" replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- dock-mailu-ext-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n" replaceTextBlockInFile "$lineStart" "$lineEnd" "$replaceText" $HOME/mailu-compose.yml false ">" cat -s $HOME/mailu-compose.yml > $HOME/ac.tmp mv $HOME/ac.tmp $HOME/mailu-compose.yml } function mfMailuV5PostUpdate() { echo "Waiting for mailu stack to initialize..." waitForStack "Listening at: http://0.0.0.0:8080" mailu-admin sleep 15 echo "Restarting mailu stack..." startStopStack mailu stop sleep 1 startStopStack mailu start } function fixMailuV6() { set +e mailu_stack_id=$(getStackID mailu) mailu_compose=$HSHQ_STACKS_DIR/portainer/compose/$mailu_stack_id/docker-compose.yml mailu_stack_firstline=$(sudo sed -n 1p $mailu_compose) mailu_stack_ver=$(getVersionFromComposeLine "$mailu_stack_firstline") sudo grep -q "mirror.gcr.io/clamav/clamav-debian:1.4.3" $mailu_compose rtVal=$? if ! [ "$mailu_stack_ver" = "6" ] || ! [ $rtVal -eq 0 ]; then return fi echo "Fixing mailu stack container images..." pullImage mirror.gcr.io/clamav/clamav:1.4.3 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi updateStackEnv mailu mfMailuV6Fix } function mfMailuV6Fix() { sed -i "s/mirror.gcr.io\/clamav\/clamav-debian:1.4.3/mirror.gcr.io\/clamav\/clamav:1.4.3/g" $HOME/mailu-compose.yml } function mfMailuV230Fix() { sed -i "s|SUBNET=.*|SUBNET=$NET_MAILU_EXT_SUBNET|g" $HOME/mailu.env sed -i "s|SUBNET_PREFIX=.*|SUBNET_PREFIX=$NET_MAILU_EXT_SUBNET_PREFIX|g" $HOME/mailu.env } function mfMailuClearRedis() { echo "Sleeping 10s..." # This is lazy, but it should be fine. sleep 10 echo "Stopping mailu stack..." startStopStack mailu stop sudo rm -fr $HSHQ_STACKS_DIR/mailu/redis/* echo "Starting mailu stack..." startStopStack mailu start } # Wazuh function installWazuh() { set +e is_integrate_hshq=$1 # Get/check contents of wazuh directory checkDeleteStackAndDirectory wazuh "Wazuh" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wazuh.dashboard) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wazuh.indexer) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wazuh.manager) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/wazuh mkdir $HSHQ_STACKS_DIR/wazuh/wazuh-cluster mkdir $HSHQ_STACKS_DIR/wazuh/wazuh-indexer mkdir $HSHQ_STACKS_DIR/wazuh/wazuh-dashboard mkdir $HSHQ_STACKS_DIR/wazuh/volumes mkdir $HSHQ_STACKS_DIR/wazuh/volumes/api-configuration mkdir $HSHQ_STACKS_DIR/wazuh/volumes/etc mkdir $HSHQ_STACKS_DIR/wazuh/volumes/var-multigroups mkdir $HSHQ_STACKS_DIR/wazuh/volumes/integrations mkdir $HSHQ_STACKS_DIR/wazuh/volumes/active-response mkdir $HSHQ_STACKS_DIR/wazuh/volumes/agentless mkdir $HSHQ_STACKS_DIR/wazuh/volumes/wodles mkdir $HSHQ_STACKS_DIR/wazuh/volumes/filebeat-etc mkdir $HSHQ_STACKS_DIR/wazuh/volumes/filebeat-var mkdir $HSHQ_STACKS_DIR/wazuh/volumes/indexer-data mkdir $HSHQ_NONBACKUP_DIR/wazuh mkdir $HSHQ_NONBACKUP_DIR/wazuh/volumes mkdir $HSHQ_NONBACKUP_DIR/wazuh/volumes/queue mkdir $HSHQ_NONBACKUP_DIR/wazuh/volumes/logs mkdir $HSHQ_NONBACKUP_DIR/wazuh/volumes/indexer-data initServicesCredentials outputConfigWazuh generateCert wazuh.manager "wazuh.manager" generateCert wazuh.indexer "wazuh.indexer" generateCert wazuh.admin "wazuh.admin" generateCert wazuh.dashboard "wazuh.dashboard" installStack wazuh wazuh.manager " " $HOME/wazuh.env retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Wazuh manager installed..." error_text="Fatal glibc error" maxACount=300 curACount=1 set +e while [ $curACount -lt $maxACount ] do if sudo test -f $HSHQ_STACKS_DIR/wazuh/volumes/etc/shared/default/agent.conf; then break fi finderror=$(docker logs wazuh.manager 2>&1 | grep "$error_text") if ! [ -z "$finderror" ]; then echo "This CPU does not support x86-64-v2, Wazuh cannot be installed, exiting..." checkDeleteStackAndDirectory wazuh "Wazuh" true true return 3 fi echo "Waiting for agent.conf to initialize ($curACount seconds)..." sleep 1 ((curACount++)) done sudo sed -i "s/.*Shared agent configuration here.*/\ \n \/var\/lib\/docker\/overlay2<\/ignore>\n <\/rootcheck>/" $HSHQ_STACKS_DIR/wazuh/volumes/etc/shared/default/agent.conf sudo tee -a $HSHQ_STACKS_DIR/wazuh/volumes/etc/rules/local_rules.xml >/dev/null < 650 alert_by_email firewall-drop add Host Blocked by firewall-drop Active Response active_response,pci_dss_11.4,gpg13_4.13,gdpr_IV_35.7.d,nist_800_53_SI.4,tsc_CC6.1,tsc_CC6.8,tsc_CC7.2,tsc_CC7.3,tsc_CC7.4, EOFRU sudo tee $HSHQ_STACKS_DIR/wazuh/volumes/etc/authd.pass >/dev/null <no<\/use_password>/yes<\/use_password>/" $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf sudo sed -i "s/run_as: true/run_as: false/" $HSHQ_STACKS_DIR/wazuh/wazuh-dashboard/wazuh.yml docker container restart wazuh.manager > /dev/null 2>&1 docker container restart wazuh.dashboard > /dev/null 2>&1 installWazuhAgent set -e inner_block="" inner_block=$inner_block">>https://$SUB_WAZUH.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://wazuh.dashboard:5601 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_WAZUH $MANAGETLS_WAZUH "$is_integrate_hshq" $NETDEFAULT_WAZUH "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll wazuh "$FMLNAME_WAZUH" $USERTYPE_WAZUH "https://$SUB_WAZUH.$HOMESERVER_DOMAIN" "wazuh.png" "$(getHeimdallOrderFromSub $SUB_WAZUH $USERTYPE_WAZUH)" restartAllCaddyContainers fi } function outputConfigWazuh() { cat < $HOME/wazuh-compose.yml $STACK_VERSION_PREFIX wazuh $(getScriptStackVersion wazuh) services: wazuh.manager: image: $(getScriptImageByContainerName wazuh.manager) container_name: wazuh.manager hostname: wazuh.manager restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true ulimits: memlock: soft: -1 hard: -1 nofile: soft: 655360 hard: 655360 networks: - int-wazuh-net - dock-ext-net - dock-internalmail-net ports: - "$WAZUH_PORT_1:$WAZUH_PORT_1" - "$WAZUH_PORT_2:$WAZUH_PORT_2" - "$WAZUH_PORT_3:$WAZUH_PORT_3" - "$WAZUH_PORT_4:$WAZUH_PORT_4" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/etc/ssl/root-ca.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.manager.crt:/etc/ssl/filebeat.pem - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.manager.key:/etc/ssl/filebeat.key - \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/wazuh-cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf - \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/local_internal_options.conf:/var/ossec/etc/local_internal_options.conf #- \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/manager_api.yaml:/var/ossec/api/configuration/api.yaml:ro - v-wazuh-api-configuration:/var/ossec/api/configuration - v-wazuh-etc:/var/ossec/etc - v-wazuh-logs:/var/ossec/logs - v-wazuh-queue:/var/ossec/queue - v-wazuh-var-multigroups:/var/ossec/var/multigroups - v-wazuh-integrations:/var/ossec/integrations - v-wazuh-active-response:/var/ossec/active-response/bin - v-wazuh-agentless:/var/ossec/agentless - v-wazuh-wodles:/var/ossec/wodles - v-wazuh-filebeat-etc:/etc/filebeat - v-wazuh-filebeat-var:/var/lib/filebeat wazuh.indexer: image: $(getScriptImageByContainerName wazuh.indexer) container_name: wazuh.indexer hostname: wazuh.indexer restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-wazuh-net - dock-ext-net ports: - "$WAZUH_PORT_5:$WAZUH_PORT_5" ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/usr/share/wazuh-indexer/config/certs/root-ca.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.indexer.crt:/usr/share/wazuh-indexer/config/certs/wazuh.indexer.pem - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.indexer.key:/usr/share/wazuh-indexer/config/certs/wazuh.indexer.key - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.admin.crt:/usr/share/wazuh-indexer/config/certs/admin.pem - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.admin.key:/usr/share/wazuh-indexer/config/certs/admin-key.pem - \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/wazuh-indexer/wazuh_indexer.yml:/usr/share/wazuh-indexer/config/opensearch.yml - \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/wazuh-indexer/internal_users.yml:/usr/share/wazuh-indexer/config/opensearch-security/internal_users.yml - v-wazuh-indexer-data:/var/lib/wazuh-indexer wazuh.dashboard: image: $(getScriptImageByContainerName wazuh.dashboard) container_name: wazuh.dashboard hostname: wazuh.dashboard restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - wazuh.indexer networks: - int-wazuh-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/usr/share/wazuh-dashboard/certs/root-ca.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.dashboard.crt:/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem - \${PORTAINER_HSHQ_SSL_DIR}/wazuh.dashboard.key:/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem - \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/wazuh-dashboard/opensearch_dashboards.yml:/usr/share/wazuh-dashboard/config/opensearch_dashboards.yml - \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/wazuh-dashboard/wazuh.yml:/usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml volumes: v-wazuh-api-configuration: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/api-configuration v-wazuh-etc: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/etc v-wazuh-logs: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/wazuh/volumes/logs v-wazuh-queue: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/wazuh/volumes/queue v-wazuh-var-multigroups: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/var-multigroups v-wazuh-integrations: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/integrations v-wazuh-active-response: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/active-response v-wazuh-agentless: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/agentless v-wazuh-wodles: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/wodles v-wazuh-filebeat-etc: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/filebeat-etc v-wazuh-filebeat-var: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wazuh/volumes/filebeat-var v-wazuh-indexer-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/wazuh/volumes/indexer-data networks: dock-internalmail-net: name: dock-internalmail external: true dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true int-wazuh-net: driver: bridge internal: true ipam: driver: default EOFWZ cat < $HOME/wazuh.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID INDEXER_URL=https://wazuh.indexer:9200 INDEXER_USERNAME=$WAZUH_USERS_ADMIN_USERNAME INDEXER_PASSWORD=$WAZUH_USERS_ADMIN_PASSWORD FILEBEAT_SSL_VERIFICATION_MODE=full SSL_CERTIFICATE_AUTHORITIES=/etc/ssl/root-ca.pem SSL_CERTIFICATE=/etc/ssl/filebeat.pem SSL_KEY=/etc/ssl/filebeat.key API_USERNAME=$WAZUH_API_USERNAME API_PASSWORD=$WAZUH_API_PASSWORD OPENSEARCH_JAVA_OPTS=-Xms4g -Xmx4g WAZUH_API_URL=https://wazuh.manager DASHBOARD_USERNAME=$WAZUH_USERS_DASHBOARD_USERNAME DASHBOARD_PASSWORD=$WAZUH_USERS_DASHBOARD_PASSWORD WAZUH_ALLOW_RUN_AS=false EOFWZ cat < $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf yes yes no no yes $SMTP_HOSTNAME $EMAIL_ADMIN_EMAIL_ADDRESS $EMAIL_ADMIN_EMAIL_ADDRESS 12 alerts.log 10m 0 3 12 plain secure $WAZUH_PORT_1 tcp 131072 no yes yes yes yes yes yes yes 43200 etc/rootcheck/rootkit_files.txt etc/rootcheck/rootkit_trojans.txt yes no 1800 1d yes wodles/java wodles/ciscat 1m 5 yes no yes yes /var/log/osquery/osqueryd.results.log /etc/osquery/osquery.conf yes no 1h yes yes yes yes yes yes yes 10 yes yes 12h yes yes yes 60m yes https://wazuh.indexer:9200 /etc/ssl/root-ca.pem /etc/ssl/filebeat.pem /etc/ssl/filebeat.key no 43200 yes yes no /etc,/usr/bin,/usr/sbin /bin,/sbin,/boot /home /etc/mtab /etc/hosts.deny /etc/mail/statistics /etc/random-seed /etc/random.seed /etc/adjtime /etc/httpd/logs /etc/utmpx /etc/wtmpx /etc/cups/certs /etc/dumpdates /etc/svc/volatile .log\$|.swp\$ /etc/ssl/private.key yes yes yes yes 10 100 yes 5m 1h 10 127.0.0.1 ^localhost.localdomain\$ disable-account disable-account yes restart-wazuh restart-wazuh firewall-drop firewall-drop yes host-deny host-deny yes route-null route-null yes win_route-null route-null.exe yes netsh netsh.exe yes command df -P 360 full_command netstat -tulpn | sed 's/\([[:alnum:]]\+\)\ \+[[:digit:]]\+\ \+[[:digit:]]\+\ \+\(.*\):\([[:digit:]]*\)\ \+\([0-9\.\:\*]\+\).\+\ \([[:digit:]]*\/[[:alnum:]\-]*\).*/\1 \2 == \3 == \4 \5/' | sort -k 4 -g | sed 's/ == \(.*\) ==/:\1/' | sed 1,2d netstat listening ports 360 full_command last -n 20 360 /var/log/suricata/eve.json json ruleset/decoders ruleset/rules 0215-policy_rules.xml etc/lists/audit-keys etc/lists/amazon/aws-eventnames etc/lists/security-eventchannel etc/decoders etc/rules yes 1 64 15m no $WAZUH_PORT_2 no yes yes HIGH:!ADH:!EXP:!MD5:!RC4:!3DES:!CAMELLIA:@STRENGTH no etc/sslmanager.cert etc/sslmanager.key no wazuh node01 master 1516 0.0.0.0 NODE_IP no yes syslog /var/ossec/logs/active-responses.log firewall-drop local 5763 300 EOFWZ cat < $HSHQ_STACKS_DIR/wazuh/wazuh-indexer/wazuh_indexer.yml network.host: "0.0.0.0" node.name: "wazuh.indexer" path.data: /var/lib/wazuh-indexer path.logs: /var/log/wazuh-indexer discovery.type: single-node compatibility.override_main_response_version: true plugins.security.ssl.http.pemcert_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.pem plugins.security.ssl.http.pemkey_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.key plugins.security.ssl.http.pemtrustedcas_filepath: /usr/share/wazuh-indexer/config/certs/root-ca.pem plugins.security.ssl.transport.pemcert_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.pem plugins.security.ssl.transport.pemkey_filepath: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.key plugins.security.ssl.transport.pemtrustedcas_filepath: /usr/share/wazuh-indexer/config/certs/root-ca.pem plugins.security.ssl.http.enabled: true plugins.security.ssl.transport.enforce_hostname_verification: false plugins.security.ssl.transport.resolve_hostname: false plugins.security.authcz.admin_dn: - "CN=wazuh.admin,OU=$CERTS_INTERNAL_OU_NAME,O=$CERTS_INTERNAL_OU_NAME,L=$CERTS_INTERNAL_LOCALITY,ST=$CERTS_INTERNAL_STATE,C=$CERTS_INTERNAL_COUNTRY" plugins.security.check_snapshot_restore_write_privileges: true plugins.security.enable_snapshot_restore_privilege: true plugins.security.nodes_dn: - "CN=wazuh.manager,OU=$CERTS_INTERNAL_OU_NAME,O=$CERTS_INTERNAL_OU_NAME,L=$CERTS_INTERNAL_LOCALITY,ST=$CERTS_INTERNAL_STATE,C=$CERTS_INTERNAL_COUNTRY" plugins.security.restapi.roles_enabled: - "all_access" - "security_rest_api_access" plugins.security.system_indices.enabled: true plugins.security.system_indices.indices: [".opendistro-alerting-config", ".opendistro-alerting-alert*", ".opendistro-anomaly-results*", ".opendistro-anomaly-detector*", ".opendistro-anomaly-checkpoints", ".opendistro-anomaly-detection-state", ".opendistro-reports-*", ".opendistro-notifications-*", ".opendistro-notebooks", ".opensearch-observability", ".opendistro-asynchronous-search-response*", ".replication-metadata-store"] plugins.security.allow_default_init_securityindex: true cluster.routing.allocation.disk.threshold_enabled: false EOFWZ cat < $HSHQ_STACKS_DIR/wazuh/local_internal_options.conf analysisd.decoder_order_size=1024 EOFWZ cat < $HSHQ_STACKS_DIR/wazuh/manager_api.yaml users: wazuh-wui: allow_run_as: true EOFWZ WAZUH_USERS_ADMIN_PASSWORD_HASH=$(htpasswd -B -n -b $WAZUH_USERS_ADMIN_USERNAME $WAZUH_USERS_ADMIN_PASSWORD | cut -d":" -f2-) WAZUH_USERS_DASHBOARD_PASSWORD_HASH=$(htpasswd -B -n -b kibanaserver $WAZUH_USERS_DASHBOARD_PASSWORD | cut -d":" -f2-) WAZUH_USERS_KIBANARO_PASSWORD_HASH=$(htpasswd -B -n -b kibanaro $WAZUH_USERS_KIBANARO_PASSWORD | cut -d":" -f2-) WAZUH_USERS_LOGSTASH_PASSWORD_HASH=$(htpasswd -B -n -b logstash $WAZUH_USERS_LOGSTASH_PASSWORD | cut -d":" -f2-) WAZUH_USERS_READALL_PASSWORD_HASH=$(htpasswd -B -n -b readall $WAZUH_USERS_READALL_PASSWORD | cut -d":" -f2-) WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD_HASH=$(htpasswd -B -n -b snapshotrestore $WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD | cut -d":" -f2-) cat < $HSHQ_STACKS_DIR/wazuh/wazuh-indexer/internal_users.yml --- # This is the internal user database # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh _meta: type: "internalusers" config_version: 2 # Define your internal users here ## Demo users $WAZUH_USERS_ADMIN_USERNAME: hash: "$WAZUH_USERS_ADMIN_PASSWORD_HASH" reserved: true backend_roles: - "admin" description: "Admin user" kibanaserver: hash: "$WAZUH_USERS_DASHBOARD_PASSWORD_HASH" reserved: true description: "KibanaServer user" kibanaro: hash: "$WAZUH_USERS_KIBANARO_PASSWORD_HASH" reserved: false backend_roles: - "kibanauser" - "readall" attributes: attribute1: "value1" attribute2: "value2" attribute3: "value3" description: "Kibanaro user" logstash: hash: "$WAZUH_USERS_LOGSTASH_PASSWORD_HASH" reserved: false backend_roles: - "logstash" description: "Logstash user" readall: hash: "$WAZUH_USERS_READALL_PASSWORD_HASH" reserved: false backend_roles: - "readall" description: "Readall user" snapshotrestore: hash: "$WAZUH_USERS_SNAPSHOTRESTORE_PASSWORD_HASH" reserved: false backend_roles: - "snapshotrestore" description: "Snapshotrestore user" EOFWZ outputWazuhDashboardConfig touch $HSHQ_STACKS_DIR/wazuh/wazuh-dashboard/wazuh.yml } function outputWazuhDashboardConfig() { rm -f $HSHQ_STACKS_DIR/wazuh/wazuh-dashboard/opensearch_dashboards.yml cat < $HSHQ_STACKS_DIR/wazuh/wazuh-dashboard/opensearch_dashboards.yml server.host: 0.0.0.0 server.port: 5601 opensearch.hosts: https://wazuh.indexer:9200 opensearch.ssl.verificationMode: certificate opensearch.requestHeadersWhitelist: ["securitytenant","Authorization"] opensearch_security.multitenancy.enabled: false opensearch_security.readonly_mode.roles: ["kibana_read_only"] server.ssl.enabled: true server.ssl.key: "/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem" server.ssl.certificate: "/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem" opensearch.ssl.certificateAuthorities: ["/usr/share/wazuh-dashboard/certs/root-ca.pem"] uiSettings.overrides.defaultRoute: /app/wz-home EOFWZ } function performUpdateWazuh() { perform_stack_name=wazuh prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=wazuh/wazuh-manager:4.6.0,wazuh/wazuh-indexer:4.6.0,wazuh/wazuh-dashboard:4.6.0 image_update_map[0]="wazuh/wazuh-manager:4.6.0,wazuh/wazuh-manager:4.7.3" image_update_map[1]="wazuh/wazuh-indexer:4.6.0,wazuh/wazuh-indexer:4.7.3" image_update_map[2]="wazuh/wazuh-dashboard:4.6.0,wazuh/wazuh-dashboard:4.7.3" ;; 2) newVer=v4 curImageList=wazuh/wazuh-manager:4.7.1,wazuh/wazuh-indexer:4.7.1,wazuh/wazuh-dashboard:4.7.1 image_update_map[0]="wazuh/wazuh-manager:4.7.1,wazuh/wazuh-manager:4.7.3" image_update_map[1]="wazuh/wazuh-indexer:4.7.1,wazuh/wazuh-indexer:4.7.3" image_update_map[2]="wazuh/wazuh-dashboard:4.7.1,wazuh/wazuh-dashboard:4.7.3" ;; 3) newVer=v4 curImageList=wazuh/wazuh-manager:4.7.2,wazuh/wazuh-indexer:4.7.2,wazuh/wazuh-dashboard:4.7.2 image_update_map[0]="wazuh/wazuh-manager:4.7.2,wazuh/wazuh-manager:4.7.3" image_update_map[1]="wazuh/wazuh-indexer:4.7.2,wazuh/wazuh-indexer:4.7.3" image_update_map[2]="wazuh/wazuh-dashboard:4.7.2,wazuh/wazuh-dashboard:4.7.3" ;; 4) # The only purpose of this stack upgrade is to implement the increased Java memory fix newVer=v5 curImageList=wazuh/wazuh-manager:4.7.3,wazuh/wazuh-indexer:4.7.3,wazuh/wazuh-dashboard:4.7.3 image_update_map[0]="wazuh/wazuh-manager:4.7.3,wazuh/wazuh-manager:4.7.3" image_update_map[1]="wazuh/wazuh-indexer:4.7.3,wazuh/wazuh-indexer:4.7.3" image_update_map[2]="wazuh/wazuh-dashboard:4.7.3,wazuh/wazuh-dashboard:4.7.3" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateWazuhStackJavaMem perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v6 curImageList=wazuh/wazuh-manager:4.7.3,wazuh/wazuh-indexer:4.7.3,wazuh/wazuh-dashboard:4.7.3 image_update_map[0]="wazuh/wazuh-manager:4.7.3,wazuh/wazuh-manager:4.9.1" image_update_map[1]="wazuh/wazuh-indexer:4.7.3,wazuh/wazuh-indexer:4.9.1" image_update_map[2]="wazuh/wazuh-dashboard:4.7.3,wazuh/wazuh-dashboard:4.9.1" updateWazuhAgents "4.9.1-1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" outputWazuhDashboardConfig false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v7 curImageList=wazuh/wazuh-manager:4.9.1,wazuh/wazuh-indexer:4.9.1,wazuh/wazuh-dashboard:4.9.1 image_update_map[0]="wazuh/wazuh-manager:4.9.1,wazuh/wazuh-manager:4.11.2" image_update_map[1]="wazuh/wazuh-indexer:4.9.1,wazuh/wazuh-indexer:4.11.2" image_update_map[2]="wazuh/wazuh-dashboard:4.9.1,wazuh/wazuh-dashboard:4.11.2" updateWazuhAgents "4.11.2-1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" version412WazuhUpdate false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v7 curImageList=wazuh/wazuh-manager:4.11.2,wazuh/wazuh-indexer:4.11.2,wazuh/wazuh-dashboard:4.11.2 image_update_map[0]="wazuh/wazuh-manager:4.11.2,mirror.gcr.io/wazuh/wazuh-manager:4.14.7" image_update_map[1]="wazuh/wazuh-indexer:4.11.2,mirror.gcr.io/wazuh/wazuh-indexer:4.14.7" image_update_map[2]="wazuh/wazuh-dashboard:4.11.2,mirror.gcr.io/wazuh/wazuh-dashboard:4.14.7" updateWazuhAgents "4.14.7-1" ;; 8) newVer=v8 curImageList=mirror.gcr.io/wazuh/wazuh-manager:4.14.7,mirror.gcr.io/wazuh/wazuh-indexer:4.14.7,mirror.gcr.io/wazuh/wazuh-dashboard:4.14.7 image_update_map[0]="mirror.gcr.io/wazuh/wazuh-manager:4.14.7,mirror.gcr.io/wazuh/wazuh-manager:4.14.7" image_update_map[1]="mirror.gcr.io/wazuh/wazuh-indexer:4.14.7,mirror.gcr.io/wazuh/wazuh-indexer:4.14.7" image_update_map[2]="mirror.gcr.io/wazuh/wazuh-dashboard:4.14.7,mirror.gcr.io/wazuh/wazuh-dashboard:4.14.7" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfUpdateWazuhStackJavaMem() { sed -i "s|OPENSEARCH_JAVA_OPTS=.*|OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g|" $HOME/wazuh.env } function installWazuhAgent() { if [ -z "$SUB_WAZUH" ] || [ -z "$HOMESERVER_DOMAIN" ] || [ -z "$WAZUH_AGENT_VERSION" ] || [ -z "$WAZUH_MANAGER_AUTH_PASSWORD" ]; then echo "Fatal error: SUB_WAZUH: $SUB_WAZUH, HOMESERVER_DOMAIN: $HOMESERVER_DOMAIN, WAZUH_AGENT_VERSION: $WAZUH_AGENT_VERSION" exit 2 fi curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | sudo gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import && sudo chmod 644 /usr/share/keyrings/wazuh.gpg echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" | sudo tee /etc/apt/sources.list.d/wazuh.list sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo WAZUH_MANAGER="$SUB_WAZUH.$HOMESERVER_DOMAIN" DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' wazuh-agent=$WAZUH_AGENT_VERSION sudo apt-mark hold wazuh-agent sudo systemctl daemon-reload set +e sudo grep "/var/log/docker/" /var/ossec/etc/ossec.conf > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo sed -i "/<\/ossec_config>/{s// \n syslog<\/log_format>\n \/var\/log\/docker\/*<\/location>\n <\/localfile>\n\n<\/ossec_config>/;:p;n;bp}" /var/ossec/etc/ossec.conf fi sudo tee /var/ossec/etc/authd.pass >/dev/null < /dev/null 2>&1 sudo systemctl daemon-reload > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' wazuh-agent=$agent_ver sudo apt-mark hold wazuh-agent if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then rm -f $HOME/$RS_UPDATE_SCRIPT_NAME cat < $HOME/$RS_UPDATE_SCRIPT_NAME #!/bin/bash set +e function main() { read -r -s -p "" rspw echo "\$rspw" | sudo -S -v -p "" > /dev/null 2>&1 sudo apt-mark unhold wazuh-agent sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' wazuh-agent=$agent_ver > /dev/null 2>&1 sudo apt-mark hold wazuh-agent } main "\$@" EOFLO updateRelayServerWithScript false fi } function removeWazuhAgent() { echo "Removing any prior wazuh agent installation..." sudo systemctl daemon-reload > /dev/null 2>&1 sudo systemctl stop wazuh-agent > /dev/null 2>&1 sudo systemctl disable wazuh-agent > /dev/null 2>&1 sudo DEBIAN_FRONTEND=noninteractive apt remove --purge wazuh-agent -y --allow-change-held-packages > /dev/null 2>&1 sudo apt-mark unhold wazuh-agent > /dev/null 2>&1 sudo systemctl daemon-reload > /dev/null 2>&1 } function version91WazuhUpdate() { vnwu_curE=${-//[^e]/} set +e grep "5763" $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo tee -a $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf >/dev/null < firewall-drop local 5763 300 EOFWZ fi grep "5763" $HSHQ_STACKS_DIR/wazuh/volumes/etc/ossec.conf > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo tee -a $HSHQ_STACKS_DIR/wazuh/volumes/etc/ossec.conf >/dev/null < firewall-drop local 5763 300 EOFWZ fi grep "firewall-drop" $HSHQ_STACKS_DIR/wazuh/volumes/etc/rules/local_rules.xml > /dev/null 2>&1 if [ $? -ne 0 ]; then sudo tee -a $HSHQ_STACKS_DIR/wazuh/volumes/etc/rules/local_rules.xml >/dev/null < 650 alert_by_email firewall-drop add Host Blocked by firewall-drop Active Response active_response,pci_dss_11.4,gpg13_4.13,gdpr_IV_35.7.d,nist_800_53_SI.4,tsc_CC6.1,tsc_CC6.8,tsc_CC7.2,tsc_CC7.3,tsc_CC7.4, EOFRU fi echo "Restarting Wazuh manager, please wait..." docker exec -it wazuh.manager service wazuh-manager restart > /dev/null 2>&1 set +e if ! [ -z "$vnwu_curE" ]; then set -e fi } function version412WazuhUpdate() { set +e grep -q "" $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf if [ $? -ne 0 ]; then wazblock=$( cat < yes https://wazuh.indexer:9200 /etc/ssl/root-ca.pem /etc/ssl/filebeat.pem /etc/ssl/filebeat.key EOFWZ ) all_text="$(cat $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf)" block_match="" echo -e "${all_text%%$block_match*}${wazblock}${all_text##*$block_match}" | sudo tee $HSHQ_STACKS_DIR/wazuh/wazuh-cluster/wazuh_manager.conf fi } # Collabora function installCollabora() { set +e is_integrate_hshq=$1 # And don't forget to check if directory exists checkDeleteStackAndDirectory collabora "Collabora" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi mkdir $HSHQ_STACKS_DIR/collabora pullImage $(getScriptImageByContainerName collabora) if [ $? -ne 0 ]; then return 1 fi set -e initServicesCredentials outputConfigCollabora installStack collabora collabora " " $HOME/collabora.env retval=$? if [ $retval -ne 0 ]; then return $retval fi inner_block="" inner_block=$inner_block">>https://$SUB_COLLABORA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>encode gzip\n" inner_block=$inner_block">>>>@is_css path *.css\n" inner_block=$inner_block">>>>header @is_css Content-Type \"text/css\"\n" inner_block=$inner_block">>>>@is_js path *.js\n" inner_block=$inner_block">>>>header @is_js Content-Type \"application/javascript\"\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://collabora:9980 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_COLLABORA $MANAGETLS_COLLABORA "$is_integrate_hshq" $NETDEFAULT_COLLABORA "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcUptimeKuma collabora "$FMLNAME_COLLABORA" $USERTYPE_COLLABORA "https://$SUB_COLLABORA.$HOMESERVER_DOMAIN" true insertEnableSvcHeimdall collabora "$FMLNAME_COLLABORA Admin" admin "https://$SUB_COLLABORA.$HOMESERVER_DOMAIN/browser/dist/admin/admin.html" "collabora.png" true "$(getHeimdallOrderFromSub $SUB_COLLABORA admin)" restartAllCaddyContainers fi } function outputConfigCollabora() { cat < $HOME/collabora-compose.yml $STACK_VERSION_PREFIX collabora $(getScriptStackVersion collabora) services: collabora: image: $(getScriptImageByContainerName collabora) container_name: collabora hostname: collabora restart: unless-stopped env_file: stack.env cap_add: - MKNOD - SYS_ADMIN networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFCO cat < $HOME/collabora.env username=$COLLABORA_ADMIN_USERNAME password=$COLLABORA_ADMIN_PASSWORD aliasgroup1=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN:443 extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:net.frame_ancestors=* EOFCO } function performUpdateCollabora() { perform_stack_name=collabora prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v8 curImageList=collabora/code:23.05.5.3.1 image_update_map[0]="collabora/code:23.05.5.3.1,mirror.gcr.io/collabora/code:25.04.4.2.1" ;; 2) newVer=v8 curImageList=collabora/code:23.05.6.4.1 image_update_map[0]="collabora/code:23.05.6.4.1,mirror.gcr.io/collabora/code:25.04.4.2.1" ;; 3) newVer=v8 curImageList=collabora/code:23.05.8.2.1 image_update_map[0]="collabora/code:23.05.8.2.1,mirror.gcr.io/collabora/code:25.04.4.2.1" ;; 4) newVer=v8 curImageList=collabora/code:23.05.9.4.1 image_update_map[0]="collabora/code:23.05.9.4.1,mirror.gcr.io/collabora/code:25.04.4.2.1" ;; 5) newVer=v8 curImageList=collabora/code:24.04.5.2.1 image_update_map[0]="collabora/code:24.04.5.2.1,mirror.gcr.io/collabora/code:25.04.4.2.1" ;; 6) newVer=v8 curImageList=collabora/code:24.04.9.1.1 image_update_map[0]="collabora/code:24.04.9.1.1,mirror.gcr.io/collabora/code:25.04.4.2.1" ;; 7) newVer=v8 curImageList=collabora/code:24.04.13.2.1 image_update_map[0]="collabora/code:24.04.13.2.1,mirror.gcr.io/collabora/code:25.04.4.2.1" ;; 8) newVer=v10 curImageList=mirror.gcr.io/collabora/code:25.04.4.2.1 image_update_map[0]="mirror.gcr.io/collabora/code:25.04.4.2.1,mirror.gcr.io/collabora/code:26.04.2.1.1" ;; 9) newVer=v10 curImageList=mirror.gcr.io/collabora/code:25.04.7.3.1 image_update_map[0]="mirror.gcr.io/collabora/code:25.04.7.3.1,mirror.gcr.io/collabora/code:26.04.2.1.1" ;; 10) newVer=v10 curImageList=mirror.gcr.io/collabora/code:26.04.2.1.1 image_update_map[0]="mirror.gcr.io/collabora/code:26.04.2.1.1,mirror.gcr.io/collabora/code:26.04.2.1.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Nextcloud function installNextcloud() { set +e is_integrate_hshq=$1 # With great power comes great responsibility checkDeleteStackAndDirectory nextcloud "Nextcloud" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-web) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-imaginary) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-push) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-talkhpb) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-talkrecord) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-harp) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName nextcloud-mcp) if [ $? -ne 0 ]; then return 1 fi if ! [ -d $HSHQ_STACKS_DIR/coturn ]; then echo "Missing coturn, installing..." installCoturn retval=$? if [ $retval -ne 0 ]; then notifyStackInstallFailure coturn fi fi stack_name=nextcloud set -e initServicesCredentials if [ -z "$NEXTCLOUD_REDIS_PASSWORD" ]; then NEXTCLOUD_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar NEXTCLOUD_REDIS_PASSWORD $NEXTCLOUD_REDIS_PASSWORD fi set +e addUserMailu alias $NEXTCLOUD_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS generateCert nextcloud-app nextcloud-app generateCert nextcloud-cron nextcloud-cron generateCert nextcloud-web nextcloud-web generateCert nextcloud-push nextcloud-push numTries=5 curTries=1 while [ $curTries -le $numTries ] do mkdir $HSHQ_STACKS_DIR/nextcloud mkdir $HSHQ_STACKS_DIR/nextcloud/app mkdir $HSHQ_STACKS_DIR/nextcloud/db mkdir $HSHQ_STACKS_DIR/nextcloud/dbexport mkdir $HSHQ_STACKS_DIR/nextcloud/web mkdir $HSHQ_STACKS_DIR/nextcloud/ssl mkdir $HSHQ_STACKS_DIR/nextcloud/record mkdir $HSHQ_STACKS_DIR/nextcloud/mcp mkdir $HSHQ_STACKS_DIR/nextcloud/mcp/data mkdir $HSHQ_STACKS_DIR/nextcloud/mcp/oauth chmod 777 $HSHQ_STACKS_DIR/nextcloud/dbexport outputConfigNextcloud cd ~ docker compose -f $HOME/nextcloud-compose-tmp.yml up -d search="ready to handle connections" error_text="rsync error" isFound=false isError=false i=0 set +e while [ $i -le 900 ] do findtext=$(docker logs nextcloud-app 2>&1 | grep "$search") finderror=$(docker logs nextcloud-app 2>&1 | grep "$error_text") if ! [ -z "$finderror" ]; then isError=true break fi if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 15 seconds, total wait=$i seconds..." sleep 15 i=$((i+15)) done if [ "$isFound" = "true" ] && [ "$isError" = "false" ]; then break fi if [ "$isError" = "true" ]; then echo "(Attempt $curTries of $numTries) Error starting Nextcloud stack, restarting..." fi cd ~ docker compose -f $HOME/nextcloud-compose-tmp.yml down -v sudo rm -fr $HSHQ_STACKS_DIR/nextcloud ((curTries++)) done if ! [ "$isFound" = "true" ]; then cd ~ docker compose -f $HOME/nextcloud-compose-tmp.yml down -v performNextcloudInstallFailCleanup echo "ERROR: Nextcloud unknown installation error, exiting..." return 2 fi # Regarding the next section, see the following: # https://github.com/nextcloud/server/issues/14482 # https://github.com/nextcloud/server/issues/14926 # https://github.com/nextcloud/server/issues/40082 # https://github.com/nextcloud/server/issues/45162 # These are just a few, but there are more related. # It doesn't appear as if they will ever fix this, # so certain measures have been put in place to # increase the success rate of installation. if sudo test -f $HSHQ_STACKS_DIR/nextcloud/app/lib/private/App/AppStore/Fetcher/Fetcher.php; then sudo sed -i "s/'timeout' => 60/'timeout' => 600/g" $HSHQ_STACKS_DIR/nextcloud/app/lib/private/App/AppStore/Fetcher/Fetcher.php > /dev/null 2>&1 fi echo "Sleeping 10 seconds..." sleep 10 set +e # Check to see if apps.json has downloaded numTries=30 curTries=1 isFound=false while [ $curTries -le $numTries ] do sudo find $HSHQ_STACKS_DIR/nextcloud/app/data -name apps.json | grep . > /dev/null 2>&1 if [ $? -eq 0 ]; then isFound=true break fi if [ $curTries -eq 1 ]; then echo "Issue with apps.json manifest, restarting nextcloud-app and nextcloud-cron containers..." docker container restart nextcloud-app > /dev/null 2>&1 docker container restart nextcloud-cron > /dev/null 2>&1 sleep 10 echo "Calling occ app:update --all, this may take a few minutes, please be patient..." docker exec -u www-data nextcloud-app php occ app:update --all > /dev/null 2>&1 fi echo "Nextcloud apps.json manifest not present, attempt ($curTries of $numTries) retrying in 10 seconds..." sleep 10 ((curTries++)) done if [ "$isFound" = "false" ]; then cd ~ docker compose -f $HOME/nextcloud-compose-tmp.yml down -v performNextcloudInstallFailCleanup echo "ERROR: Nextcloud could not obtain apps manifest, exiting..." return 3 fi ls /usr/local/share/ca-certificates/ | while read cert do docker exec -u www-data nextcloud-app php occ --no-warnings security:certificates:import /usr/local/share/ca-certificates/$cert done set +e if [ -f $HOME/nc_apps.tar.gz ]; then sudo tar xvzf $HOME/nc_apps.tar.gz > /dev/null 2>&1 sudo chown -R 82:82 $HOME/custom_apps sudo mv $HOME/custom_apps/* $HOME/hshq/data/stacks/nextcloud/app/custom_apps/ sudo rm -f $HOME/nc_apps.tar.gz sudo rm -fr $HOME/custom_apps fi docker exec -u www-data nextcloud-app php occ --no-warnings config:system:set default_phone_region --type string --value="$DEFAULT_PHONE_REGION" docker exec -u www-data nextcloud-app php occ --no-warnings config:system:set allow_local_remote_servers --value=true docker exec -u www-data nextcloud-app php occ --no-warnings app:install calendar docker exec -u www-data nextcloud-app php occ --no-warnings app:enable calendar # Sometimes there are issues with apps, check the first one to see if there is an error. docker exec -u www-data nextcloud-app php occ --no-warnings app:list --enabled --disabled | grep -iq "calendar" if [ $? -ne 0 ]; then cd ~ docker compose -f $HOME/nextcloud-compose-tmp.yml down -v performNextcloudInstallFailCleanup echo "ERROR: Nextcloud apps did not install correctly, exiting..." return 4 fi docker exec -u www-data nextcloud-app php occ user:setting $NEXTCLOUD_ADMIN_USERNAME settings email "$NEXTCLOUD_ADMIN_EMAIL_ADDRESS" docker exec -u www-data nextcloud-app php occ user:setting $NEXTCLOUD_ADMIN_USERNAME settings display_name "${HOMESERVER_ABBREV^^} Nextcloud Admin" docker exec -u www-data nextcloud-app php occ --no-warnings app:install bruteforcesettings docker exec -u www-data nextcloud-app php occ --no-warnings app:enable bruteforcesettings docker exec -u www-data nextcloud-app php occ --no-warnings app:install contacts docker exec -u www-data nextcloud-app php occ --no-warnings app:enable contacts #docker exec -u www-data nextcloud-app php occ --no-warnings app:install groupfolders #docker exec -u www-data nextcloud-app php occ --no-warnings app:enable groupfolders docker exec -u www-data nextcloud-app php occ --no-warnings app:install tasks docker exec -u www-data nextcloud-app php occ --no-warnings app:enable tasks docker exec -u www-data nextcloud-app php occ --no-warnings app:install deck docker exec -u www-data nextcloud-app php occ --no-warnings app:enable deck docker exec -u www-data nextcloud-app php occ --no-warnings app:install forms docker exec -u www-data nextcloud-app php occ --no-warnings app:enable forms docker exec -u www-data nextcloud-app php occ --no-warnings app:install user_ldap docker exec -u www-data nextcloud-app php occ --no-warnings app:enable user_ldap docker exec -u www-data nextcloud-app php occ --no-warnings app:install mail docker exec -u www-data nextcloud-app php occ --no-warnings app:enable mail docker exec -u www-data nextcloud-app php occ --no-warnings app:install maps docker exec -u www-data nextcloud-app php occ --no-warnings app:enable maps docker exec -u www-data nextcloud-app php occ --no-warnings app:install notes docker exec -u www-data nextcloud-app php occ --no-warnings app:enable notes docker exec -u www-data nextcloud-app php occ --no-warnings app:install spreed docker exec -u www-data nextcloud-app php occ --no-warnings app:enable spreed docker exec -u www-data nextcloud-app php occ --no-warnings app:install integration_excalidraw docker exec -u www-data nextcloud-app php occ --no-warnings app:enable integration_excalidraw docker exec -u www-data nextcloud-app php occ config:app:set integration_excalidraw base_url --value="https://$SUB_WHITEBOARDRT_WEB.$HOMESERVER_DOMAIN" docker exec -u www-data nextcloud-app php occ config:app:set integration_excalidraw enabled --value="yes" docker exec -u www-data nextcloud-app php occ --no-warnings app:install integration_mastodon docker exec -u www-data nextcloud-app php occ --no-warnings app:enable integration_mastodon docker exec -u www-data nextcloud-app php occ config:app:set integration_mastodon use_popup --value="1" docker exec -u www-data nextcloud-app php occ config:app:set integration_mastodon oauth_instance_url --value="https://$SUB_MASTODON.$HOMESERVER_DOMAIN" docker exec -u www-data nextcloud-app php occ config:app:set integration_mastodon enabled --value="yes" #docker exec -u www-data nextcloud-app php occ --no-warnings app:install riotchat #docker exec -u www-data nextcloud-app php occ --no-warnings app:enable riotchat docker exec -u www-data nextcloud-app php occ --no-warnings app:install previewgenerator docker exec -u www-data nextcloud-app php occ --no-warnings app:enable previewgenerator docker exec -u www-data nextcloud-app php occ config:app:set riotchat custom_json --value="{\"disable_guests\":true,\"piwik\":false,\"settingDefaults\":{\"language\":\"en\"},\"disable_custom_urls\":true,\"disable_login_language_selector\":true,\"disable_3pid_login\": true,\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https:\/\/$SUB_MATRIX_SYNAPSE.$HOMESERVER_DOMAIN\",\"server_name\":\"$HOMESERVER_DOMAIN\"}},\"brand\":\"Nextcloud\",\"branding\":{\"authHeaderLogoUrl\":\"\/core\/img\/logo\/logo.svg?v=0\"},\"showLabsSettings\":true,\"sso_immediate_redirect\":false,\"jitsi\": {\"preferred_domain\": \"$SUB_JITSI.$HOMESERVER_DOMAIN\"},\"roomDirectory\": {\"servers\": [\"$HOMESERVER_DOMAIN\"]},\"default_theme\": \"dark\"}" #docker exec -u www-data nextcloud-app php occ --no-warnings app:install jitsi #docker exec -u www-data nextcloud-app php occ --no-warnings app:enable jitsi docker exec -u www-data nextcloud-app php occ config:app:set jitsi display_join_using_the_jitsi_app --value=1 docker exec -u www-data nextcloud-app php occ config:app:set jitsi jitsi_server_url --value="https://$SUB_JITSI.$HOMESERVER_DOMAIN" #docker exec -u www-data nextcloud-app php occ config:app:set jitsi enabled --value="yes" docker exec -u www-data nextcloud-app php occ config:app:set spreed turn_servers --value="[{\"schemes\":\"turn\",\"server\":\"$SUB_COTURN.$HOMESERVER_DOMAIN:$COTURN_PRIMARY_PORT\",\"secret\":\"$COTURN_STATIC_SECRET\",\"protocols\":\"udp,tcp\"}]" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ config:app:set spreed signaling_servers --value="{\"servers\":[{\"server\":\"https:\/\/$SUB_NCTALKHPB.$HOMESERVER_DOMAIN\/standalone-signaling\",\"verify\":true}],\"secret\":\"$NEXTCLOUD_TALKHPB_SIGNALING_SECRET\"}" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ config:app:set spreed recording_servers --value="{\"servers\":[{\"server\":\"https:\/\/$SUB_NCTALKRECORD.$HOMESERVER_DOMAIN\",\"verify\":true}],\"secret\":\"$NEXTCLOUD_TALKHPB_RECORDING_SECRET\"}" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ config:app:set spreed enabled --value="yes" docker exec -u www-data nextcloud-app php occ --no-warnings app:install richdocuments docker exec -u www-data nextcloud-app php occ --no-warnings app:enable richdocuments docker exec -u www-data nextcloud-app php occ config:app:set richdocuments wopi_allowlist --value="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8" docker exec -u www-data nextcloud-app php occ config:app:set richdocuments public_wopi_url --value="https://$SUB_COLLABORA.$HOMESERVER_DOMAIN" docker exec -u www-data nextcloud-app php occ config:app:set richdocuments wopi_url --value="https://$SUB_COLLABORA.$HOMESERVER_DOMAIN:443" docker exec -u www-data nextcloud-app php occ config:app:set richdocuments federation_use_trusted_domains --value="yes" docker exec -u www-data nextcloud-app php occ config:app:set richdocuments enabled --value="yes" #docker exec -u www-data nextcloud-app php occ --no-warnings app:install files_mindmap #docker exec -u www-data nextcloud-app php occ --no-warnings app:enable files_mindmap docker exec -u www-data nextcloud-app php occ --no-warnings app:install files_external docker exec -u www-data nextcloud-app php occ --no-warnings app:enable files_external docker exec -u www-data nextcloud-app php occ ldap:create-empty-config docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapAgentName "$LDAP_ADMIN_BIND_DN" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapAgentPassword "$LDAP_ADMIN_BIND_PASSWORD" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapBase "$LDAP_BASE_DN" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapBaseGroups "$LDAP_BASE_DN" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapBaseUsers "$LDAP_BASE_DN" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapExpertUsernameAttr "uid" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapGroupFilter "(&(|(objectclass=groupOfUniqueNames)(objectclass=posixGroup)(objectclass=top))(|(cn=$LDAP_ADMIN_USER_GROUP_NAME)(cn=$LDAP_PRIMARY_USER_GROUP_NAME)))" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapGroupFilterGroups "$LDAP_ADMIN_USER_GROUP_NAME;$LDAP_PRIMARY_USER_GROUP_NAME" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapGroupFilterObjectclass "groupOfUniqueNames;posixGroup;top" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapGroupMemberAssocAttr "uniqueMember" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapHost "ldaps://ldapserver" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapPort "636" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapLoginFilter "(&(&(|(objectclass=inetOrgPerson)(objectclass=person)(objectclass=posixAccount))(|(memberof=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)(memberof=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)))(uid=%uid))" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapUserFilter "(&(|(objectclass=inetOrgPerson)(objectclass=person)(objectclass=posixAccount))(|(memberof=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)(memberof=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)))" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapUserDisplayName cn docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapEmailAttribute mail docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapUserFilterGroups "$LDAP_ADMIN_USER_GROUP_NAME;$LDAP_PRIMARY_USER_GROUP_NAME" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapUserFilterObjectclass "inetOrgPerson;person;posixAccount" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 turnOnPasswordChange "1" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 useMemberOfToDetectMembership "1" docker exec -u www-data nextcloud-app php occ ldap:set-config s01 ldapConfigurationActive "1" docker exec -u www-data nextcloud-app php occ ldap:test-config s01 docker exec -u www-data nextcloud-app php occ config:app:set user_ldap enabled --value="yes" docker exec -u www-data nextcloud-app php occ ldap:check-user --force "$LDAP_ADMIN_USER_USERNAME" docker exec -u www-data nextcloud-app php occ ldap:search "$LDAP_ADMIN_USER_USERNAME" docker exec -u www-data nextcloud-app php occ ldap:promote-group "admins" -y docker exec -u www-data nextcloud-app php occ ldap:check-group --update "$LDAP_PRIMARY_USER_GROUP_NAME" docker exec -u www-data nextcloud-app php occ --no-warnings app:install ldap_write_support docker exec -u www-data nextcloud-app php occ --no-warnings app:enable ldap_write_support docker exec -u www-data nextcloud-app php occ config:system:set trusted_domains 2 --value=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN docker exec -u www-data nextcloud-app php occ config:system:set enabledPreviewProviders 0 --value="OC\\Preview\\Imaginary" docker exec -u www-data nextcloud-app php occ config:system:set preview_imaginary_url --value="http://nextcloud-imaginary:$NEXTCLOUD_IMAGINARY_PORT" docker exec -u www-data nextcloud-app php occ --no-warnings app:install notify_push docker exec -u www-data nextcloud-app php occ --no-warnings app:enable notify_push docker exec -u www-data nextcloud-app php occ app_api:daemon:register --net dock-ext --compute_device cpu --set-default --harp --harp_frp_address nextcloud-harp:8782 --harp_shared_key $NEXTCLOUD_HARP_SHARED_KEY harp_proxy_docker "HaRP Proxy (Docker)" docker-install http nextcloud-harp:8780 https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN docker exec -u www-data nextcloud-app php occ config:system:set maintenance_window_start --type=integer --value=1 if ! [ "$(isServiceDisabled clamav)" = "true" ]; then docker exec -u www-data nextcloud-app php occ --no-warnings app:install files_antivirus docker exec -u www-data nextcloud-app php occ --no-warnings app:enable files_antivirus docker exec -u www-data nextcloud-app php occ config:app:set files_antivirus av_mode --value="daemon" docker exec -u www-data nextcloud-app php occ config:app:set files_antivirus av_host --value="mailu-antivirus" docker exec -u www-data nextcloud-app php occ config:app:set files_antivirus av_port --value="3310" docker exec -u www-data nextcloud-app php occ config:app:set files_antivirus av_stream_max_length --value="26214400" docker exec -u www-data nextcloud-app php occ config:app:set files_antivirus av_max_file_size --value="-1" docker exec -u www-data nextcloud-app php occ config:app:set files_antivirus av_infected_action --value="delete" docker exec -u www-data nextcloud-app php occ config:app:set files_antivirus enabled --value="yes" fi addSharedDirsNextcloud docker exec -u www-data nextcloud-app php occ dav:create-addressbook "$NEXTCLOUD_ADMIN_USERNAME" "Global" docker exec -u www-data nextcloud-app php occ db:add-missing-indices > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ maintenance:repair --include-expensive > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ background:cron docker exec -u www-data nextcloud-app php occ mail:account:create "$NEXTCLOUD_ADMIN_USERNAME" "Nextcloud $(getAdminEmailName)" "$EMAIL_ADMIN_EMAIL_ADDRESS" "mailu-front" 993 ssl "$EMAIL_ADMIN_EMAIL_ADDRESS" "$EMAIL_ADMIN_PASSWORD" "mailu-front" 465 ssl "$EMAIL_ADMIN_EMAIL_ADDRESS" "$EMAIL_ADMIN_PASSWORD" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ user:sync-account-data sleep 5 cd ~ docker compose -f $HOME/nextcloud-compose-tmp.yml down -v rm -f $HOME/nextcloud-compose-tmp.yml if sudo test -d $HSHQ_STACKS_DIR/nextcloud/app/custom_apps/notify_push/bin; then # This is dumb. https://github.com/nextcloud/notify_push/issues/355 sudo chmod -R 755 $HSHQ_STACKS_DIR/nextcloud/app/custom_apps/notify_push/bin else performNextcloudInstallFailCleanup echo "ERROR: Nextcloud Push plugin was not installed correctly, exiting..." return 5 fi set +e echo "post_max_size=16G" | sudo tee -a $HSHQ_STACKS_DIR/nextcloud/app/.user.ini echo "upload_max_filesize=16G" | sudo tee -a $HSHQ_STACKS_DIR/nextcloud/app/.user.ini echo "memory_limit=2G" | sudo tee -a $HSHQ_STACKS_DIR/nextcloud/app/.user.ini echo "max_input_time=3600" | sudo tee -a $HSHQ_STACKS_DIR/nextcloud/app/.user.ini echo "max_execution_time=3600" | sudo tee -a $HSHQ_STACKS_DIR/nextcloud/app/.user.ini #sudo sed -i "s/'overwrite.cli.url'.*/'overwrite.cli.url' => 'https:\/\/$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN',/" $HSHQ_STACKS_DIR/nextcloud/app/config/config.php sudo sed -i "/overwriteprotocol.*/a\ 'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED_PROTO', 'HTTP_X_REAL_IP']," $HSHQ_STACKS_DIR/nextcloud/app/config/config.php installStack nextcloud nextcloud-app "ready to handle connections" $HOME/nextcloud.env if [ $? -ne 0 ]; then nc_arr=($(docker ps --filter name=nextcloud --format "{{.Names}}")) for curcont in "${nc_arr[@]}" do docker container stop $curcont docker container rm $curcont done docker volume rm nextcloud_v-nextcloud-data > /dev/null 2>&1 docker image rm $(getScriptImageByContainerName nextcloud-app) > /dev/null 2>&1 docker image rm $(getScriptImageByContainerName nextcloud-imaginary) > /dev/null 2>&1 stack_name=nextcloud echo "ERROR: Nextcloud did not start up correctly, exiting..." return 6 fi sleep 3 set +e addMCPServerLiteLLM "nextcloud" "nc" "http://nextcloud-mcp:80/mcp" http none "" addReadOnlyUserToDatabase Nextcloud postgres nextcloud-db $NEXTCLOUD_DATABASE_NAME $NEXTCLOUD_DATABASE_USER $NEXTCLOUD_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/nextcloud/dbexport $NEXTCLOUD_DATABASE_READONLYUSER $NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>rewrite /.well-known/carddav /remote.php/dav\n" inner_block=$inner_block">>>>rewrite /.well-known/caldav /remote.php/dav\n" inner_block=$inner_block">>>>rewrite /.well-known/webfinger /index.php/.well-known/webfinger\n" inner_block=$inner_block">>>>rewrite /.well-known/nodeinfo /index.php/.well-known/nodeinfo\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>route /push/* {\n" inner_block=$inner_block">>>>>>>>uri strip_prefix /push\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-push:$NEXTCLOUD_PUSH_PORT\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>route /exapps/* {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-harp:$NEXTCLOUD_HARP_PORT\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy https://nextcloud-web {\n" inner_block=$inner_block">>>>>>>>header_up Authorization {header.Authorization}\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NEXTCLOUD $MANAGETLS_NEXTCLOUD "$is_integrate_hshq" $NETDEFAULT_NEXTCLOUD "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_NCTALKHPB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>route /standalone-signaling/* {\n" inner_block=$inner_block">>>>>>>>uri strip_prefix /standalone-signaling\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-talkhpb:8081 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>>>header_up X-Real-IP {remote_host}\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NCTALKHPB $MANAGETLS_NCTALKHPB "$is_integrate_hshq" $NETDEFAULT_NCTALKHPB "$inner_block" insertSubAuthelia $SUB_NCTALKHPB.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_NCTALKRECORD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://nextcloud-talkrecord:1234 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NCTALKRECORD $MANAGETLS_NCTALKRECORD "$is_integrate_hshq" $NETDEFAULT_NCTALKRECORD "$inner_block" echo "Updating authelia..." insertSubAuthelia $SUB_NCTALKRECORD.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll nextcloud "$FMLNAME_NEXTCLOUD" $USERTYPE_NEXTCLOUD "https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN" "nextcloud.png" "$(getHeimdallOrderFromSub $SUB_NEXTCLOUD $USERTYPE_NEXTCLOUD)" restartAllCaddyContainers fi echo "Enabling push notifications..." docker exec -u www-data nextcloud-app php occ app:enable notify_push docker exec -u www-data nextcloud-app php occ notify_push:setup https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/push docker exec -i nextcloud-web curl -fsS -u "$NEXTCLOUD_ADMIN_USERNAME:$NEXTCLOUD_ADMIN_PASSWORD" -X POST \ -H 'Content-Type: application/xml; charset=UTF-8' \ --data-binary @- "http://localhost/remote.php/dav/addressbooks/users/$NEXTCLOUD_ADMIN_USERNAME/Global" >/dev/null < principal:principals/groups/$LDAP_PRIMARY_USER_GROUP_NAME $LDAP_PRIMARY_USER_GROUP_NAME XML outputNextcloudInotifyScan echo "Nextcloud stack configured!" } function performNextcloudInstallFailCleanup() { sudo rm -fr $HSHQ_STACKS_DIR/nextcloud rm -f $HOME/nextcloud-compose-tmp.yml rm -f $HOME/nextcloud-compose.yml rm -f $HOME/nextcloud.env rm -f $HOME/nextcloud-json.tmp docker volume rm nextcloud_v-nextcloud-data > /dev/null 2>&1 docker image rm $(getScriptImageByContainerName nextcloud-app) > /dev/null 2>&1 docker image rm $(getScriptImageByContainerName nextcloud-imaginary) > /dev/null 2>&1 stack_name=nextcloud } function outputConfigNextcloud() { NCTALKRECORD_PYTHON_VER=python3.14 outputNGINXConfigNextcloud cat < $HSHQ_STACKS_DIR/nextcloud/www.conf [www] user = www-data group = www-data listen = 0.0.0.0:9000 pm = dynamic pm.max_children = 200 pm.start_servers = 50 pm.min_spare_servers = 50 pm.max_spare_servers = 150 EOFNC cat < $HSHQ_STACKS_DIR/nextcloud/ldap-app.conf TLS_CERT /opt/ssl/nextcloud-app.crt TLS_KEY /opt/ssl/nextcloud-app.key TLS_CACERT /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT demand TLS_REQSAN demand EOFLD cat < $HSHQ_STACKS_DIR/nextcloud/ldap-cron.conf TLS_CERT /opt/ssl/nextcloud-cron.crt TLS_KEY /opt/ssl/nextcloud-cron.key TLS_CACERT /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT demand TLS_REQSAN demand EOFLC cat < $HSHQ_STACKS_DIR/nextcloud/ldap-push.conf TLS_CERT /opt/ssl/nextcloud-push.crt TLS_KEY /opt/ssl/nextcloud-push.key TLS_CACERT /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT demand TLS_REQSAN demand EOFLC cat < $HOME/nextcloud-compose-tmp.yml services: nextcloud-db: image: $(getScriptImageByContainerName nextcloud-db) container_name: nextcloud-db hostname: nextcloud-db user: "${USERID}" restart: unless-stopped security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-nextcloud-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - ${HSHQ_STACKS_DIR}/nextcloud/db:/var/lib/postgresql/data environment: - TZ=$TZ - POSTGRES_DB=$NEXTCLOUD_DATABASE_NAME - POSTGRES_USER=$NEXTCLOUD_DATABASE_USER - POSTGRES_PASSWORD=$NEXTCLOUD_DATABASE_USER_PASSWORD nextcloud-app: image: $(getScriptImageByContainerName nextcloud-app) container_name: nextcloud-app hostname: nextcloud-app restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - $HSHQ_STACKS_DIR/nextcloud/www.conf:/usr/local/etc/php-fpm.d/www.conf - $HSHQ_SSL_DIR/nextcloud-app.crt:/opt/ssl/nextcloud-app.crt - $HSHQ_SSL_DIR/nextcloud-app.key:/opt/ssl/nextcloud-app.key - $HSHQ_STACKS_DIR/nextcloud/ldap-app.conf:/etc/openldap/ldap.conf - $HSHQ_STACKS_DIR/shared:/shared - v-nextcloud-data:/var/www/html environment: - TZ=$TZ - POSTGRES_HOST=nextcloud-db - POSTGRES_DB=$NEXTCLOUD_DATABASE_NAME - POSTGRES_USER=$NEXTCLOUD_DATABASE_USER - POSTGRES_PASSWORD=$NEXTCLOUD_DATABASE_USER_PASSWORD - REDIS_HOST_PASSWORD=$NEXTCLOUD_REDIS_PASSWORD - REDIS_HOST=nextcloud-redis - NEXTCLOUD_ADMIN_USER=$NEXTCLOUD_ADMIN_USERNAME - NEXTCLOUD_ADMIN_PASSWORD=$NEXTCLOUD_ADMIN_PASSWORD - SMTP_MODE=smtp - SMTP_HOST=$SMTP_HOSTNAME - SMTP_PORT=$SMTP_HOSTPORT - SMTP_SECURE=tls - SMTP_AUTH=0 - SMTP_AUTHTYPE=NONE - MAIL_FROM_ADDRESS=$EMAIL_SMTP_USERNAME - MAIL_DOMAIN=$HOMESERVER_DOMAIN - "NEXTCLOUD_TRUSTED_DOMAINS=nextcloud-web" - "TRUSTED_PROXIES=$TRUSTED_PROXIES" - NEXTCLOUD_DATA_DIR=/var/www/html/data - OVERWRITEHOST=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITECLIURL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITEPROTOCOL=https nextcloud-cron: image: $(getScriptImageByContainerName nextcloud-cron) container_name: nextcloud-cron hostname: nextcloud-cron restart: unless-stopped security_opt: - no-new-privileges:true entrypoint: /cron.sh depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - $HSHQ_SSL_DIR/nextcloud-cron.crt:/opt/ssl/nextcloud-cron.crt - $HSHQ_SSL_DIR/nextcloud-cron.key:/opt/ssl/nextcloud-cron.key - $HSHQ_STACKS_DIR/nextcloud/ldap-cron.conf:/etc/openldap/ldap.conf - $HSHQ_STACKS_DIR/shared:/shared - v-nextcloud-data:/var/www/html environment: - TZ=$TZ - POSTGRES_HOST=nextcloud-db - POSTGRES_DB=$NEXTCLOUD_DATABASE_NAME - POSTGRES_USER=$NEXTCLOUD_DATABASE_USER - POSTGRES_PASSWORD=$NEXTCLOUD_DATABASE_USER_PASSWORD - REDIS_HOST_PASSWORD=$NEXTCLOUD_REDIS_PASSWORD - REDIS_HOST=nextcloud-redis - NEXTCLOUD_ADMIN_USER=$NEXTCLOUD_ADMIN_USERNAME - NEXTCLOUD_ADMIN_PASSWORD=$NEXTCLOUD_ADMIN_PASSWORD - SMTP_MODE=smtp - SMTP_HOST=$SMTP_HOSTNAME - SMTP_PORT=$SMTP_HOSTPORT - SMTP_SECURE=tls - SMTP_AUTH=0 - SMTP_AUTHTYPE=NONE - MAIL_FROM_ADDRESS=$EMAIL_SMTP_USERNAME - MAIL_DOMAIN=$HOMESERVER_DOMAIN - "NEXTCLOUD_TRUSTED_DOMAINS=nextcloud-web" - "TRUSTED_PROXIES=$TRUSTED_PROXIES" - NEXTCLOUD_DATA_DIR=/var/www/html/data nextcloud-imaginary: image: $(getScriptImageByContainerName nextcloud-imaginary) container_name: nextcloud-imaginary hostname: nextcloud-imaginary restart: unless-stopped command: -concurrency 50 -enable-url-source cap_add: - SYS_NICE depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net environment: - PORT=$NEXTCLOUD_IMAGINARY_PORT nextcloud-talkhpb: image: $(getScriptImageByContainerName nextcloud-talkhpb) container_name: nextcloud-talkhpb hostname: nextcloud-talkhpb restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TALK_HOST=$SUB_COTURN.$HOMESERVER_DOMAIN - TALK_PORT=$COTURN_PRIMARY_PORT - TURN_SECRET=$COTURN_STATIC_SECRET - SIGNALING_SECRET=$NEXTCLOUD_TALKHPB_SIGNALING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro nextcloud-talkrecord: image: $(getScriptImageByContainerName nextcloud-talkrecord) container_name: nextcloud-talkrecord hostname: nextcloud-talkrecord user: "122" restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TZ=$TZ - RECORDING_SECRET=$NEXTCLOUD_TALKHPB_RECORDING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET - SKIP_VERIFY=true - HPB_DOMAIN=$SUB_NCTALKHPB.$HOMESERVER_DOMAIN shm_size: 1073741824 read_only: true tmpfs: - /conf cap_drop: - NET_RAW volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/$NCTALKRECORD_PYTHON_VER/site-packages/certifi/cacert.pem:ro - v-nextcloud-record:/tmp:rw nextcloud-web: image: $(getScriptImageByContainerName nextcloud-web) container_name: nextcloud-web hostname: nextcloud-web restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-nextcloud-data:/var/www/html:ro - $HSHQ_STACKS_DIR/nextcloud/web/nginx.conf:/etc/nginx/nginx.conf - $HSHQ_SSL_DIR/nextcloud-web.crt:/etc/nginx/certs/cert.crt - $HSHQ_SSL_DIR/nextcloud-web.key:/etc/nginx/certs/cert.key environment: - TZ=$TZ nextcloud-harp: image: $(getScriptImageByContainerName nextcloud-harp) container_name: nextcloud-harp hostname: nextcloud-harp restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /var/run/docker.sock:/var/run/docker.sock environment: - HP_SHARED_KEY=$NEXTCLOUD_HARP_SHARED_KEY - NC_INSTANCE_URL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN nextcloud-mcp: image: $(getScriptImageByContainerName nextcloud-mcp) container_name: nextcloud-mcp hostname: nextcloud-mcp restart: unless-stopped security_opt: - no-new-privileges:true pull_policy: always networks: - int-nextcloud-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro nextcloud-redis: image: $(getScriptImageByContainerName nextcloud-redis) container_name: nextcloud-redis hostname: nextcloud-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $NEXTCLOUD_REDIS_PASSWORD --appendonly no networks: - int-nextcloud-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-nextcloud-data: driver: local driver_opts: type: none o: bind device: $HSHQ_STACKS_DIR/nextcloud/app v-nextcloud-record: driver: local driver_opts: type: none o: bind device: $HSHQ_STACKS_DIR/nextcloud/record v-nextcloud-mcp-data: driver: local driver_opts: type: none o: bind device: $HSHQ_STACKS_DIR/nextcloud/mcp/data v-nextcloud-mcp-oauth: driver: local driver_opts: type: none o: bind device: $HSHQ_STACKS_DIR/nextcloud/mcp/oauth networks: dock-proxy-net: name: dock-proxy external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true int-nextcloud-net: driver: bridge internal: true ipam: driver: default EOFNC outputComposeNextcloud outputMCPConfigNextcloud cat < $HOME/nextcloud.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL REDIS_TLS_ENABLED=no PYTHON_VER=$NCTALKRECORD_PYTHON_VER NEXTCLOUD_BASE_URL=http://nextcloud-web:80 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFNC } function outputComposeNextcloud() { rm -f $HOME/nextcloud-compose.yml cat < $HOME/nextcloud-compose.yml $STACK_VERSION_PREFIX nextcloud $(getScriptStackVersion nextcloud) services: nextcloud-db: image: $(getScriptImageByContainerName nextcloud-db) container_name: nextcloud-db hostname: nextcloud-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped security_opt: - no-new-privileges:true shm_size: 256mb depends_on: - nextcloud-redis networks: - int-nextcloud-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/dbexport:/dbexport environment: - TZ=$TZ - POSTGRES_DB=$NEXTCLOUD_DATABASE_NAME - POSTGRES_USER=$NEXTCLOUD_DATABASE_USER - POSTGRES_PASSWORD=$NEXTCLOUD_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.nextcloud-hourly-db.schedule=@every 1h" - "ofelia.job-exec.nextcloud-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.nextcloud-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.nextcloud-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.nextcloud-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.nextcloud-hourly-db.email-from=Nextcloud Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.nextcloud-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.nextcloud-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.nextcloud-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.nextcloud-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.nextcloud-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.nextcloud-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.nextcloud-monthly-db.email-from=Nextcloud Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.nextcloud-monthly-db.mail-only-on-error=false" nextcloud-app: image: $(getScriptImageByContainerName nextcloud-app) container_name: nextcloud-app hostname: nextcloud-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/www.conf:/usr/local/etc/php-fpm.d/www.conf - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-app.crt:/opt/ssl/nextcloud-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-app.key:/opt/ssl/nextcloud-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-app.conf:/etc/openldap/ldap.conf - \${PORTAINER_HSHQ_STACKS_DIR}/shared:/shared - v-nextcloud-data:/var/www/html environment: - OVERWRITEHOST=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITECLIURL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITEPROTOCOL=https nextcloud-cron: image: $(getScriptImageByContainerName nextcloud-cron) container_name: nextcloud-cron hostname: nextcloud-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true entrypoint: /cron.sh depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-cron.crt:/opt/ssl/nextcloud-cron.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-cron.key:/opt/ssl/nextcloud-cron.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-cron.conf:/etc/openldap/ldap.conf - \${PORTAINER_HSHQ_STACKS_DIR}/shared:/shared - v-nextcloud-data:/var/www/html nextcloud-push: image: $(getScriptImageByContainerName nextcloud-push) container_name: nextcloud-push hostname: nextcloud-push restart: unless-stopped entrypoint: /var/www/html/custom_apps/notify_push/bin/x86_64/notify_push /var/www/html/config/config.php security_opt: - no-new-privileges:true depends_on: - nextcloud-db - nextcloud-redis - nextcloud-app networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-push.crt:/opt/ssl/nextcloud-push.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-push.key:/opt/ssl/nextcloud-push.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-push.conf:/etc/openldap/ldap.conf - v-nextcloud-data:/var/www/html environment: - PORT=$NEXTCLOUD_PUSH_PORT - NEXTCLOUD_URL=http://nextcloud-web nextcloud-imaginary: image: $(getScriptImageByContainerName nextcloud-imaginary) container_name: nextcloud-imaginary hostname: nextcloud-imaginary restart: unless-stopped command: -concurrency 50 -enable-url-source cap_add: - SYS_NICE depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net environment: - PORT=$NEXTCLOUD_IMAGINARY_PORT nextcloud-talkhpb: image: $(getScriptImageByContainerName nextcloud-talkhpb) container_name: nextcloud-talkhpb hostname: nextcloud-talkhpb restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TALK_HOST=$SUB_COTURN.$HOMESERVER_DOMAIN - TALK_PORT=$COTURN_PRIMARY_PORT - TURN_SECRET=$COTURN_STATIC_SECRET - SIGNALING_SECRET=$NEXTCLOUD_TALKHPB_SIGNALING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro nextcloud-talkrecord: image: $(getScriptImageByContainerName nextcloud-talkrecord) container_name: nextcloud-talkrecord hostname: nextcloud-talkrecord user: "122" restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TZ=\${PORTAINER_TZ} - RECORDING_SECRET=$NEXTCLOUD_TALKHPB_RECORDING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET - SKIP_VERIFY=true - HPB_DOMAIN=$SUB_NCTALKHPB.$HOMESERVER_DOMAIN shm_size: 1073741824 read_only: true tmpfs: - /conf cap_drop: - NET_RAW volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-nextcloud-record:/tmp:rw nextcloud-web: image: $(getScriptImageByContainerName nextcloud-web) container_name: nextcloud-web hostname: nextcloud-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-nextcloud-data:/var/www/html:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/web/nginx.conf:/etc/nginx/nginx.conf - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-web.crt:/etc/nginx/certs/cert.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-web.key:/etc/nginx/certs/cert.key nextcloud-harp: image: $(getScriptImageByContainerName nextcloud-harp) container_name: nextcloud-harp hostname: nextcloud-harp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /var/run/docker.sock:/var/run/docker.sock environment: - HP_SHARED_KEY=$NEXTCLOUD_HARP_SHARED_KEY - NC_INSTANCE_URL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN nextcloud-mcp: image: $(getScriptImageByContainerName nextcloud-mcp) container_name: nextcloud-mcp hostname: nextcloud-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-nextcloud-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro nextcloud-redis: image: $(getScriptImageByContainerName nextcloud-redis) container_name: nextcloud-redis hostname: nextcloud-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $NEXTCLOUD_REDIS_PASSWORD --appendonly no networks: - int-nextcloud-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-nextcloud-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/app v-nextcloud-record: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/record v-nextcloud-mcp-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/mcp/data v-nextcloud-mcp-oauth: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/mcp/oauth networks: dock-proxy-net: name: dock-proxy external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-nextcloud-net: driver: bridge internal: true ipam: driver: default EOFNC } function outputMCPConfigNextcloud() { cat < $HSHQ_STACKS_DIR/nextcloud/mcp/settings.toml # Nextcloud MCP Server Configuration # This file defines all configuration keys with their default values. # Environment variables always override values from this file. # # Loading priority (last wins): # 1. settings.toml [default] section # 2. settings.toml [] section (via MCP_DEPLOYMENT_MODE) # 3. .secrets.toml [default] section # 4. .secrets.toml [] section # 5. settings.local.toml (all sections, gitignored) # 6. Environment variables (highest priority) # # See docs/ADR-024-dynaconf-configuration-management.md for details. [default] # --- Deployment mode (ADR-021) --- # Valid: single_user_basic, multi_user_basic, login_flow, keycloak, oauth_single_audience # If unset, mode is auto-detected from other settings. mcp_deployment_mode = "@none" # --- Nextcloud core --- nextcloud_host = "@none" nextcloud_username = "@none" nextcloud_password = "@none" nextcloud_app_password = "@none" nextcloud_verify_ssl = true nextcloud_ca_bundle = "@none" nextcloud_mcp_server_url = "@none" nextcloud_resource_uri = "@none" # --- OAuth/OIDC --- oidc_discovery_url = "@none" nextcloud_oidc_client_id = "@none" nextcloud_oidc_client_secret = "@none" oidc_issuer = "@none" jwks_uri = "@none" introspection_uri = "@none" userinfo_uri = "@none" oidc_resource_server_id = "@none" # --- Mode flags --- enable_multi_user_basic_auth = false enable_login_flow = true enable_semantic_search = false enable_background_operations = false # Deprecated aliases (declared so env var overrides work) vector_sync_enabled = false enable_offline_access = false enable_token_exchange = false # --- Token storage --- token_encryption_key = "@none" token_storage_db = "/tmp/tokens.db" # --- Vector sync --- vector_sync_scan_interval = 300 vector_sync_processor_workers = 3 vector_sync_queue_max_size = 10000 vector_sync_user_poll_interval = 60 # --- Qdrant --- # No default for qdrant_location — conditional default (:memory:) is in Settings.__post_init__ qdrant_url = "@none" qdrant_location = "@none" qdrant_api_key = "@none" qdrant_collection = "nextcloud_content" # --- Ollama --- ollama_base_url = "@none" ollama_embedding_model = "nomic-embed-text" ollama_verify_ssl = true # --- OpenAI --- openai_api_key = "@none" openai_base_url = "@none" openai_embedding_model = "text-embedding-3-small" # --- Document chunking --- document_chunk_size = 2048 document_chunk_overlap = 200 # --- Observability --- metrics_enabled = true metrics_port = 9090 otel_exporter_otlp_endpoint = "@none" otel_exporter_verify_ssl = false otel_service_name = "nextcloud-mcp-server" otel_traces_sampler = "always_on" otel_traces_sampler_arg = 1.0 log_format = "text" log_level = "INFO" log_include_trace_context = true # --- Document processing --- enable_document_processing = false document_processor = "unstructured" enable_unstructured = false unstructured_api_url = "http://unstructured:8000" unstructured_timeout = 120 unstructured_strategy = "auto" unstructured_languages = "eng,deu" progress_interval = 10 enable_tesseract = false tesseract_cmd = "@none" tesseract_lang = "eng" enable_pymupdf = true pymupdf_extract_images = true pymupdf_image_dir = "@none" enable_custom_processor = false custom_processor_url = "@none" custom_processor_types = "application/pdf" custom_processor_name = "custom" custom_processor_api_key = "@none" custom_processor_timeout = 60 # --- Deployment mode sections --- # Keys here override [default] when MCP_DEPLOYMENT_MODE matches. [single_user_basic] [multi_user_basic] enable_multi_user_basic_auth = true [login_flow] enable_login_flow = true [keycloak] [oauth_single_audience] EOFNC } function outputNGINXConfigNextcloud() { rm -f $HSHQ_STACKS_DIR/nextcloud/web/nginx.conf cat < $HSHQ_STACKS_DIR/nextcloud/web/nginx.conf worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; types { text/html html htm shtml; text/css css; text/xml xml; text/javascript js mjs; text/plain txt; application/json json; application/pdf pdf; application/zip zip; application/msword doc; application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; application/vnd.ms-excel xls; application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; application/vnd.oasis.opendocument.text odt; application/vnd.oasis.opendocument.spreadsheet ods; application/vnd.oasis.opendocument.presentation odp; application/octet-stream bin exe dll stream mid mov mpeg mpg mpe mp3 mp4 m4a m4v flv webm wav aac ogg; audio/mpeg mp3; audio/wav wav; audio/ogg ogg; video/mpeg mpeg mpg mpe; video/quicktime mov qt; video/mp4 mp4; video/webm webm; video/x-flv flv; image/gif gif; image/jpeg jpg jpeg; image/png png; image/svg+xml svg svgz; image/webp webp; } log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' '\$status \$body_bytes_sent "\$http_referer" ' '"\$http_user_agent" "\$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; # Prevent nginx HTTP Server Detection server_tokens off; keepalive_timeout 65; # Set the immutable cache control options only for assets with a cache busting v argument map \$arg_v \$asset_immutable { "" ""; default ", immutable"; } #gzip on; resolver 127.0.0.11 valid=2s; upstream php-handler { zone backends 64k; server nextcloud-app:9000 resolve; } server { listen 80; listen 443 ssl; http2 on; ssl_certificate /etc/nginx/certs/cert.crt; ssl_certificate_key /etc/nginx/certs/cert.key; # HSTS settings # WARNING: Only add the preload option once you read about # the consequences in https://hstspreload.org/. This option # will add the domain to a hardcoded list that is shipped # in all major browsers and getting removed from this list # could take several months. add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" always; # set max upload size client_max_body_size 16400M; fastcgi_buffers 64 16K; fastcgi_buffer_size 16K; client_body_buffer_size 512k; # Enable gzip but do not remove ETag headers gzip on; gzip_vary on; gzip_comp_level 4; gzip_min_length 256; gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; # Pagespeed is not supported by Nextcloud, so if your server is built # with the ngx_pagespeed module, uncomment this line to disable it. #pagespeed off; # HTTP response headers borrowed from Nextcloud .htaccess add_header Referrer-Policy "no-referrer" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Download-Options "noopen" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Permitted-Cross-Domain-Policies "none" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header X-XSS-Protection "1; mode=block" always; # Remove X-Powered-By, which is an information leak fastcgi_hide_header X-Powered-By; # Path to the root of your installation root /var/www/html; # Specify how to handle directories -- specifying /index.php\$request_uri # here as the fallback means that Nginx always exhibits the desired behaviour # when a client requests a path that corresponds to a directory that exists # on the server. In particular, if that directory contains an index.php file, # that file is correctly served; if it doesn't, then the request is passed to # the front-end controller. This consistent behaviour means that we don't need # to specify custom rules for certain paths (e.g. images and other assets, # /updater, /ocm-provider, /ocs-provider), and thus # try_files \$uri \$uri/ /index.php\$request_uri # always provides the desired behaviour. index index.php index.html /index.php\$request_uri; # Rule borrowed from .htaccess to handle Microsoft DAV clients location = / { if ( \$http_user_agent ~ ^DavClnt ) { return 302 /remote.php/webdav/\$is_args\$args; } } location = /robots.txt { allow all; log_not_found off; access_log off; } # Make a regex exception for /.well-known so that clients can still # access it despite the existence of the regex rule # location ~ /(\.|autotest|...) which would otherwise handle requests # for /.well-known. location ^~ /.well-known { # The rules in this block are an adaptation of the rules # in .htaccess that concern /.well-known. location = /.well-known/carddav { return 301 /remote.php/dav/; } location = /.well-known/caldav { return 301 /remote.php/dav/; } location /.well-known/acme-challenge { try_files \$uri \$uri/ =404; } location /.well-known/pki-validation { try_files \$uri \$uri/ =404; } # Let Nextcloud's API for /.well-known URIs handle all other # requests by passing them to the front-end controller. return 301 /index.php\$request_uri; } # Rules borrowed from .htaccess to hide certain paths from clients location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:\$|/) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } # Ensure this block, which passes PHP files to the PHP process, is above the blocks # which handle static assets (as seen below). If this block is not declared first, # then Nginx will encounter an infinite rewriting loop when it prepends /index.php # to the URI, resulting in a HTTP 500 error response. location ~ \.php(?:\$|/) { # Required for legacy support rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php\$request_uri; fastcgi_split_path_info ^(.+?\.php)(/.*)\$; set \$path_info \$fastcgi_path_info; try_files \$fastcgi_script_name =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; fastcgi_param PATH_INFO \$path_info; #fastcgi_param HTTPS on; fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice fastcgi_param front_controller_active true; # Enable pretty urls fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering on; fastcgi_max_temp_file_size 0; } # Serve static files location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac|mp4|webm)\$ { try_files \$uri /index.php\$request_uri; add_header Cache-Control "public, max-age=15778463\$asset_immutable"; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Download-Options "noopen" always; add_header Referrer-Policy "no-referrer" always; add_header X-Robots-Tag "noindex, nofollow" always; add_header X-Permitted-Cross-Domain-Policies "none" always; add_header X-XSS-Protection "1; mode=block" always; default_type application/octet-stream; access_log off; } location ~ \.(otf|woff2?)\$ { try_files \$uri /index.php\$request_uri; expires 7d; # Cache-Control policy borrowed from .htaccess access_log off; # Optional: Don't log access to assets } # Rule borrowed from .htaccess location /remote { return 301 /remote.php\$request_uri; } location / { try_files \$uri \$uri/ /index.php\$request_uri; } } } EOFNG } function performUpdateNextcloud() { perform_stack_name=nextcloud prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:27.1.3-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.23.2-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="nextcloud:27.1.3-fpm-alpine,nextcloud:27.1.7-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:latest,nextcloud/aio-imaginary:latest" image_update_map[4]="nginx:1.23.2-alpine,nginx:1.25.3-alpine" ;; 2) newVer=v4 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:27.1.5-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="nextcloud:27.1.5-fpm-alpine,nextcloud:27.1.7-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:latest,nextcloud/aio-imaginary:latest" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:27.1.6-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="nextcloud:27.1.6-fpm-alpine,nextcloud:27.1.7-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:latest,nextcloud/aio-imaginary:latest" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:27.1.7-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="nextcloud:27.1.7-fpm-alpine,nextcloud:28.0.8-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:latest,nextcloud/aio-imaginary:latest" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfNextcloudUpdateNGINXConfig false perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud docker exec -u www-data nextcloud-app php occ config:system:set maintenance_window_start --type=integer --value=1 return ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:28.0.8-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="nextcloud:28.0.8-fpm-alpine,nextcloud:29.0.8-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:latest,nextcloud/aio-imaginary:latest" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:29.0.8-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="nextcloud:29.0.8-fpm-alpine,nextcloud:30.0.1-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:latest,nextcloud/aio-imaginary:latest" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfNextcloudUpdateNGINXConfig false perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud return ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,nextcloud:30.0.1-fpm-alpine,nextcloud/aio-imaginary:latest,nginx:1.25.3-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[2]="nextcloud:30.0.1-fpm-alpine,nextcloud:31.0.2-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:latest,nextcloud/aio-imaginary:20250325_084656" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.27.4-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfNextcloudUpdateNGINXConfig false perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud return ;; 8) newVer=v9 curImageList=postgres:15.0-bullseye,bitnami/redis:7.4.2,nextcloud:31.0.2-fpm-alpine,nextcloud/aio-imaginary:20250325_084656,nginx:1.27.4-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.4.2,bitnami/redis:7.4.2" image_update_map[2]="nextcloud:31.0.2-fpm-alpine,nextcloud:31.0.2-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:20250325_084656,nextcloud/aio-imaginary:20250325_084656" image_update_map[4]="nginx:1.27.4-alpine,nginx:1.27.4-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfNextcloudAddTalkHPB docker exec -u www-data nextcloud-app php occ config:app:set spreed turn_servers --value="[{\"schemes\":\"turn\",\"server\":\"$SUB_COTURN.$HOMESERVER_DOMAIN:$COTURN_PRIMARY_PORT\",\"secret\":\"$COTURN_STATIC_SECRET\",\"protocols\":\"udp,tcp\"}]" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ config:app:set spreed signaling_servers --value="{\"servers\":[{\"server\":\"https:\/\/$SUB_NCTALKHPB.$HOMESERVER_DOMAIN\/standalone-signaling\",\"verify\":true}],\"secret\":\"$NEXTCLOUD_TALKHPB_SIGNALING_SECRET\"}" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ config:app:set spreed recording_servers --value="{\"servers\":[{\"server\":\"https:\/\/$SUB_NCTALKRECORD.$HOMESERVER_DOMAIN\",\"verify\":true}],\"secret\":\"$NEXTCLOUD_TALKHPB_RECORDING_SECRET\"}" > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ config:app:set spreed enabled --value="yes" perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud return ;; 9) newVer=v10 curImageList=postgres:15.0-bullseye,bitnami/redis:7.4.2,nextcloud:31.0.2-fpm-alpine,nextcloud/aio-imaginary:20250325_084656,nginx:1.27.4-alpine,ghcr.io/nextcloud-releases/aio-talk:20250512_082954,ghcr.io/nextcloud-releases/aio-talk-recording:20250512_082954 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[2]="nextcloud:31.0.2-fpm-alpine,mirror.gcr.io/nextcloud:31.0.7-fpm-alpine" image_update_map[3]="nextcloud/aio-imaginary:20250325_084656,mirror.gcr.io/nextcloud/aio-imaginary:20250811_115851" image_update_map[4]="nginx:1.27.4-alpine,mirror.gcr.io/nginx:1.28.0-alpine" image_update_map[5]="ghcr.io/nextcloud-releases/aio-talk:20250512_082954,ghcr.io/nextcloud-releases/aio-talk:20250811_115851" image_update_map[6]="ghcr.io/nextcloud-releases/aio-talk-recording:20250512_082954,ghcr.io/nextcloud-releases/aio-talk-recording:20250811_115851" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfNextcloudFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud return ;; 10) newVer=v12 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/nextcloud:31.0.7-fpm-alpine,mirror.gcr.io/nextcloud/aio-imaginary:20250811_115851,mirror.gcr.io/nginx:1.28.0-alpine,ghcr.io/nextcloud-releases/aio-talk:20250811_115851,ghcr.io/nextcloud-releases/aio-talk-recording:20250811_115851 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/nextcloud:31.0.7-fpm-alpine,mirror.gcr.io/nextcloud:32.0.2-fpm-alpine" image_update_map[3]="mirror.gcr.io/nextcloud/aio-imaginary:20250811_115851,mirror.gcr.io/nextcloud/aio-imaginary:20251128_084214" image_update_map[4]="mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[5]="ghcr.io/nextcloud-releases/aio-talk:20250811_115851,ghcr.io/nextcloud-releases/aio-talk:20251128_084214" image_update_map[6]="ghcr.io/nextcloud-releases/aio-talk-recording:20250811_115851,ghcr.io/nextcloud-releases/aio-talk-recording:20251128_084214" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfNextcloudV11AddHaRP perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud docker exec -u www-data nextcloud-app php occ app_api:daemon:register --net dock-ext --compute_device cpu --set-default --harp --harp_frp_address nextcloud-harp:8782 --harp_shared_key $NEXTCLOUD_HARP_SHARED_KEY harp_proxy_docker "HaRP Proxy (Docker)" docker-install http nextcloud-harp:8780 https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN return ;; 11) newVer=v12 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/nextcloud:32.0.1-fpm-alpine,mirror.gcr.io/nextcloud/aio-imaginary:20251031_122139,mirror.gcr.io/nginx:1.29.3-alpine,ghcr.io/nextcloud-releases/aio-talk:20251031_122139,ghcr.io/nextcloud-releases/aio-talk-recording:20251031_122139,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.0 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/nextcloud:32.0.1-fpm-alpine,mirror.gcr.io/nextcloud:32.0.2-fpm-alpine" image_update_map[3]="mirror.gcr.io/nextcloud/aio-imaginary:20251031_122139,mirror.gcr.io/nextcloud/aio-imaginary:20251128_084214" image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[5]="ghcr.io/nextcloud-releases/aio-talk:20251031_122139,ghcr.io/nextcloud-releases/aio-talk:20251128_084214" image_update_map[6]="ghcr.io/nextcloud-releases/aio-talk-recording:20251031_122139,ghcr.io/nextcloud-releases/aio-talk-recording:20251128_084214" image_update_map[7]="ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.0,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.0" ;; 12) newVer=v13 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/nextcloud:32.0.2-fpm-alpine,mirror.gcr.io/nextcloud/aio-imaginary:20251128_084214,mirror.gcr.io/nginx:1.29.3-alpine,ghcr.io/nextcloud-releases/aio-talk:20251128_084214,ghcr.io/nextcloud-releases/aio-talk-recording:20251128_084214,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.0 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[2]="mirror.gcr.io/nextcloud:32.0.2-fpm-alpine,mirror.gcr.io/nextcloud:33.0.2-fpm-alpine" image_update_map[3]="mirror.gcr.io/nextcloud/aio-imaginary:20251128_084214,mirror.gcr.io/nextcloud/aio-imaginary:20260409_094910" image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[5]="ghcr.io/nextcloud-releases/aio-talk:20251128_084214,ghcr.io/nextcloud-releases/aio-talk:20260409_094910" image_update_map[6]="ghcr.io/nextcloud-releases/aio-talk-recording:20251128_084214,ghcr.io/nextcloud-releases/aio-talk-recording:20260409_094910" image_update_map[7]="ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.0,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateNextcloudV13 perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud return ;; 13) newVer=v14 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/nextcloud:33.0.2-fpm-alpine,mirror.gcr.io/nextcloud/aio-imaginary:20260409_094910,mirror.gcr.io/nginx:1.30.0-alpine,ghcr.io/nextcloud-releases/aio-talk:20260409_094910,ghcr.io/nextcloud-releases/aio-talk-recording:20260409_094910,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.2,ghcr.io/cbcoutinho/nextcloud-mcp-server:0.72.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[2]="mirror.gcr.io/nextcloud:33.0.2-fpm-alpine,mirror.gcr.io/nextcloud:34.0.1-fpm-alpine" image_update_map[3]="mirror.gcr.io/nextcloud/aio-imaginary:20260409_094910,mirror.gcr.io/nextcloud/aio-imaginary:20260702_083546" image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.31.2-alpine" image_update_map[5]="ghcr.io/nextcloud-releases/aio-talk:20260409_094910,ghcr.io/nextcloud-releases/aio-talk:20260702_083546" image_update_map[6]="ghcr.io/nextcloud-releases/aio-talk-recording:20260409_094910,ghcr.io/nextcloud-releases/aio-talk-recording:20260702_083546" image_update_map[7]="ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.2,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.4.2" image_update_map[8]="ghcr.io/cbcoutinho/nextcloud-mcp-server:0.72.1,ghcr.io/homeserverhq/nextcloud-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateNextcloudV14 perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud return ;; 14) newVer=v14 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/nextcloud:34.0.1-fpm-alpine,mirror.gcr.io/nextcloud/aio-imaginary:20260702_083546,mirror.gcr.io/nginx:1.31.2-alpine,ghcr.io/nextcloud-releases/aio-talk:20260702_083546,ghcr.io/nextcloud-releases/aio-talk-recording:20260702_083546,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.4.2,ghcr.io/homeserverhq/nextcloud-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[2]="mirror.gcr.io/nextcloud:34.0.1-fpm-alpine,mirror.gcr.io/nextcloud:34.0.1-fpm-alpine" image_update_map[3]="mirror.gcr.io/nextcloud/aio-imaginary:20260702_083546,mirror.gcr.io/nextcloud/aio-imaginary:20260702_083546" image_update_map[4]="mirror.gcr.io/nginx:1.31.2-alpine,mirror.gcr.io/nginx:1.31.2-alpine" image_update_map[5]="ghcr.io/nextcloud-releases/aio-talk:20260702_083546,ghcr.io/nextcloud-releases/aio-talk:20260702_083546" image_update_map[6]="ghcr.io/nextcloud-releases/aio-talk-recording:20260702_083546,ghcr.io/nextcloud-releases/aio-talk-recording:20260702_083546" image_update_map[7]="ghcr.io/nextcloud/nextcloud-appapi-harp:v0.4.2,ghcr.io/nextcloud/nextcloud-appapi-harp:v0.4.2" image_update_map[8]="ghcr.io/homeserverhq/nextcloud-mcp:v2,ghcr.io/homeserverhq/nextcloud-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" performMaintenanceNextcloud } function performMaintenanceNextcloud() { echo -e "\n========================================================================" echo -e " Performing maintenance on Nextcloud, this could take a few minutes..." echo -e "========================================================================\n" waitForStack "ready to handle connections" nextcloud-app 3600 15 if [ "$isStackReady" = "true" ]; then docker exec -u www-data nextcloud-app php occ upgrade > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ db:add-missing-indices > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ maintenance:repair --include-expensive > /dev/null 2>&1 docker exec -u www-data nextcloud-app php cron.php > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ dav:sync-system-addressbook > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ federation:sync-addressbooks > /dev/null 2>&1 else echo "ERROR: There was a problem with the Nextcloud update..." fi echo "Maintenance complete!" } function mfNextcloudUpdateNGINXConfig() { outputNGINXConfigNextcloud } function mfNextcloudAddTalkHPB() { set +e initServicesCredentials pullImage $(getScriptImageByContainerName nextcloud-talkhpb) pullImage $(getScriptImageByContainerName nextcloud-talkrecord) outputComposeNextcloud set +e grep "PYTHON_VER=" $HOME/nextcloud.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "PYTHON_VER=python3.13" >> $HOME/nextcloud.env fi mkdir -p $HSHQ_STACKS_DIR/nextcloud/record inner_block="" inner_block=$inner_block">>https://$SUB_NCTALKHPB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>route /standalone-signaling/* {\n" inner_block=$inner_block">>>>>>>>uri strip_prefix /standalone-signaling\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-talkhpb:8081 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>>>header_up X-Real-IP {remote_host}\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NCTALKHPB $MANAGETLS_NCTALKHPB "$is_integrate_hshq" $NETDEFAULT_NCTALKHPB "$inner_block" insertSubAuthelia $SUB_NCTALKHPB.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_NCTALKRECORD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://nextcloud-talkrecord:1234 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NCTALKRECORD $MANAGETLS_NCTALKRECORD "$is_integrate_hshq" $NETDEFAULT_NCTALKRECORD "$inner_block" insertSubAuthelia $SUB_NCTALKRECORD.$HOMESERVER_DOMAIN bypass restartAllCaddyContainers } function mfNextcloudFixRedisCompose() { replaceRedisBlock nextcloud nextcloud-redis mirror.gcr.io/redis:8.2.0-bookworm false } function mfNextcloudV11AddHaRP() { set +e initServicesCredentials pullImage $(getScriptImageByContainerName nextcloud-harp) set +e sed -i "s/^PYTHON_VER=.*/PYTHON_VER=python3.14/" $HOME/nextcloud.env outputComposeNextcloud inner_block="" inner_block=$inner_block">>https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>rewrite /.well-known/carddav /remote.php/dav\n" inner_block=$inner_block">>>>rewrite /.well-known/caldav /remote.php/dav\n" inner_block=$inner_block">>>>rewrite /.well-known/webfinger /index.php/.well-known/webfinger\n" inner_block=$inner_block">>>>rewrite /.well-known/nodeinfo /index.php/.well-known/nodeinfo\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>route /push/* {\n" inner_block=$inner_block">>>>>>>>uri strip_prefix /push\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-push:$NEXTCLOUD_PUSH_PORT\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>route /exapps/* {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-harp:$NEXTCLOUD_HARP_PORT\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy https://nextcloud-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NEXTCLOUD $MANAGETLS_NEXTCLOUD "$is_integrate_hshq" $NETDEFAULT_NEXTCLOUD "$inner_block" set +e } function mfUpdateNextcloudV13() { set +e pullImage $IMG_NEXTCLOUD_MCP if [ $? -ne 0 ]; then return 1 fi mkdir -p $HSHQ_STACKS_DIR/nextcloud/mcp/data mkdir -p $HSHQ_STACKS_DIR/nextcloud/mcp/oauth rm -f $HOME/nextcloud-compose.yml cat < $HOME/nextcloud-compose.yml $STACK_VERSION_PREFIX nextcloud v13 services: nextcloud-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: nextcloud-db hostname: nextcloud-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped security_opt: - no-new-privileges:true shm_size: 256mb depends_on: - nextcloud-redis networks: - int-nextcloud-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/dbexport:/dbexport environment: - TZ=$TZ - POSTGRES_DB=$NEXTCLOUD_DATABASE_NAME - POSTGRES_USER=$NEXTCLOUD_DATABASE_USER - POSTGRES_PASSWORD=$NEXTCLOUD_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.nextcloud-hourly-db.schedule=@every 1h" - "ofelia.job-exec.nextcloud-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.nextcloud-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.nextcloud-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.nextcloud-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.nextcloud-hourly-db.email-from=Nextcloud Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.nextcloud-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.nextcloud-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.nextcloud-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.nextcloud-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.nextcloud-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.nextcloud-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.nextcloud-monthly-db.email-from=Nextcloud Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.nextcloud-monthly-db.mail-only-on-error=false" nextcloud-app: image: mirror.gcr.io/nextcloud:33.0.2-fpm-alpine container_name: nextcloud-app hostname: nextcloud-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/www.conf:/usr/local/etc/php-fpm.d/www.conf - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-app.crt:/opt/ssl/nextcloud-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-app.key:/opt/ssl/nextcloud-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-app.conf:/etc/openldap/ldap.conf - \${PORTAINER_HSHQ_STACKS_DIR}/shared:/shared - v-nextcloud-data:/var/www/html environment: - OVERWRITEHOST=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITECLIURL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITEPROTOCOL=https nextcloud-cron: image: mirror.gcr.io/nextcloud:33.0.2-fpm-alpine container_name: nextcloud-cron hostname: nextcloud-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true entrypoint: /cron.sh depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-cron.crt:/opt/ssl/nextcloud-cron.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-cron.key:/opt/ssl/nextcloud-cron.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-cron.conf:/etc/openldap/ldap.conf - \${PORTAINER_HSHQ_STACKS_DIR}/shared:/shared - v-nextcloud-data:/var/www/html nextcloud-push: image: mirror.gcr.io/nextcloud:33.0.2-fpm-alpine container_name: nextcloud-push hostname: nextcloud-push restart: unless-stopped entrypoint: /var/www/html/custom_apps/notify_push/bin/x86_64/notify_push /var/www/html/config/config.php security_opt: - no-new-privileges:true depends_on: - nextcloud-db - nextcloud-redis - nextcloud-app networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-push.crt:/opt/ssl/nextcloud-push.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-push.key:/opt/ssl/nextcloud-push.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-push.conf:/etc/openldap/ldap.conf - v-nextcloud-data:/var/www/html environment: - PORT=$NEXTCLOUD_PUSH_PORT - NEXTCLOUD_URL=http://nextcloud-web nextcloud-imaginary: image: mirror.gcr.io/nextcloud/aio-imaginary:20260409_094910 container_name: nextcloud-imaginary hostname: nextcloud-imaginary restart: unless-stopped command: -concurrency 50 -enable-url-source cap_add: - SYS_NICE depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net environment: - PORT=$NEXTCLOUD_IMAGINARY_PORT nextcloud-talkhpb: image: ghcr.io/nextcloud-releases/aio-talk:20260409_094910 container_name: nextcloud-talkhpb hostname: nextcloud-talkhpb restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TALK_HOST=$SUB_COTURN.$HOMESERVER_DOMAIN - TALK_PORT=$COTURN_PRIMARY_PORT - TURN_SECRET=$COTURN_STATIC_SECRET - SIGNALING_SECRET=$NEXTCLOUD_TALKHPB_SIGNALING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro nextcloud-talkrecord: image: ghcr.io/nextcloud-releases/aio-talk-recording:20260409_094910 container_name: nextcloud-talkrecord hostname: nextcloud-talkrecord user: "122" restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TZ=\${PORTAINER_TZ} - RECORDING_SECRET=$NEXTCLOUD_TALKHPB_RECORDING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET - SKIP_VERIFY=true - HPB_DOMAIN=$SUB_NCTALKHPB.$HOMESERVER_DOMAIN shm_size: 1073741824 read_only: true tmpfs: - /conf cap_drop: - NET_RAW volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-nextcloud-record:/tmp:rw nextcloud-web: image: mirror.gcr.io/nginx:1.30.0-alpine container_name: nextcloud-web hostname: nextcloud-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-nextcloud-data:/var/www/html:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/web/nginx.conf:/etc/nginx/nginx.conf - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-web.crt:/etc/nginx/certs/cert.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-web.key:/etc/nginx/certs/cert.key nextcloud-harp: image: ghcr.io/nextcloud/nextcloud-appapi-harp:v0.3.2 container_name: nextcloud-harp hostname: nextcloud-harp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /var/run/docker.sock:/var/run/docker.sock environment: - HP_SHARED_KEY=$NEXTCLOUD_HARP_SHARED_KEY - NC_INSTANCE_URL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN nextcloud-mcp: image: ghcr.io/cbcoutinho/nextcloud-mcp-server:0.72.1 container_name: nextcloud-mcp hostname: nextcloud-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: ["--transport", "streamable-http"] depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-nextcloud-mcp-data:/app/data - v-nextcloud-mcp-oauth:/app/.oauth - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/mcp/settings.toml:/app/settings.toml:ro nextcloud-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: nextcloud-redis hostname: nextcloud-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $NEXTCLOUD_REDIS_PASSWORD --appendonly no networks: - int-nextcloud-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-nextcloud-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/app v-nextcloud-record: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/record v-nextcloud-mcp-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/mcp/data v-nextcloud-mcp-oauth: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/mcp/oauth networks: dock-proxy-net: name: dock-proxy external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-nextcloud-net: driver: bridge internal: true ipam: driver: default EOFNC outputMCPConfigNextcloud outputNGINXConfigNextcloud rm -f $HSHQ_STACKS_DIR/nextcloud/www.conf cat < $HSHQ_STACKS_DIR/nextcloud/www.conf [www] user = www-data group = www-data listen = 0.0.0.0:9000 pm = dynamic pm.max_children = 200 pm.start_servers = 50 pm.min_spare_servers = 50 pm.max_spare_servers = 150 EOFNC inner_block="" inner_block=$inner_block">>https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>rewrite /.well-known/carddav /remote.php/dav\n" inner_block=$inner_block">>>>rewrite /.well-known/caldav /remote.php/dav\n" inner_block=$inner_block">>>>rewrite /.well-known/webfinger /index.php/.well-known/webfinger\n" inner_block=$inner_block">>>>rewrite /.well-known/nodeinfo /index.php/.well-known/nodeinfo\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>route /push/* {\n" inner_block=$inner_block">>>>>>>>uri strip_prefix /push\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-push:$NEXTCLOUD_PUSH_PORT\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>route /exapps/* {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://nextcloud-harp:$NEXTCLOUD_HARP_PORT\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy https://nextcloud-web {\n" inner_block=$inner_block">>>>>>>>header_up Authorization {header.Authorization}\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NEXTCLOUD $MANAGETLS_NEXTCLOUD "$is_integrate_hshq" $NETDEFAULT_NEXTCLOUD "$inner_block" restartAllCaddyContainers set +e grep -q "forwarded_for_headers" $HSHQ_STACKS_DIR/nextcloud/app/config/config.php if [ $? -ne 0 ]; then sudo sed -i "/overwriteprotocol.*/a\ 'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED_PROTO', 'HTTP_X_REAL_IP']," $HSHQ_STACKS_DIR/nextcloud/app/config/config.php fi grep -q ENABLE_LOGIN_FLOW $HOME/nextcloud.env if [ $? -ne 0 ]; then echo "NEXTCLOUD_HOST=http://nextcloud-web:80" >> $HOME/nextcloud.env echo "NEXTCLOUD_MCP_SERVER_URL=http://nextcloud-mcp:8000" >> $HOME/nextcloud.env echo "NEXTCLOUD_PUBLIC_ISSUER_URL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN" >> $HOME/nextcloud.env echo "ENABLE_LOGIN_FLOW=false" >> $HOME/nextcloud.env echo "TOKEN_ENCRYPTION_KEY=$NEXTCLOUD_TOKEN_ENCRYPTION_KEY" >> $HOME/nextcloud.env echo "TOKEN_STORAGE_DB=/app/data/tokens.db" >> $HOME/nextcloud.env echo "ENABLE_MULTI_USER_BASIC_AUTH=true" >> $HOME/nextcloud.env fi addMCPServerLiteLLM "nextcloud" "nc" "http://nextcloud-mcp:80/mcp" http none "" } function mfUpdateNextcloudV14() { cat < $HOME/nextcloud-compose.yml $STACK_VERSION_PREFIX nextcloud $(getScriptStackVersion nextcloud) services: nextcloud-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: nextcloud-db hostname: nextcloud-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped security_opt: - no-new-privileges:true shm_size: 256mb depends_on: - nextcloud-redis networks: - int-nextcloud-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/dbexport:/dbexport environment: - TZ=$TZ - POSTGRES_DB=$NEXTCLOUD_DATABASE_NAME - POSTGRES_USER=$NEXTCLOUD_DATABASE_USER - POSTGRES_PASSWORD=$NEXTCLOUD_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.nextcloud-hourly-db.schedule=@every 1h" - "ofelia.job-exec.nextcloud-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.nextcloud-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.nextcloud-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.nextcloud-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.nextcloud-hourly-db.email-from=Nextcloud Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.nextcloud-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.nextcloud-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.nextcloud-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.nextcloud-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.nextcloud-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.nextcloud-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.nextcloud-monthly-db.email-from=Nextcloud Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.nextcloud-monthly-db.mail-only-on-error=false" nextcloud-app: image: mirror.gcr.io/nextcloud:34.0.1-fpm-alpine container_name: nextcloud-app hostname: nextcloud-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/www.conf:/usr/local/etc/php-fpm.d/www.conf - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-app.crt:/opt/ssl/nextcloud-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-app.key:/opt/ssl/nextcloud-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-app.conf:/etc/openldap/ldap.conf - \${PORTAINER_HSHQ_STACKS_DIR}/shared:/shared - v-nextcloud-data:/var/www/html environment: - OVERWRITEHOST=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITECLIURL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - OVERWRITEPROTOCOL=https nextcloud-cron: image: mirror.gcr.io/nextcloud:34.0.1-fpm-alpine container_name: nextcloud-cron hostname: nextcloud-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true entrypoint: /cron.sh depends_on: - nextcloud-db - nextcloud-redis networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net - dock-proxy-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-cron.crt:/opt/ssl/nextcloud-cron.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-cron.key:/opt/ssl/nextcloud-cron.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-cron.conf:/etc/openldap/ldap.conf - \${PORTAINER_HSHQ_STACKS_DIR}/shared:/shared - v-nextcloud-data:/var/www/html nextcloud-push: image: mirror.gcr.io/nextcloud:34.0.1-fpm-alpine container_name: nextcloud-push hostname: nextcloud-push restart: unless-stopped entrypoint: /var/www/html/custom_apps/notify_push/bin/x86_64/notify_push /var/www/html/config/config.php security_opt: - no-new-privileges:true depends_on: - nextcloud-db - nextcloud-redis - nextcloud-app networks: - int-nextcloud-net - dock-ldap-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-push.crt:/opt/ssl/nextcloud-push.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-push.key:/opt/ssl/nextcloud-push.key - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/ldap-push.conf:/etc/openldap/ldap.conf - v-nextcloud-data:/var/www/html environment: - PORT=$NEXTCLOUD_PUSH_PORT - NEXTCLOUD_URL=http://nextcloud-web nextcloud-imaginary: image: mirror.gcr.io/nextcloud/aio-imaginary:20260702_083546 container_name: nextcloud-imaginary hostname: nextcloud-imaginary restart: unless-stopped command: -concurrency 50 -enable-url-source cap_add: - SYS_NICE depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net environment: - PORT=$NEXTCLOUD_IMAGINARY_PORT nextcloud-talkhpb: image: ghcr.io/nextcloud-releases/aio-talk:20260702_083546 container_name: nextcloud-talkhpb hostname: nextcloud-talkhpb restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TALK_HOST=$SUB_COTURN.$HOMESERVER_DOMAIN - TALK_PORT=$COTURN_PRIMARY_PORT - TURN_SECRET=$COTURN_STATIC_SECRET - SIGNALING_SECRET=$NEXTCLOUD_TALKHPB_SIGNALING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro nextcloud-talkrecord: image: ghcr.io/nextcloud-releases/aio-talk-recording:20260702_083546 container_name: nextcloud-talkrecord hostname: nextcloud-talkrecord user: "122" restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net environment: - NC_DOMAIN=$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN - TZ=\${PORTAINER_TZ} - RECORDING_SECRET=$NEXTCLOUD_TALKHPB_RECORDING_SECRET - INTERNAL_SECRET=$NEXTCLOUD_TALKHPB_INTERNAL_SECRET - SKIP_VERIFY=true - HPB_DOMAIN=$SUB_NCTALKHPB.$HOMESERVER_DOMAIN shm_size: 1073741824 read_only: true tmpfs: - /conf cap_drop: - NET_RAW volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-nextcloud-record:/tmp:rw nextcloud-web: image: mirror.gcr.io/nginx:1.31.2-alpine container_name: nextcloud-web hostname: nextcloud-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-nextcloud-data:/var/www/html:ro - \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/web/nginx.conf:/etc/nginx/nginx.conf - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-web.crt:/etc/nginx/certs/cert.crt - \${PORTAINER_HSHQ_SSL_DIR}/nextcloud-web.key:/etc/nginx/certs/cert.key nextcloud-harp: image: ghcr.io/nextcloud/nextcloud-appapi-harp:v0.4.2 container_name: nextcloud-harp hostname: nextcloud-harp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - nextcloud-app networks: - int-nextcloud-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /var/run/docker.sock:/var/run/docker.sock environment: - HP_SHARED_KEY=$NEXTCLOUD_HARP_SHARED_KEY - NC_INSTANCE_URL=https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN nextcloud-mcp: image: ghcr.io/homeserverhq/nextcloud-mcp:v2 container_name: nextcloud-mcp hostname: nextcloud-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-nextcloud-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro nextcloud-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: nextcloud-redis hostname: nextcloud-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $NEXTCLOUD_REDIS_PASSWORD --appendonly no networks: - int-nextcloud-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-nextcloud-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/app v-nextcloud-record: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/record v-nextcloud-mcp-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/mcp/data v-nextcloud-mcp-oauth: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/nextcloud/mcp/oauth networks: dock-proxy-net: name: dock-proxy external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-nextcloud-net: driver: bridge internal: true ipam: driver: default EOFNC cat < $HOME/nextcloud.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL REDIS_TLS_ENABLED=no PYTHON_VER=$NCTALKRECORD_PYTHON_VER NEXTCLOUD_BASE_URL=http://nextcloud-web:80 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFNC } function addSharedDirsNextcloud() { docker exec -u www-data nextcloud-app php occ --no-warnings app:install files_external docker exec -u www-data nextcloud-app php occ --no-warnings app:enable files_external curID=$(docker exec -u www-data nextcloud-app php occ files_external:create Images local null::null -c datadir=/shared/Images/\$user/Images | rev | cut -d" " -f1 | rev) docker exec -u www-data -it nextcloud-app php occ files_external:option $curID enable_sharing true > /dev/null 2>&1 docker exec -u www-data -it nextcloud-app php occ files_external:applicable --remove-all $curID > /dev/null 2>&1 curID=$(docker exec -u www-data nextcloud-app php occ files_external:create SharedConsume local null::null -c datadir=/shared/SharedConsume/\$user/SharedConsume | rev | cut -d" " -f1 | rev) docker exec -u www-data -it nextcloud-app php occ files_external:applicable --remove-all $curID > /dev/null 2>&1 curID=$(docker exec -u www-data nextcloud-app php occ files_external:create SharedProcessed local null::null -c datadir=/shared/SharedProcessed | rev | cut -d" " -f1 | rev) docker exec -u www-data -it nextcloud-app php occ files_external:option $curID readonly true > /dev/null 2>&1 docker exec -u www-data -it nextcloud-app php occ files_external:option $curID enable_sharing true > /dev/null 2>&1 docker exec -u www-data -it nextcloud-app php occ files_external:applicable --remove-all $curID > /dev/null 2>&1 curID=$(docker exec -u www-data nextcloud-app php occ files_external:create PersonalConsume local null::null -c datadir=/shared/PersonalConsume/\$user/PersonalConsume | rev | cut -d" " -f1 | rev) docker exec -u www-data -it nextcloud-app php occ files_external:applicable --remove-all $curID > /dev/null 2>&1 curID=$(docker exec -u www-data nextcloud-app php occ files_external:create PersonalTranscribeInput local null::null -c datadir=/shared/PersonalTranscribeInput/\$user | rev | cut -d" " -f1 | rev) docker exec -u www-data -it nextcloud-app php occ files_external:applicable --remove-all $curID > /dev/null 2>&1 curID=$(docker exec -u www-data nextcloud-app php occ files_external:create PersonalProcessed local null::null -c datadir=/shared/PersonalProcessed/\$user/PersonalProcessed | rev | cut -d" " -f1 | rev) docker exec -u www-data -it nextcloud-app php occ files_external:option $curID readonly true > /dev/null 2>&1 docker exec -u www-data -it nextcloud-app php occ files_external:option $curID enable_sharing true > /dev/null 2>&1 docker exec -u www-data -it nextcloud-app php occ files_external:applicable --remove-all $curID > /dev/null 2>&1 docker exec -u www-data nextcloud-app php occ files:scan --all > /dev/null 2>&1 } # Jitsi function installJitsi() { set +e is_integrate_hshq=$1 # Remember to check if directory exists checkDeleteStackAndDirectory jitsi "Jitsi" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName jitsi-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName jitsi-prosody) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName jitsi-jicofo) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName jitsi-jvb) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/jitsi outputConfigJitsi generateCert jitsi-web jitsi-web installStack jitsi jitsi-web "starting services" $HOME/jitsi.env retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Jitsi installed, sleeping 5 seconds..." sleep 5 inner_block="" inner_block=$inner_block">>https://$SUB_JITSI.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://jitsi-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_JITSI $MANAGETLS_JITSI "$is_integrate_hshq" $NETDEFAULT_JITSI "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll jitsi "$FMLNAME_JITSI" $USERTYPE_JITSI "https://$SUB_JITSI.$HOMESERVER_DOMAIN" "jitsi.png" "$(getHeimdallOrderFromSub $SUB_JITSI $USERTYPE_JITSI)" restartAllCaddyContainers fi } function outputConfigJitsi() { cat < $HOME/jitsi-compose.yml $STACK_VERSION_PREFIX jitsi $(getScriptStackVersion jitsi) services: jitsi-web: image: $(getScriptImageByContainerName jitsi-web) container_name: jitsi-web hostname: jitsi-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-jitsi-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/jitsi/web:/config:Z - \${PORTAINER_HSHQ_STACKS_DIR}/jitsi/web/crontabs:/var/spool/cron/crontabs:Z - \${PORTAINER_HSHQ_STACKS_DIR}/jitsi/transcripts:/usr/share/jitsi-meet/transcripts:Z - \${PORTAINER_HSHQ_SSL_DIR}/jitsi-web.crt:/config/keys/cert.crt - \${PORTAINER_HSHQ_SSL_DIR}/jitsi-web.key:/config/keys/cert.key jitsi-prosody: image: $(getScriptImageByContainerName jitsi-prosody) container_name: jitsi-prosody hostname: jitsi-prosody restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: int-jitsi-net: aliases: - xmpp.meet.jitsi volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/jitsi/prosody/config:/config:Z - \${PORTAINER_HSHQ_STACKS_DIR}/jitsi/prosody/prosody-plugins-custom:/prosody-plugins-custom:Z jitsi-jicofo: image: $(getScriptImageByContainerName jitsi-jicofo) container_name: jitsi-jicofo hostname: jitsi-jicofo restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-jitsi-net depends_on: - jitsi-prosody volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/jitsi/jicofo:/config:Z jitsi-jvb: image: $(getScriptImageByContainerName jitsi-jvb) container_name: jitsi-jvb hostname: jitsi-jvb restart: unless-stopped env_file: stack.env depends_on: - jitsi-prosody networks: - int-jitsi-net - dock-privateip-net ports: - "$JITSI_MEET_PORT:${JITSI_MEET_PORT}/udp" - "$JITSI_JVB_PORT:$JITSI_JVB_PORT" - "127.0.0.1:$JITSI_COLIBRI_PORT:$JITSI_COLIBRI_PORT" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/jitsi/jvb:/config:Z networks: int-jitsi-net: driver: bridge internal: true ipam: driver: default dock-proxy-net: name: dock-proxy external: true dock-privateip-net: name: dock-privateip external: true EOFJT outputEnvJitsi } function outputEnvJitsi() { cat < $HOME/jitsi.env JVB_COLIBRI_PORT=$JITSI_COLIBRI_PORT PUBLIC_URL=https://$SUB_JITSI.$HOMESERVER_DOMAIN JVB_ADVERTISE_IPS=\${PORTAINER_ALL_INTERFACE_IPS} DOCKER_HOST_ADDRESS=\${PORTAINER_HOMESERVER_HOST_PRIMARY_INTERFACE_IP} ENABLE_LETSENCRYPT=0 ENABLE_AUTH=0 JICOFO_AUTH_PASSWORD=$(openssl rand -hex 16) JVB_AUTH_PASSWORD=$(openssl rand -hex 16) JIGASI_XMPP_PASSWORD=$(openssl rand -hex 16) JIBRI_RECORDER_PASSWORD=$(openssl rand -hex 16) JIBRI_XMPP_PASSWORD=$(openssl rand -hex 16) ENABLE_P2P=false JVB_STUN_SERVERS=none JVB_DISABLE_STUN=true EOFJT } function performUpdateJitsi() { perform_stack_name=jitsi prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v9 curImageList=jitsi/jicofo:stable-8719,jitsi/jvb:stable-8719,jitsi/prosody:stable-8719,jitsi/web:stable-8719 image_update_map[0]="jitsi/jicofo:stable-8719,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-8719,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-8719,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-8719,jitsi/web:stable-10655" ;; 2) newVer=v9 curImageList=jitsi/jicofo:stable-9111,jitsi/jvb:stable-9111,jitsi/prosody:stable-9111,jitsi/web:stable-9111 image_update_map[0]="jitsi/jicofo:stable-9111,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-9111,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-9111,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-9111,jitsi/web:stable-10655" ;; 3) newVer=v9 curImageList=jitsi/jicofo:stable-9220,jitsi/jvb:stable-9220,jitsi/prosody:stable-9220,jitsi/web:stable-9220 image_update_map[0]="jitsi/jicofo:stable-9220,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-9220,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-9220,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-9220,jitsi/web:stable-10655" ;; 4) newVer=v9 curImageList=jitsi/jicofo:stable-9258,jitsi/jvb:stable-9258,jitsi/prosody:stable-9258,jitsi/web:stable-9258 image_update_map[0]="jitsi/jicofo:stable-9258,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-9258,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-9258,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-9258,jitsi/web:stable-10655" ;; 5) newVer=v9 curImageList=jitsi/jicofo:stable-9584,jitsi/jvb:stable-9584,jitsi/prosody:stable-9584,jitsi/web:stable-9584 image_update_map[0]="jitsi/jicofo:stable-9584,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-9584,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-9584,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-9584,jitsi/web:stable-10655" ;; 6) newVer=v9 curImageList=jitsi/jicofo:stable-9779,jitsi/jvb:stable-9779,jitsi/prosody:stable-9779,jitsi/web:stable-9779 image_update_map[0]="jitsi/jicofo:stable-9779,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-9779,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-9779,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-9779,jitsi/web:stable-10655" ;; 7) newVer=v9 curImageList=jitsi/jicofo:stable-10133-1,jitsi/jvb:stable-10133-1,jitsi/prosody:stable-10133-1,jitsi/web:stable-10133-1 image_update_map[0]="jitsi/jicofo:stable-10133-1,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-10133-1,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-10133-1,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-10133-1,jitsi/web:stable-10655" ;; 8) newVer=v9 curImageList=jitsi/jicofo:stable-10431,jitsi/jvb:stable-10431,jitsi/prosody:stable-10431,jitsi/web:stable-10431 image_update_map[0]="jitsi/jicofo:stable-10431,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-10431,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-10431,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-10431,jitsi/web:stable-10655" ;; 9) newVer=v9 curImageList=jitsi/jicofo:stable-10655,jitsi/jvb:stable-10655,jitsi/prosody:stable-10655,jitsi/web:stable-10655 image_update_map[0]="jitsi/jicofo:stable-10655,jitsi/jicofo:stable-10655" image_update_map[1]="jitsi/jvb:stable-10655,jitsi/jvb:stable-10655" image_update_map[2]="jitsi/prosody:stable-10655,jitsi/prosody:stable-10655" image_update_map[3]="jitsi/web:stable-10655,jitsi/web:stable-10655" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Matrix function installMatrix() { set +e is_integrate_hshq=$1 # Only install if directory does not exist checkDeleteStackAndDirectory matrix "Matrix" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName matrix-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName matrix-synapse) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName matrix-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName matrix-element-private) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/matrix mkdir $HSHQ_STACKS_DIR/matrix/db mkdir $HSHQ_STACKS_DIR/matrix/dbexport mkdir $HSHQ_STACKS_DIR/matrix/synapse mkdir $HSHQ_STACKS_DIR/matrix/element chmod 777 $HSHQ_STACKS_DIR/matrix/dbexport initServicesCredentials if [ -z "$MATRIX_REDIS_PASSWORD" ]; then MATRIX_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MATRIX_REDIS_PASSWORD $MATRIX_REDIS_PASSWORD fi if [ -z "$MATRIX_SYNAPSE_REGISTRATION_SECRET" ]; then MATRIX_SYNAPSE_REGISTRATION_SECRET=$(pwgen -c -n 64 1) updateConfigVar MATRIX_SYNAPSE_REGISTRATION_SECRET $MATRIX_SYNAPSE_REGISTRATION_SECRET fi if [ -z "$MATRIX_SYNAPSE_MACAROON_SECRET" ]; then MATRIX_SYNAPSE_MACAROON_SECRET=$(pwgen -c -n 64 1) updateConfigVar MATRIX_SYNAPSE_MACAROON_SECRET $MATRIX_SYNAPSE_MACAROON_SECRET fi if [ -z "$MATRIX_SYNAPSE_FORM_SECRET" ]; then MATRIX_SYNAPSE_FORM_SECRET=$(pwgen -c -n 64 1) updateConfigVar MATRIX_SYNAPSE_FORM_SECRET $MATRIX_SYNAPSE_FORM_SECRET fi outputConfigMatrix generateCert matrix-synapse matrix-synapse installStack matrix matrix-synapse "Starting synapse with args" $HOME/matrix.env retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Matrix installed, sleeping 3 seconds..." sleep 3 addReadOnlyUserToDatabase Matrix postgres matrix-db $MATRIX_DATABASE_NAME $MATRIX_DATABASE_USER $MATRIX_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/matrix/dbexport $MATRIX_DATABASE_READONLYUSER $MATRIX_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_MATRIX_SYNAPSE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>encode zstd gzip\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy \"/_matrix/*\" https://matrix-synapse:8448 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy \"/_synapse/client/*\" https://matrix-synapse:8448 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MATRIX_SYNAPSE $MANAGETLS_MATRIX_SYNAPSE "$is_integrate_hshq" $NETDEFAULT_MATRIX_SYNAPSE "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_MATRIX_ELEMENT_PRIVATE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>encode zstd gzip\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://matrix-element-private {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MATRIX_ELEMENT_PRIVATE $MANAGETLS_MATRIX_ELEMENT_PRIVATE "$is_integrate_hshq" $NETDEFAULT_MATRIX_ELEMENT_PRIVATE "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_MATRIX_ELEMENT_PUBLIC.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>encode zstd gzip\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://matrix-element-public {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MATRIX_ELEMENT_PUBLIC $MANAGETLS_MATRIX_ELEMENT_PUBLIC "$is_integrate_hshq" $NETDEFAULT_MATRIX_ELEMENT_PUBLIC "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll matrix "$FMLNAME_MATRIX_ELEMENT_PRIVATE" $USERTYPE_MATRIX_ELEMENT_PRIVATE "https://$SUB_MATRIX_ELEMENT_PRIVATE.$HOMESERVER_DOMAIN" "element-private.png" "$(getHeimdallOrderFromSub $SUB_MATRIX_ELEMENT_PRIVATE $USERTYPE_MATRIX_ELEMENT_PRIVATE)" insertEnableSvcAll matrix "$FMLNAME_MATRIX_ELEMENT_PUBLIC" $USERTYPE_MATRIX_ELEMENT_PUBLIC "https://$SUB_MATRIX_ELEMENT_PUBLIC.$HOMESERVER_DOMAIN" "element-public.png" "$(getHeimdallOrderFromSub $SUB_MATRIX_ELEMENT_PUBLIC $USERTYPE_MATRIX_ELEMENT_PUBLIC)" restartAllCaddyContainers fi } function outputConfigMatrix() { cat < $HOME/matrix-compose.yml $STACK_VERSION_PREFIX matrix $(getScriptStackVersion matrix) services: matrix-db: image: $(getScriptImageByContainerName matrix-db) container_name: matrix-db hostname: matrix-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-matrix-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/matrix/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/matrix/dbexport:/dbexport environment: - POSTGRES_DB=$MATRIX_DATABASE_NAME - POSTGRES_USER=$MATRIX_DATABASE_USER - POSTGRES_PASSWORD=$MATRIX_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.matrix-hourly-db.schedule=@every 1h" - "ofelia.job-exec.matrix-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.matrix-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.matrix-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.matrix-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.matrix-hourly-db.email-from=Matrix Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.matrix-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.matrix-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.matrix-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.matrix-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.matrix-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.matrix-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.matrix-monthly-db.email-from=Matrix Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.matrix-monthly-db.mail-only-on-error=false" matrix-synapse: image: $(getScriptImageByContainerName matrix-synapse) container_name: matrix-synapse hostname: matrix-synapse restart: unless-stopped env_file: stack.env depends_on: - matrix-db - matrix-redis networks: - int-matrix-net - dock-ldap-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/matrix-synapse.crt:/data/matrix-synapse.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/matrix-synapse.key:/data/matrix-synapse.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/matrix/synapse:/data matrix-redis: image: $(getScriptImageByContainerName matrix-redis) container_name: matrix-redis hostname: matrix-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $MATRIX_REDIS_PASSWORD --appendonly no networks: - int-matrix-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro matrix-element-private: image: $(getScriptImageByContainerName matrix-element-private) container_name: matrix-element-private hostname: matrix-element-private restart: unless-stopped env_file: stack.env networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/matrix/element/element-private-config.json:/app/config.json matrix-element-public: image: $(getScriptImageByContainerName matrix-element-public) container_name: matrix-element-public hostname: matrix-element-public restart: unless-stopped env_file: stack.env networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/matrix/element/element-public-config.json:/app/config.json networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-ldap-net: name: dock-ldap external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true int-matrix-net: driver: bridge internal: true ipam: driver: default EOFJT cat < $HOME/matrix.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' SYNAPSE_CONFIG_DIR=/data SYNAPSE_CONFIG_PATH=/data/homeserver.yaml REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL REDIS_TLS_ENABLED=no EOFJT cat < $HSHQ_STACKS_DIR/matrix/synapse/homeserver.yaml server_name: $HOMESERVER_DOMAIN pid_file: /data/homeserver.pid web_client_location: https://$SUB_MATRIX_ELEMENT_PUBLIC.$HOMESERVER_DOMAIN/ public_baseurl: https://$SUB_MATRIX_SYNAPSE.$HOMESERVER_DOMAIN/ limit_profile_requests_to_users_who_share_rooms: true include_profile_data_on_invite: false allow_public_rooms_over_federation: false default_room_version: "9" auto_join_rooms: - "#Main:$HOMESERVER_DOMAIN" listeners: - port: 8008 tls: false type: http x_forwarded: true resources: - names: [client, federation] compress: false - port: 8448 tls: true type: http x_forwarded: true resources: - names: [client, federation] compress: false ip_range_whitelist: - '10.0.0.0/8' max_avatar_size: 10M redaction_retention_period: 1d tls_certificate_path: /data/matrix-synapse.crt tls_private_key_path: /data/matrix-synapse.key #federation_domain_whitelist: # - $HOMESERVER_DOMAIN allow_profile_lookup_over_federation: false allow_device_name_lookup_over_federation: false database: name: psycopg2 txn_limit: 10000 args: user: $MATRIX_DATABASE_USER password: $MATRIX_DATABASE_USER_PASSWORD database: $MATRIX_DATABASE_NAME host: matrix-db port: 5432 cp_min: 5 cp_max: 10 log_config: "/data/${HOMESERVER_DOMAIN}.log.config" enable_media_repo: true media_store_path: "/data/media_store" max_upload_size: 512M dynamic_thumbnails: true thumbnail_sizes: - width: 32 height: 32 method: crop - width: 96 height: 96 method: crop - width: 320 height: 240 method: scale - width: 640 height: 480 method: scale - width: 800 height: 600 method: scale url_preview_accept_language: - en-US;q=0.9 enable_registration: false session_lifetime: 720h registration_shared_secret: "$MATRIX_SYNAPSE_REGISTRATION_SECRET" macaroon_secret_key: "$MATRIX_SYNAPSE_MACAROON_SECRET" form_secret: "$MATRIX_SYNAPSE_FORM_SECRET" allow_guest_access: false enable_set_displayname: true enable_set_avatar_url: true enable_3pid_changes: false enable_metrics: false report_stats: false signing_key_path: "/data/${HOMESERVER_DOMAIN}.signing.key" trusted_key_servers: - server_name: "matrix.org" encryption_enabled_by_default_for_room_type: off enable_group_creation: true group_creation_prefix: "unofficial_" password_providers: - module: "ldap_auth_provider.LdapAuthProvider" config: enabled: true uri: "ldap://ldapserver:389" start_tls: true base: "ou=people,$LDAP_BASE_DN" attributes: uid: "uid" mail: "mail" name: "givenName" bind_dn: $LDAP_READONLY_USER_BIND_DN bind_password: $LDAP_READONLY_USER_PASSWORD filter: "(&(objectClass=person)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))" tls_options: validate: false local_certificate_file: /data/matrix-synapse.crt local_private_key_file: /data/matrix-synapse.key password_config: enabled: true localdb_enabled: false user_directory: enabled: true search_all_users: true prefer_local_users: true enable_room_list_search: true redis: enabled: true host: matrix-redis port: 6379 password: $MATRIX_REDIS_PASSWORD admin_contact: 'mailto:$EMAIL_ADMIN_EMAIL_ADDRESS' email: smtp_host: $SMTP_HOSTNAME smtp_port: SMTP_HOSTPORT require_transport_security: true enable_tls: true notif_from: "Matrix Homeserver <$EMAIL_ADMIN_EMAIL_ADDRESS>" app_name: "$HOMESERVER_NAME Secure Communications" enable_notifs: true notif_for_new_users: true #client_base_url: "http://localhost/riot" #validation_token_lifetime: 15m #invite_client_location: https://app.element.io #subjects: #message_from_person_in_room: "[%(app)s] You have a message on %(app)s from %(person)s in the %(room)s room..." #message_from_person: "[%(app)s] You have a message on %(app)s from %(person)s..." #messages_from_person: "[%(app)s] You have messages on %(app)s from %(person)s..." #messages_in_room: "[%(app)s] You have messages on %(app)s in the %(room)s room..." #messages_in_room_and_others: "[%(app)s] You have messages on %(app)s in the %(room)s room and others..." #messages_from_person_and_others: "[%(app)s] You have messages on %(app)s from %(person)s and others..." #invite_from_person_to_room: "[%(app)s] %(person)s has invited you to join the %(room)s room on %(app)s..." #invite_from_person: "[%(app)s] %(person)s has invited you to chat on %(app)s..." #password_reset: "[%(server_name)s] Password reset" #email_validation: "[%(server_name)s] Validate your email" EOFJT cat < $HSHQ_STACKS_DIR/matrix/synapse/${HOMESERVER_DOMAIN}.log.config version: 1 formatters: precise: format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' handlers: console: class: logging.StreamHandler formatter: precise loggers: synapse.storage.SQL: level: INFO root: level: INFO handlers: [console] disable_existing_loggers: false EOFJT outputMatrixElementJSONConfig "$SUB_JITSI.$HOMESERVER_DOMAIN" $HSHQ_STACKS_DIR/matrix/element/element-private-config.json outputMatrixElementJSONConfig "meet.element.io" $HSHQ_STACKS_DIR/matrix/element/element-public-config.json } function performUpdateMatrix() { perform_stack_name=matrix prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.90.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.40 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="matrixdotorg/synapse:v1.90.0,matrixdotorg/synapse:v1.102.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[3]="vectorim/element-web:v1.11.40,vectorim/element-web:v1.11.61" ;; 2) newVer=v4 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.98.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.52 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="matrixdotorg/synapse:v1.98.0,matrixdotorg/synapse:v1.102.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[3]="vectorim/element-web:v1.11.52,vectorim/element-web:v1.11.61" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.100.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.57 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="matrixdotorg/synapse:v1.100.0,matrixdotorg/synapse:v1.102.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[3]="vectorim/element-web:v1.11.57,vectorim/element-web:v1.11.61" ;; 4) newVer=v7 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.102.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.61 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="matrixdotorg/synapse:v1.102.0,matrixdotorg/synapse:v1.127.1" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[3]="vectorim/element-web:v1.11.61,vectorim/element-web:v1.11.96" ;; 5) newVer=v7 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.112.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.72 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="matrixdotorg/synapse:v1.112.0,matrixdotorg/synapse:v1.127.1" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[3]="vectorim/element-web:v1.11.72,vectorim/element-web:v1.11.96" ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.118.0,bitnami/redis:7.0.5,vectorim/element-web:v1.11.83 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="matrixdotorg/synapse:v1.118.0,matrixdotorg/synapse:v1.127.1" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[3]="vectorim/element-web:v1.11.83,vectorim/element-web:v1.11.96" ;; 7) newVer=v9 curImageList=postgres:15.0-bullseye,matrixdotorg/synapse:v1.127.1,bitnami/redis:7.4.2,vectorim/element-web:v1.11.96 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="matrixdotorg/synapse:v1.127.1,mirror.gcr.io/matrixdotorg/synapse:v1.143.0" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="vectorim/element-web:v1.11.96,mirror.gcr.io/vectorim/element-web:v1.12.6" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfMatrixFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 8) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/matrixdotorg/synapse:v1.136.0,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/vectorim/element-web:v1.11.109 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/matrixdotorg/synapse:v1.136.0,mirror.gcr.io/matrixdotorg/synapse:v1.143.0" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/vectorim/element-web:v1.11.109,mirror.gcr.io/vectorim/element-web:v1.12.6" ;; 9) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/matrixdotorg/synapse:v1.143.0,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/vectorim/element-web:v1.12.6 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/matrixdotorg/synapse:v1.143.0,mirror.gcr.io/matrixdotorg/synapse:v1.143.0" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/vectorim/element-web:v1.12.6,mirror.gcr.io/vectorim/element-web:v1.12.6" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function outputMatrixElementJSONConfig() { jitsi_url=$1 output_file=$2 cat < $output_file { "default_server_config": { "m.homeserver": { "base_url": "https://$SUB_MATRIX_SYNAPSE.$HOMESERVER_DOMAIN", "server_name": "$HOMESERVER_DOMAIN" }, "m.identity_server": { "base_url": "https://vector.im" } }, "disable_custom_urls": true, "disable_guests": true, "disable_login_language_selector": true, "disable_3pid_login": true, "brand": "Element", "integrations_ui_url": "https://scalar.vector.im/", "integrations_rest_url": "https://scalar.vector.im/api", "integrations_widgets_urls": [ "https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/api", "https://scalar-staging.vector.im/_matrix/integrations/v1", "https://scalar-staging.vector.im/api", "https://scalar-staging.riot.im/scalar/api" ], "hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web", "bug_report_endpoint_url": "https://element.io/bugreports/submit", "defaultCountryCode": "US", "uisi_autorageshake_app": "element-auto-uisi", "showLabsSettings": true, "jitsi": { "preferred_domain": "$jitsi_url" }, "piwik": { "url": "https://piwik.riot.im/", "siteId": 1, "policyUrl": "https://element.io/cookie-policy" }, "roomDirectory": { "servers": [ "$HOMESERVER_DOMAIN" ] }, "enable_presence_by_hs_url": { "https://matrix.org": false, "https://matrix-client.matrix.org": false }, "terms_and_conditions_links": [ { "url": "https://element.io/privacy", "text": "Privacy Policy" }, { "url": "https://element.io/cookie-policy", "text": "Cookie Policy" } ], "hostSignup": { "brand": "$HOMESERVER_NAME", "cookiePolicyUrl": "https://element.io/cookie-policy", "domains": [ "matrix.org" ], "privacyPolicyUrl": "https://element.io/privacy", "termsOfServiceUrl": "https://element.io/terms-of-service", "url": "https://ems.element.io/element-home/in-app-loader" }, "sentry": { "dsn": "https://029a0eb289f942508ae0fb17935bd8c5@sentry.matrix.org/6", "environment": "develop" }, "posthog": { "projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", "apiHost": "https://posthog.hss.element.io" }, "features": { "feature_spotlight": true, "feature_new_spinner": "labs", "feature_pinning": "labs", "feature_custom_status": "labs", "feature_custom_tags": "labs", "feature_state_counters": "labs" }, "default_federate": false, "default_theme": "dark", "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx" } EOFEL } function mfMatrixFixRedisCompose() { replaceRedisBlock matrix matrix-redis mirror.gcr.io/redis:8.2.0-bookworm false } function fixMatrixV8() { set +e matrix_stack_id=$(getStackID matrix) if [ -z "$matrix_stack_id" ]; then return fi stackStatus=$(getStackStatusByName matrix) matrix_compose=$HSHQ_STACKS_DIR/portainer/compose/$matrix_stack_id/docker-compose.yml matrix_stack_firstline=$(sudo sed -n 1p $matrix_compose) matrix_stack_ver=$(getVersionFromComposeLine "$matrix_stack_firstline") sudo grep -q "matrixdotorg/synapse:v1.127.1" $matrix_compose rtVal=$? if ! [ "$matrix_stack_ver" = "8" ] || ! [ $rtVal -eq 0 ]; then return fi echo "Fixing matrix stack container images..." pullImage mirror.gcr.io/matrixdotorg/synapse:v1.136.0 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi updateStackEnv matrix mfMatrixV8Fix if ! [ "$stackStatus" = "1" ];then startStopStack matrix stop fi } function mfMatrixV8Fix() { sed -i "s/matrixdotorg\/synapse:v1.127.1/mirror.gcr.io\/matrixdotorg\/synapse:v1.136.0/g" $HOME/matrix-compose.yml } # Wikijs function installWikijs() { set +e is_integrate_hshq=$1 # Test if directory exists checkDeleteStackAndDirectory wikijs "Wikijs" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName wikijs-web) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName wikijs-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName wikijs-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/wikijs mkdir $HSHQ_STACKS_DIR/wikijs/config mkdir $HSHQ_STACKS_DIR/wikijs/db mkdir $HSHQ_STACKS_DIR/wikijs/dbexport mkdir $HSHQ_STACKS_DIR/wikijs/web chmod 777 $HSHQ_STACKS_DIR/wikijs/dbexport initServicesCredentials outputConfigWikijs generateCert wikijs-web wikijs-web installStack wikijs wikijs-web "HTTP Server on port" $HOME/wikijs.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi if ! [ "$WIKIJS_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_WIKIJS Admin Login Info" -b "$FMLNAME_WIKIJS Admin Email Address: $WIKIJS_ADMIN_EMAIL_ADDRESS\n$FMLNAME_WIKIJS Admin Password: $WIKIJS_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" WIKIJS_INIT_ENV=true updateConfigVar WIKIJS_INIT_ENV $WIKIJS_INIT_ENV fi echo "Wikijs installed, sleeping 3 seconds..." sleep 3 addReadOnlyUserToDatabase Wiki.js postgres wikijs-db $WIKIJS_DATABASE_NAME $WIKIJS_DATABASE_USER $WIKIJS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/wikijs/dbexport $WIKIJS_DATABASE_READONLYUSER $WIKIJS_DATABASE_READONLYUSER_PASSWORD addMCPServerLiteLLM "wikijs" "wjs" "http://wikijs-mcp:80/mcp" http none "" initializeSiteWikijs inner_block="" inner_block=$inner_block">>https://$SUB_WIKIJS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://wikijs-web:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_WIKIJS $MANAGETLS_WIKIJS "$is_integrate_hshq" $NETDEFAULT_WIKIJS "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll wikijs "$FMLNAME_WIKIJS" $USERTYPE_WIKIJS "https://$SUB_WIKIJS.$HOMESERVER_DOMAIN" "wikijs.png" "$(getHeimdallOrderFromSub $SUB_WIKIJS $USERTYPE_WIKIJS)" restartAllCaddyContainers fi } function outputConfigWikijs() { cat < $HOME/wikijs-compose.yml $STACK_VERSION_PREFIX wikijs $(getScriptStackVersion wikijs) services: wikijs-db: image: $(getScriptImageByContainerName wikijs-db) container_name: wikijs-db hostname: wikijs-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-wikijs-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wikijs/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wikijs/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.wikijs-hourly-db.schedule=@every 1h" - "ofelia.job-exec.wikijs-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.wikijs-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wikijs-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wikijs-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wikijs-hourly-db.email-from=Wikijs Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wikijs-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.wikijs-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.wikijs-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.wikijs-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wikijs-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wikijs-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wikijs-monthly-db.email-from=Wikijs Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wikijs-monthly-db.mail-only-on-error=false" wikijs-web: image: $(getScriptImageByContainerName wikijs-web) container_name: wikijs-web hostname: wikijs-web depends_on: - wikijs-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-wikijs-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_SSL_DIR}/wikijs-web.crt:/certs/wikijs-web.crt - \${PORTAINER_HSHQ_SSL_DIR}/wikijs-web.key:/certs/wikijs-web.key - v-wikijs-web:/wiki/data/content wikijs-mcp: image: $(getScriptImageByContainerName wikijs-mcp) container_name: wikijs-mcp hostname: wikijs-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-wikijs-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-wikijs-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wikijs/web networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true dock-dbs-net: name: dock-dbs external: true int-wikijs-net: driver: bridge internal: true ipam: driver: default EOFWJ cat < $HOME/wikijs.env UID=$USERID GID=$GROUPID POSTGRES_DB=$WIKIJS_DATABASE_NAME POSTGRES_USER=$WIKIJS_DATABASE_USER POSTGRES_PASSWORD=$WIKIJS_DATABASE_USER_PASSWORD DB_TYPE=postgres DB_HOST=wikijs-db DB_PORT=5432 DB_NAME=$WIKIJS_DATABASE_NAME DB_USER=$WIKIJS_DATABASE_USER DB_PASS=$WIKIJS_DATABASE_USER_PASSWORD WIKIJS_BASE_URL=http://wikijs-web:3000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false WIKIJS_PUBLIC_URL=https://$SUB_WIKIJS.$HOMESERVER_DOMAIN EOFWJ } function initializeSiteWikijs() { set +e docker exec wikijs-web curl -sS -X POST http://localhost:3000/finalize \ -H 'Content-Type: application/json' \ -d "{ \"adminEmail\": \"$WIKIJS_ADMIN_EMAIL_ADDRESS\", \"adminPassword\": \"$WIKIJS_ADMIN_PASSWORD\", \"adminPasswordConfirm\": \"$WIKIJS_ADMIN_PASSWORD\", \"siteUrl\": \"https://$SUB_WIKIJS.$HOMESERVER_DOMAIN\", \"telemetry\": false }" > /dev/null 2>&1 echo "Wikijs site initialized..." sleep 5 waitForContainerLogString wikijs-web 3 60 "Syncing locales with Graph endpoint" local jq_node='let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).data.authentication.login.jwt)}catch(e){process.exit(1)}})' local resp jwt resp=$(docker exec wikijs-web curl -s "http://localhost:3000/graphql" -H 'Content-Type: application/json' \ -d "{\"query\":\"mutation { authentication { login(strategy: \\\"local\\\", username: \\\"$WIKIJS_ADMIN_EMAIL_ADDRESS\\\", password: \\\"$WIKIJS_ADMIN_PASSWORD\\\") { jwt responseResult { succeeded } } } }\"}") jwt=$(docker exec -e resp="$resp" -e jq_node="$jq_node" wikijs-web sh -c 'printf "%s" "$resp" | node -e "$jq_node"') if [ -z "$jwt" ]; then echo "ERROR: login failed" return 1 fi docker exec wikijs-web curl -s "http://localhost:3000/graphql" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $jwt" \ -d '{"query":"mutation { authentication { setApiState(enabled: true) { responseResult { succeeded } } } }"}' >/dev/null 2>&1 if [ -z "$(docker exec wikijs-db psql -U wikijs-user -d wikijsdb -tA -c \ "select 1 from pages where path = 'home' and \"localeCode\" = 'en'")" ]; then docker exec wikijs-web curl -s "http://localhost:3000/graphql" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $jwt" \ -d '{"query":"mutation { pages { create( content: \"Home\", description: \"\", editor: \"markdown\", isPublished: true, isPrivate: false, locale: \"en\", path: \"home\", tags: [], title: \"Home\" ) { responseResult { succeeded } } } }"}' >/dev/null 2>&1 fi docker exec wikijs-web curl -s "http://localhost:3000/graphql" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $jwt" \ -d '{"query":"mutation { theming { setConfig(theme: \"default\", iconset: \"mdi\", darkMode: true) { responseResult { succeeded } } } }"}' >/dev/null 2>&1 resp=$(docker exec wikijs-web curl -s "http://localhost:3000/graphql" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $jwt" \ -d '{"query":"query { authentication { apiKeys { id name isRevoked expiration } } }"}') local id dtNow dtNow=$(date -u +%Y-%m-%dT%H:%M:%S) id=$(docker exec -e resp="$resp" -e dtNow="$dtNow" wikijs-web node -e ' let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{ const now=process.env.dtNow||""; try{ const ks=(JSON.parse(process.env.resp).data.authentication.apiKeys||[]); const k=ks.find(x=>!x.isRevoked && (!x.expiration || String(x.expiration)>=now)); console.log(k?k.id:""); }catch(e){process.exit(1)} });') if [ -n "$id" ]; then docker exec wikijs-db psql -U wikijs-user -d wikijsdb -tA -c "select key from \"apiKeys\" where id = $id" return fi resp=$(docker exec wikijs-web curl -s "http://localhost:3000/graphql" -H 'Content-Type: application/json' \ -H "Authorization: Bearer $jwt" \ -d "{\"query\":\"mutation { authentication { createApiKey(name: \\\"MCP\\\", expiration: \\\"88y\\\", fullAccess: true) { key responseResult { succeeded message } } } }\"}") WIKIJS_ADMIN_API_KEY=$(docker exec -e resp="$resp" wikijs-web node -e ' let s=""; try { const data = JSON.parse(process.env.resp); console.log(data.data.authentication.createApiKey.key || ""); } catch(e) { process.exit(1); }') updateConfigVar WIKIJS_ADMIN_API_KEY $WIKIJS_ADMIN_API_KEY set -e } function performUpdateWikijs() { perform_stack_name=wikijs prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=postgres:15.0-bullseye,requarks/wiki:2.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="requarks/wiki:2.5,requarks/wiki:2.5.301" ;; 2) newVer=v5 curImageList=postgres:15.0-bullseye,requarks/wiki:2.5.301 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="requarks/wiki:2.5.301,mirror.gcr.io/requarks/wiki:2.5.308" ;; 3) newVer=v5 curImageList=postgres:15.0-bullseye,requarks/wiki:2.5.305 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="requarks/wiki:2.5.305,mirror.gcr.io/requarks/wiki:2.5.308" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,requarks/wiki:2.5.307 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="requarks/wiki:2.5.307,mirror.gcr.io/requarks/wiki:2.5.308" ;; 5) newVer=v6 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/requarks/wiki:2.5.308 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/requarks/wiki:2.5.308,mirror.gcr.io/requarks/wiki:2.5.314" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfWikijsV6Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v6 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/requarks/wiki:2.5.314,ghcr.io/homeserverhq/wikijs-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/requarks/wiki:2.5.314,mirror.gcr.io/requarks/wiki:2.5.314" image_update_map[2]="ghcr.io/homeserverhq/wikijs-mcp:v2,ghcr.io/homeserverhq/wikijs-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfWikijsV6Update() { cat < $HOME/wikijs-compose.yml $STACK_VERSION_PREFIX wikijs v6 services: wikijs-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: wikijs-db hostname: wikijs-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-wikijs-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wikijs/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wikijs/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.wikijs-hourly-db.schedule=@every 1h" - "ofelia.job-exec.wikijs-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.wikijs-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wikijs-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wikijs-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wikijs-hourly-db.email-from=Wikijs Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wikijs-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.wikijs-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.wikijs-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.wikijs-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wikijs-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wikijs-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wikijs-monthly-db.email-from=Wikijs Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wikijs-monthly-db.mail-only-on-error=false" wikijs-web: image: mirror.gcr.io/requarks/wiki:2.5.314 container_name: wikijs-web hostname: wikijs-web depends_on: - wikijs-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-wikijs-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_SSL_DIR}/wikijs-web.crt:/certs/wikijs-web.crt - \${PORTAINER_HSHQ_SSL_DIR}/wikijs-web.key:/certs/wikijs-web.key - v-wikijs-web:/wiki/data/content wikijs-mcp: image: ghcr.io/homeserverhq/wikijs-mcp:v2 container_name: wikijs-mcp hostname: wikijs-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-wikijs-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-wikijs-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wikijs/web networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true dock-dbs-net: name: dock-dbs external: true int-wikijs-net: driver: bridge internal: true ipam: driver: default EOFWJ cat < $HOME/wikijs.env UID=$USERID GID=$GROUPID POSTGRES_DB=$WIKIJS_DATABASE_NAME POSTGRES_USER=$WIKIJS_DATABASE_USER POSTGRES_PASSWORD=$WIKIJS_DATABASE_USER_PASSWORD DB_TYPE=postgres DB_HOST=wikijs-db DB_PORT=5432 DB_NAME=$WIKIJS_DATABASE_NAME DB_USER=$WIKIJS_DATABASE_USER DB_PASS=$WIKIJS_DATABASE_USER_PASSWORD WIKIJS_BASE_URL=http://wikijs-web:3000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false WIKIJS_PUBLIC_URL=https://$SUB_WIKIJS.$HOMESERVER_DOMAIN EOFWJ addMCPServerLiteLLM "wikijs" "wjs" "http://wikijs-mcp:80/mcp" http none "" } # Duplicati function installDuplicati() { set +e is_integrate_hshq=$1 # Evaluate whether directory exists checkDeleteStackAndDirectory duplicati "Duplicati" cdRes=$? if [ $cdRes -ne 0 ]; then echo "ERROR: Duplicati directory exists" exit 1 fi pullImage $(getScriptImageByContainerName duplicati) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain Duplicati docker image" exit 1 fi set -e mkdir $HSHQ_STACKS_DIR/duplicati mkdir $HSHQ_STACKS_DIR/duplicati/config mkdir $HSHQ_NONBACKUP_DIR/duplicati mkdir $HSHQ_NONBACKUP_DIR/duplicati/restore initServicesCredentials echo "Installing Duplicati..." outputConfigDuplicati rm -f $HOME/duplicati.env cat < $HOME/duplicati.env PUID=0 PGID=0 SETTINGS_ENCRYPTION_KEY=$DUPLICATI_SETTINGS_ENCRYPTION_KEY CLI_ARGS=--webservice-password=$DUPLICATI_ADMIN_PASSWORD EOFDP generateCert duplicati duplicati installStack duplicati duplicati "\[ls.io-init\] done" $HOME/duplicati.env 5 retval=$? if [ $retval -ne 0 ]; then echo "ERROR: There was a problem installing Duplicati" exit $retval fi echo "Duplicati installed, sleeping 5 seconds..." sleep 5 startStopStack duplicati stop db_name=$HSHQ_STACKS_DIR/duplicati/config/Duplicati-server.sqlite sudo sqlite3 $db_name "INSERT INTO Option(BackupID,Filter,Name,Value) VALUES(-1,'','--accept-any-ssl-certificate','true');" sudo sqlite3 $db_name "INSERT INTO Option(BackupID,Filter,Name,Value) VALUES(-1,'','--send-mail-from','Duplicati $(getAdminEmailName)<$EMAIL_ADMIN_EMAIL_ADDRESS>');" sudo sqlite3 $db_name "INSERT INTO Option(BackupID,Filter,Name,Value) VALUES(-1,'','--send-mail-to','$EMAIL_ADMIN_EMAIL_ADDRESS');" sudo sqlite3 $db_name "INSERT INTO Option(BackupID,Filter,Name,Value) VALUES(-1,'','--send-mail-url','smtp://$SMTP_HOSTNAME:$SMTP_HOSTPORT?starttls=never');" sudo sqlite3 $db_name "INSERT INTO Option(BackupID,Filter,Name,Value) VALUES(-1,'','--send-mail-level','all');" sudo sqlite3 $db_name "INSERT INTO Option(BackupID,Filter,Name,Value) VALUES(-2,'','startup-delay','45s');" set +e updateStackEnv duplicati outputEnvDuplicati set -e inner_block="" inner_block=$inner_block">>https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://duplicati:8200 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DUPLICATI $MANAGETLS_DUPLICATI "$is_integrate_hshq" $NETDEFAULT_DUPLICATI "$inner_block" } function outputConfigDuplicati() { rm -f $HOME/duplicati-compose.yml cat < $HOME/duplicati-compose.yml $STACK_VERSION_PREFIX duplicati $(getScriptStackVersion duplicati) services: duplicati: image: $(getScriptImageByContainerName duplicati) container_name: duplicati hostname: duplicati restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/duplicati/config:/config - \${PORTAINER_HSHQ_NONBACKUP_DIR}/duplicati/restore:/restore - \${PORTAINER_HSHQ_BACKUP_DIR}:/backups - \${PORTAINER_HSHQ_DATA_DIR}:/source networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true EOFDP outputEnvDuplicati } function outputEnvDuplicati() { rm -f $HOME/duplicati.env cat < $HOME/duplicati.env PUID=0 PGID=0 SETTINGS_ENCRYPTION_KEY=$DUPLICATI_SETTINGS_ENCRYPTION_KEY EOFDP } function performUpdateDuplicati() { perform_stack_name=duplicati prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=linuxserver/duplicati:2.0.7 image_update_map[0]="linuxserver/duplicati:2.0.7,linuxserver/duplicati:2.0.8" ;; 2) newVer=v4 curImageList=linuxserver/duplicati:2.0.8 image_update_map[0]="linuxserver/duplicati:2.0.8,linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls246" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfDuplicatiAddSettingsEncryptionKey perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v4 curImageList=linuxserver/duplicati:2.1.0 image_update_map[0]="linuxserver/duplicati:2.1.0,linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls246" ;; 4) newVer=v7 curImageList=linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls246 image_update_map[0]="linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls246,mirror.gcr.io/linuxserver/duplicati:v2.2.0.1_stable_2025-11-09-ls269" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfDuplicatiUpdateEnv perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v7 curImageList=linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls246 image_update_map[0]="linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls246,mirror.gcr.io/linuxserver/duplicati:v2.2.0.1_stable_2025-11-09-ls269" ;; 6) newVer=v7 curImageList=mirror.gcr.io/linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls256 image_update_map[0]="mirror.gcr.io/linuxserver/duplicati:v2.1.0.5_stable_2025-03-04-ls256,mirror.gcr.io/linuxserver/duplicati:v2.2.0.1_stable_2025-11-09-ls269" ;; 7) newVer=v8 curImageList=mirror.gcr.io/linuxserver/duplicati:v2.2.0.1_stable_2025-11-09-ls269 image_update_map[0]="mirror.gcr.io/linuxserver/duplicati:v2.2.0.1_stable_2025-11-09-ls269,mirror.gcr.io/linuxserver/duplicati:v2.3.0.4_stable_2026-07-09-ls300" ;; 8) newVer=v8 curImageList=mirror.gcr.io/linuxserver/duplicati:v2.3.0.4_stable_2026-07-09-ls300 image_update_map[0]="mirror.gcr.io/linuxserver/duplicati:v2.3.0.4_stable_2026-07-09-ls300,mirror.gcr.io/linuxserver/duplicati:v2.3.0.4_stable_2026-07-09-ls300" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfDuplicatiUpdateEnv() { set +e initServicesCredentials set +e outputEnvDuplicati } function mfDuplicatiAddSettingsEncryptionKey() { mfdasek_curE=${-//[^e]/} set +e grep "SETTINGS_ENCRYPTION_KEY=" $HOME/duplicati.env > /dev/null 2>&1 if [ $? -ne 0 ]; then initServicesCredentials echo "SETTINGS_ENCRYPTION_KEY=$DUPLICATI_SETTINGS_ENCRYPTION_KEY" >> $HOME/duplicati.env fi if ! [ -z "$mfdasek_curE" ]; then set -e fi } # Mastodon function installMastodon() { set +e is_integrate_hshq=$1 # Test if directory exists checkDeleteStackAndDirectory mastodon "Mastodon" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi set -e pullImage $(getScriptImageByContainerName mastodon-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName mastodon-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName mastodon-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName mastodon-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName mastodon-streaming) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName mastodon-sidekiq) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/mastodon mkdir $HSHQ_STACKS_DIR/mastodon/db mkdir $HSHQ_STACKS_DIR/mastodon/dbexport mkdir $HSHQ_STACKS_DIR/mastodon/elasticsearch mkdir $HSHQ_STACKS_DIR/mastodon/system mkdir $HSHQ_STACKS_DIR/mastodon/web sudo chown 991:991 $HSHQ_STACKS_DIR/mastodon/system chmod 777 $HSHQ_STACKS_DIR/mastodon/dbexport mkdir $HSHQ_NONBACKUP_DIR/mastodon mkdir $HSHQ_NONBACKUP_DIR/mastodon/static MASTODON_SECRET_KEY_BASE=$(openssl rand -hex 64) MASTODON_OTP_SECRET=$(openssl rand -hex 64) openssl ecparam -name prime256v1 -genkey -noout -out $HOME/vapid_private_key.pem openssl ec -in $HOME/vapid_private_key.pem -pubout -out $HOME/vapid_public_key.pem MASTODON_VAPID_PRIVATE_KEY=$(cat $HOME/vapid_private_key.pem | sed -e "1 d" -e "$ d" | tr -d "\n") MASTODON_VAPID_PUBLIC_KEY=$(cat $HOME/vapid_public_key.pem | sed -e "1 d" -e "$ d" | tr -d "\n") rm -f $HOME/vapid_private_key.pem rm -f $HOME/vapid_public_key.pem initServicesCredentials if [ -z "$MASTODON_REDIS_PASSWORD" ]; then MASTODON_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MASTODON_REDIS_PASSWORD $MASTODON_REDIS_PASSWORD fi if [ -z "$MASTODON_ELASTICSEARCH_PASSWORD" ]; then MASTODON_ELASTICSEARCH_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar MASTODON_ELASTICSEARCH_PASSWORD $MASTODON_ELASTICSEARCH_PASSWORD fi outputConfigMastodon generateCert mastodon-app mastodon-app generateCert mastodon-web mastodon-web generateCert mastodon-elasticsearch mastodon-elasticsearch outputMastdonMigrate $(getScriptImageByContainerName mastodon-db) $(getScriptImageByContainerName mastodon-redis) $(getScriptImageByContainerName mastodon-app) cp $HOME/mastodon.env $HSHQ_STACKS_DIR/mastodon/stack.env sed -i "s|^TZ=.*|TZ=${TZ}|g" $HSHQ_STACKS_DIR/mastodon/stack.env migrateMastodon install installStack mastodon mastodon-app "Listening on http" $HOME/mastodon.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Mastodon installed, sleeping 3 seconds..." sleep 3 set +e addUserMailu alias $MASTODON_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS addaccount_res=$(docker exec mastodon-app tootctl accounts create $MASTODON_ADMIN_USERNAME --email=$MASTODON_ADMIN_EMAIL_ADDRESS --confirmed --role=Admin) MASTODON_ADMIN_PASSWORD=$(echo $addaccount_res | rev | cut -d" " -f1 | rev) updateConfigVar MASTODON_ADMIN_PASSWORD $MASTODON_ADMIN_PASSWORD sendEmail -s "Mastodon Login Info" -b "Mastodon Admin Username: $MASTODON_ADMIN_EMAIL_ADDRESS\nMastodon Admin Password: $MASTODON_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" docker exec mastodon-app tootctl settings registrations close > /dev/null 2>&1 docker exec mastodon-app tootctl accounts approve $MASTODON_ADMIN_USERNAME > /dev/null 2>&1 rm -f $HOME/mastodon-compose-tmp.yml addReadOnlyUserToDatabase Mastodon postgres mastodon-db $MASTODON_DATABASE_NAME $MASTODON_DATABASE_USER $MASTODON_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/mastodon/dbexport $MASTODON_DATABASE_READONLYUSER $MASTODON_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_MASTODON.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://mastodon-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MASTODON $MANAGETLS_MASTODON "$is_integrate_hshq" $NETDEFAULT_MASTODON "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll mastodon "$FMLNAME_MASTODON" $USERTYPE_MASTODON "https://$SUB_MASTODON.$HOMESERVER_DOMAIN" "mastodon.png" "$(getHeimdallOrderFromSub $SUB_MASTODON $USERTYPE_MASTODON)" restartAllCaddyContainers fi } function outputConfigMastodon() { outputMastodonCompose cat < $HOME/mastodon.env TZ=${TZ} UID=$USERID GID=$GROUPID LOCAL_DOMAIN=$HOMESERVER_DOMAIN WEB_DOMAIN=$SUB_MASTODON.$HOMESERVER_DOMAIN SINGLE_USER_MODE=false SECRET_KEY_BASE=$MASTODON_SECRET_KEY_BASE OTP_SECRET=$MASTODON_OTP_SECRET VAPID_PRIVATE_KEY=$MASTODON_VAPID_PRIVATE_KEY VAPID_PUBLIC_KEY=$MASTODON_VAPID_PUBLIC_KEY RAILS_SERVE_STATIC_FILES=true ALLOWED_PRIVATE_ADDRESSES=10.0.0.0/8 DB_HOST=mastodon-db DB_PORT=5432 DB_NAME=$MASTODON_DATABASE_NAME DB_USER=$MASTODON_DATABASE_USER DB_PASS=$MASTODON_DATABASE_USER_PASSWORD POSTGRES_DB=$MASTODON_DATABASE_NAME POSTGRES_USER=$MASTODON_DATABASE_USER POSTGRES_PASSWORD=$MASTODON_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' REDIS_HOST=mastodon-redis REDIS_PORT=6379 REDIS_PASSWORD=$MASTODON_REDIS_PASSWORD CACHE_REDIS_HOST=mastodon-redis CACHE_REDIS_PORT=6379 CACHE_REDIS_PASSWORD=$MASTODON_REDIS_PASSWORD S3_ENABLED=false IP_RETENTION_PERIOD=31556952 SESSION_RETENTION_PERIOD=31556952 SMTP_SERVER=$SMTP_HOSTNAME SMTP_PORT=$SMTP_HOSTPORT SMTP_FROM_ADDRESS=Mastodon $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> ES_ENABLED=false ES_HOST=mastodon-elasticsearch ES_PORT=9200 ES_USER=elastic ES_PASS=$MASTODON_ELASTICSEARCH_PASSWORD LDAP_ENABLED=true LDAP_HOST=ldapserver LDAP_PORT=389 LDAP_METHOD=start_tls LDAP_TLS_NO_VERIFY=false LDAP_BASE=$LDAP_BASE_DN LDAP_BIND_DN=$LDAP_READONLY_USER_BIND_DN LDAP_PASSWORD=$LDAP_READONLY_USER_PASSWORD LDAP_UID=uid LDAP_SEARCH_FILTER=(&(|(%{uid}=%{email})(%{mail}=%{email}))(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)) ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$MASTODON_ARE_DETERMINISTIC_KEY ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$MASTODON_ARE_KEY_DERIVATION_SALT ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$MASTODON_ARE_PRIMARY_KEY EOFMD cat < $HSHQ_STACKS_DIR/mastodon/web/nginx.conf worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' '\$status \$body_bytes_sent "\$http_referer" ' '"\$http_user_agent" "\$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; # Prevent nginx HTTP Server Detection server_tokens off; keepalive_timeout 65; map \$http_upgrade \$connection_upgrade { default upgrade; '' close; } #gzip on; upstream backend { server mastodon-app:3000 fail_timeout=0; } upstream streaming { server mastodon-streaming:4000 fail_timeout=0; } proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g; server { listen 80; listen [::]:80; root /var/www/html; location /.well-known/acme-challenge/ { allow all; } location / { return 301 https://\$host\$request_uri; } } server { listen 443 ssl http2; listen [::]:443 ssl http2; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_certificate /etc/nginx/certs/cert.crt; ssl_certificate_key /etc/nginx/certs/cert.key; keepalive_timeout 70; sendfile on; client_max_body_size 256m; root /var/www/html; gzip on; gzip_disable "msie6"; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;" always; location / { try_files \$uri @proxy; } location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) { add_header Cache-Control "public, max-age=31536000, immutable"; add_header Strict-Transport-Security "max-age=31536000" always; try_files \$uri @proxy; } location /sw.js { add_header Cache-Control "public, max-age=0"; add_header Strict-Transport-Security "max-age=31536000" always; try_files \$uri @proxy; } location @proxy { proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header Proxy ""; proxy_pass_header Server; proxy_pass http://backend; proxy_buffering on; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \$connection_upgrade; proxy_cache CACHE; proxy_cache_valid 200 7d; proxy_cache_valid 410 24h; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; add_header X-Cached \$upstream_cache_status; add_header Strict-Transport-Security "max-age=31536000" always; tcp_nodelay on; } location /api/v1/streaming { proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header Proxy ""; proxy_pass http://streaming; proxy_buffering off; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \$connection_upgrade; tcp_nodelay on; } error_page 500 501 502 503 504 /500.html; } } EOFMD } function outputMastodonCompose() { cat < $HOME/mastodon-compose.yml $STACK_VERSION_PREFIX mastodon $(getScriptStackVersion mastodon) services: mastodon-db: image: $(getScriptImageByContainerName mastodon-db) container_name: mastodon-db hostname: mastodon-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-mastodon-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mastodon/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mastodon/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.mastodon-hourly-db.schedule=@every 1h" - "ofelia.job-exec.mastodon-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.mastodon-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.mastodon-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.mastodon-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.mastodon-hourly-db.email-from=Mastodon Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.mastodon-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.mastodon-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.mastodon-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.mastodon-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.mastodon-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.mastodon-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.mastodon-monthly-db.email-from=Mastodon Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.mastodon-monthly-db.mail-only-on-error=false" mastodon-redis: image: $(getScriptImageByContainerName mastodon-redis) container_name: mastodon-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $MASTODON_REDIS_PASSWORD --appendonly no networks: - int-mastodon-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro mastodon-redis-cache: image: $(getScriptImageByContainerName mastodon-redis-cache) container_name: mastodon-redis-cache restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $MASTODON_REDIS_PASSWORD --appendonly no networks: - int-mastodon-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro mastodon-app: image: $(getScriptImageByContainerName mastodon-app) container_name: mastodon-app hostname: mastodon-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: bundle exec puma -C config/puma.rb depends_on: - mastodon-db - mastodon-redis #- mastodon-elasticsearch healthcheck: test: ['CMD-SHELL',"curl -s --noproxy localhost localhost:3000/health | grep -q 'OK' || exit 1"] networks: - int-mastodon-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mastodon/system:/mastodon/public/system - v-mastodon-static:/mastodon/public mastodon-streaming: image: $(getScriptImageByContainerName mastodon-streaming) container_name: mastodon-streaming hostname: mastodon-streaming restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: node ./streaming/index.js depends_on: - mastodon-db - mastodon-redis healthcheck: test: ['CMD-SHELL', "curl -s --noproxy localhost localhost:4000/api/v1/streaming/health | grep -q 'OK' || exit 1"] networks: - int-mastodon-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro mastodon-sidekiq: image: $(getScriptImageByContainerName mastodon-sidekiq) container_name: mastodon-sidekiq hostname: mastodon-sidekiq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: bundle exec sidekiq depends_on: - mastodon-db - mastodon-redis healthcheck: test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 8' || false"] networks: - int-mastodon-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mastodon/system:/mastodon/public/system mastodon-web: image: $(getScriptImageByContainerName mastodon-web) container_name: mastodon-web hostname: mastodon-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-mastodon-net - dock-proxy-net depends_on: - mastodon-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/mastodon-web.crt:/etc/nginx/certs/cert.crt - \${PORTAINER_HSHQ_SSL_DIR}/mastodon-web.key:/etc/nginx/certs/cert.key - \${PORTAINER_HSHQ_STACKS_DIR}/mastodon/web/nginx.conf:/etc/nginx/nginx.conf - v-mastodon-static:/var/www/html:ro # mastodon-elasticsearch: # image: $(getScriptImageByContainerName mastodon-elasticsearch) # container_name: mastodon-elasticsearch # hostname: mastodon-elasticsearch # restart: unless-stopped # security_opt: # - no-new-privileges:true # healthcheck: # test: ["CMD-SHELL", "nc -z mastodon-elasticsearch 9200"] # networks: # - int-mastodon-net # - dock-ext-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_SSL_DIR}/mastodon-elasticsearch.crt:/usr/share/elasticsearch/config/certs/mastodon-elasticsearch.crt # - \${PORTAINER_HSHQ_SSL_DIR}/mastodon-elasticsearch.key:/usr/share/elasticsearch/config/certs/mastodon-elasticsearch.key # - \${PORTAINER_HSHQ_STACKS_DIR}/mastodon/elasticsearch:/usr/share/elasticsearch/data # ulimits: # memlock: # soft: -1 # hard: -1 # nofile: # soft: 65536 # hard: 65536 # environment: # - "ELASTIC_PASSWORD=$MASTODON_ELASTICSEARCH_PASSWORD" # - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # - "xpack.security.enabled=true" # - "xpack.security.transport.ssl.enabled=true" # - "xpack.security.transport.ssl.certificate=/usr/share/elasticsearch/config/certs/mastodon-elasticsearch.crt" # - "xpack.security.transport.ssl.key=/usr/share/elasticsearch/config/certs/mastodon-elasticsearch.key" # - "xpack.watcher.enabled=false" # - "xpack.graph.enabled=false" # - "xpack.ml.enabled=false" # - "ingest.geoip.downloader.enabled=false" # - "bootstrap.memory_lock=true" # - "cluster.name=es-mastodon" # - "discovery.type=single-node" volumes: v-mastodon-static: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/mastodon/static networks: dock-proxy-net: name: dock-proxy external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-mastodon-net: driver: bridge internal: true ipam: driver: default EOFMD } function performUpdateMastodon() { perform_stack_name=mastodon prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # Stack status: 1=running, 2=stopped stackStatus=$(getStackStatusByID $perform_stack_id) if [ -z "$stackStatus" ]; then is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Could not get stack status" return fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.1.6,nginx:1.23.2-alpine,elasticsearch:8.8.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="tootsuite/mastodon:v4.1.6,tootsuite/mastodon:v4.2.8" image_update_map[3]="nginx:1.23.2-alpine,nginx:1.25.3-alpine" image_update_map[4]="elasticsearch:8.8.1,elasticsearch:8.12.2" # This upgrade requires a migration prepMastodonMigrate postgres:15.0-bullseye bitnami/redis:7.0.5 tootsuite/mastodon:v4.2.3 $perform_stack_id upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfMigrateMastodon false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v4 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.2.3,nginx:1.25.3-alpine,elasticsearch:8.11.3 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="tootsuite/mastodon:v4.2.3,tootsuite/mastodon:v4.2.8" image_update_map[3]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" image_update_map[4]="elasticsearch:8.11.3,elasticsearch:8.12.2" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.2.3,nginx:1.25.3-alpine,elasticsearch:8.12.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="tootsuite/mastodon:v4.2.3,tootsuite/mastodon:v4.2.8" image_update_map[3]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" image_update_map[4]="elasticsearch:8.12.0,elasticsearch:8.12.2" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.2.8,nginx:1.25.3-alpine,elasticsearch:8.12.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="tootsuite/mastodon:v4.2.8,tootsuite/mastodon:v4.2.10" image_update_map[3]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" image_update_map[4]="elasticsearch:8.12.2,elasticsearch:8.12.2" ;; 5) newVer=v7 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.2.10,nginx:1.25.3-alpine,elasticsearch:8.12.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="tootsuite/mastodon:v4.2.10,tootsuite/mastodon:v4.3.1" image_update_map[3]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" image_update_map[4]="elasticsearch:8.12.2,elasticsearch:8.12.2" # This upgrade requires a migration prepMastodonMigrate postgres:15.0-bullseye bitnami/redis:7.0.5 tootsuite/mastodon:v4.3.1 $perform_stack_id echo "ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$MASTODON_ARE_DETERMINISTIC_KEY" >> $HSHQ_STACKS_DIR/mastodon/stack.env echo "ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$MASTODON_ARE_KEY_DERIVATION_SALT" >> $HSHQ_STACKS_DIR/mastodon/stack.env echo "ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$MASTODON_ARE_PRIMARY_KEY" >> $HSHQ_STACKS_DIR/mastodon/stack.env upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfMigrateMastodon true mfUpdateMastodonV7 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.3.1,nginx:1.25.3-alpine,elasticsearch:8.12.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[2]="tootsuite/mastodon:v4.3.1,tootsuite/mastodon:v4.3.1" image_update_map[3]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" image_update_map[4]="elasticsearch:8.12.2,elasticsearch:8.12.2" # This update simply fixes the mastodon-streaming issue, i.e. it has its own container and a few aspects of the docker compose needed an update # See https://github.com/mastodon/mastodon/pull/31554/files/51638f89b57613f01b1696b8644478ff4937937b#diff-e45e45baeda1c1e73482975a664062aa56f20c03dd9d64a827aba57775bed0d3 upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearStaticAssetsMastodon true mfUpdateMastodonV7 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,tootsuite/mastodon:v4.3.1,nginx:1.25.3-alpine,elasticsearch:8.12.2,tootsuite/mastodon-streaming:v4.3.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[2]="tootsuite/mastodon:v4.3.1,tootsuite/mastodon:v4.3.7" image_update_map[3]="nginx:1.25.3-alpine,nginx:1.27.4-alpine" image_update_map[4]="elasticsearch:8.12.2,elasticsearch:8.17.4" image_update_map[5]="tootsuite/mastodon-streaming:v4.3.1,tootsuite/mastodon-streaming:v4.3.7" ;; 8) newVer=9 curImageList=postgres:15.0-bullseye,bitnami/redis:7.4.2,tootsuite/mastodon:v4.3.7,nginx:1.27.4-alpine,elasticsearch:8.17.4,tootsuite/mastodon-streaming:v4.3.7 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[2]="tootsuite/mastodon:v4.3.7,mirror.gcr.io/tootsuite/mastodon:v4.3.11" image_update_map[3]="nginx:1.27.4-alpine,mirror.gcr.io/nginx:1.28.0-alpine" image_update_map[4]="elasticsearch:8.17.4,mirror.gcr.io/elasticsearch:9.1.1" image_update_map[5]="tootsuite/mastodon-streaming:v4.3.7,mirror.gcr.io/tootsuite/mastodon-streaming:v4.3.11" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearStaticAssetsMastodon true mfMastodonFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/tootsuite/mastodon:v4.3.11,mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/elasticsearch:9.1.1,mirror.gcr.io/tootsuite/mastodon-streaming:v4.3.11 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/tootsuite/mastodon:v4.3.11,mirror.gcr.io/tootsuite/mastodon:v4.5.2" image_update_map[3]="mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[4]="mirror.gcr.io/elasticsearch:9.1.1,mirror.gcr.io/elasticsearch:9.2.2" image_update_map[5]="mirror.gcr.io/tootsuite/mastodon-streaming:v4.3.11,mirror.gcr.io/tootsuite/mastodon-streaming:v4.5.2" # This upgrade requires a migration prepMastodonMigrate mirror.gcr.io/postgres:15.0-bullseye mirror.gcr.io/redis:8.4.0-bookworm mirror.gcr.io/tootsuite/mastodon:v4.5.2 $perform_stack_id upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfMigrateMastodon true mfMastodonV10Fix perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 10) newVer=10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/tootsuite/mastodon:v4.5.2,mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/tootsuite/mastodon-streaming:v4.5.2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/tootsuite/mastodon:v4.5.2,mirror.gcr.io/tootsuite/mastodon:v4.5.2" image_update_map[3]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[4]="mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/elasticsearch:9.2.2" image_update_map[5]="mirror.gcr.io/tootsuite/mastodon-streaming:v4.5.2,mirror.gcr.io/tootsuite/mastodon-streaming:v4.5.2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearStaticAssetsMastodon false perform_update_report="${perform_update_report}$stack_upgrade_report" } function outputMastdonMigrate() { mastodon_db_image=$1 mastodon_redis_image=$2 mastodon_app_image=$3 cat < $HSHQ_STACKS_DIR/mastodon/docker-compose.yml services: mastodon-db: image: $mastodon_db_image container_name: mastodon-db hostname: mastodon-db user: "${USERID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-mastodon-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - ${HSHQ_STACKS_DIR}/mastodon/db:/var/lib/postgresql/data mastodon-redis: image: $mastodon_redis_image container_name: mastodon-redis hostname: mastodon-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $MASTODON_REDIS_PASSWORD --appendonly no networks: - int-mastodon-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro mastodon-redis-cache: image: $mastodon_redis_image container_name: mastodon-redis-cache restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $MASTODON_REDIS_PASSWORD --appendonly no networks: - int-mastodon-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro mastodon-app: image: $mastodon_app_image container_name: mastodon-app hostname: mastodon-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: bundle exec puma -C config/puma.rb depends_on: - mastodon-db - mastodon-redis healthcheck: test: ['CMD-SHELL',"curl -s --noproxy localhost localhost:3000/health | grep -q 'OK' || exit 1"] networks: - int-mastodon-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${HSHQ_STACKS_DIR}/mastodon/system:/mastodon/public/system - v-mastodon-static:/mastodon/public volumes: v-mastodon-static: driver: local driver_opts: type: none o: bind device: ${HSHQ_NONBACKUP_DIR}/mastodon/static networks: dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true int-mastodon-net: driver: bridge internal: true ipam: driver: default EOFMD } function prepMastodonMigrate() { mastodon_db_image=$1 mastodon_redis_image=$2 mastodon_app_image=$3 mastodon_stack_id=$4 outputMastdonMigrate "$mastodon_db_image" "$mastodon_redis_image" "$mastodon_app_image" sudo cp $HSHQ_STACKS_DIR/portainer/compose/$mastodon_stack_id/stack.env $HSHQ_STACKS_DIR/mastodon/stack.env sudo chown $USERID:$USERID $HSHQ_STACKS_DIR/mastodon/stack.env sed -i "s|^TZ=.*|TZ=${TZ}|g" $HSHQ_STACKS_DIR/mastodon/stack.env } function migrateMastodon() { isInstallOrMigrate="$1" # This function assumes the Mastodon stack/containers are NOT running. echo -e "\nPerforming Mastodon database migration...this could take a few minutes\n" clearStaticAssetsMastodon cd ~ case "$isInstallOrMigrate" in install) docker compose -f $HSHQ_STACKS_DIR/mastodon/docker-compose.yml run --rm mastodon-app bundle exec rails db:setup > /dev/null 2>&1 ;; migrate) docker compose -f $HSHQ_STACKS_DIR/mastodon/docker-compose.yml run --rm mastodon-app bundle exec rails db:migrate > /dev/null 2>&1 ;; *) echo "Case not found, returning..." return ;; esac cd ~ docker compose -f $HSHQ_STACKS_DIR/mastodon/docker-compose.yml down -v rm -f $HSHQ_STACKS_DIR/mastodon/docker-compose.yml rm -f $HSHQ_STACKS_DIR/mastodon/stack.env echo -e "\nFinished mastodon database migration, sleeping 3 seconds" sleep 3 } function mfMigrateMastodon() { if [ "$stackStatus" = "1" ];then # Stack is running, stop it, then migrate startStopStack mastodon stop isStartStackFromStopped=true sleep 3 fi migrateMastodon migrate } function mfUpdateMastodonV7() { set +e rm -f $HOME/mastodon-compose.yml outputMastodonCompose pullImage $(getScriptImageByContainerName mastodon-streaming) grep "ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY" $HSHQ_STACKS_DIR/mastodon/stack.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$MASTODON_ARE_DETERMINISTIC_KEY" >> $HSHQ_STACKS_DIR/mastodon/stack.env echo "ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$MASTODON_ARE_KEY_DERIVATION_SALT" >> $HSHQ_STACKS_DIR/mastodon/stack.env echo "ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$MASTODON_ARE_PRIMARY_KEY" >> $HSHQ_STACKS_DIR/mastodon/stack.env fi } function clearStaticAssetsMastodon() { # This function assumes the Mastodon stack/containers are NOT running. echo -e "\nClearing Mastodon static assets...\n" sudo rm -fr ${HSHQ_NONBACKUP_DIR}/mastodon/static/* sudo rm -fr ${HSHQ_NONBACKUP_DIR}/mastodon/redis/* sleep 3 } function mfClearStaticAssetsMastodon() { if [ "$stackStatus" = "1" ];then # Stack is running, stop it, then clear assets startStopStack mastodon stop isStartStackFromStopped=true sleep 3 fi clearStaticAssetsMastodon } function mfMastodonFixRedisCompose() { set +e lineStart=" mastodon-redis:" lineEnd="$(grep REDIS_PASSWORD=.* $HOME/mastodon-compose.yml | tail -1)" rpass="$(echo $lineEnd | cut -d"=" -f2)" replaceText="" replaceText=$replaceText">>mastodon-redis:\n" replaceText=$replaceText">>>>image: mirror.gcr.io/redis:8.2.0-bookworm\n" replaceText=$replaceText">>>>container_name: mastodon-redis\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>env_file: stack.env\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--requirepass $rpass\n" replaceText=$replaceText">>>>>>--appendonly no\n" replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- int-mastodon-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n\n" replaceText=$replaceText">>mastodon-redis-cache:\n" replaceText=$replaceText">>>>image: mirror.gcr.io/redis:8.2.0-bookworm\n" replaceText=$replaceText">>>>container_name: mastodon-redis-cache\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>env_file: stack.env\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--requirepass $rpass\n" replaceText=$replaceText">>>>>>--appendonly no\n" replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- int-mastodon-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n" replaceTextBlockInFile "$lineStart" "$lineEnd" "$replaceText" $HOME/mastodon-compose.yml false ">" cat -s $HOME/mastodon-compose.yml > $HOME/mastodon.tmp mv $HOME/mastodon.tmp $HOME/mastodon-compose.yml sudo rm -fr $HSHQ_NONBACKUP_DIR/mastodon/redis/* } function fixMastodonV9() { set +e mastodon_stack_id=$(getStackID mastodon) if [ -z "$mastodon_stack_id" ]; then return fi stackStatus=$(getStackStatusByName mastodon) mastodon_compose=$HSHQ_STACKS_DIR/portainer/compose/$mastodon_stack_id/docker-compose.yml mastodon_stack_firstline=$(sudo sed -n 1p $mastodon_compose) mastodon_stack_ver=$(getVersionFromComposeLine "$mastodon_stack_firstline") sudo grep -q "tootsuite/mastodon:v4.3.7" $mastodon_compose rtVal=$? if ! [ "$mastodon_stack_ver" = "9" ] || ! [ $rtVal -eq 0 ]; then return fi echo "Fixing mastodon stack container images..." pullImage mirror.gcr.io/tootsuite/mastodon:v4.3.11 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi startStopStack mastodon stop clearStaticAssetsMastodon updateStackEnv mastodon mfMastodonV9Fix if ! [ "$stackStatus" = "1" ];then startStopStack mastodon stop fi } function mfMastodonV9Fix() { sed -i "s/tootsuite\/mastodon:v4.3.7/mirror.gcr.io\/tootsuite\/mastodon:v4.3.11/g" $HOME/mastodon-compose.yml } function mfMastodonV10Fix() { rm -r $HOME/mastodon-compose.yml outputMastodonCompose } # Dozzle function installDozzle() { set +e is_integrate_hshq=$1 # Have to check if stack/container exists checkDeleteStackAndDirectory dozzle "Dozzle" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi set -e pullImage $(getScriptImageByContainerName dozzle) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/dozzle initServicesCredentials outputConfigDozzle installStack dozzle dozzle "Accepting connections on" $HOME/dozzle.env retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Dozzle installed, sleeping 3 seconds..." sleep 3 inner_block="" inner_block=$inner_block">>https://$SUB_DOZZLE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://dozzle:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DOZZLE $MANAGETLS_DOZZLE "$is_integrate_hshq" $NETDEFAULT_DOZZLE "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll dozzle "$FMLNAME_DOZZLE" $USERTYPE_DOZZLE "https://$SUB_DOZZLE.$HOMESERVER_DOMAIN" "dozzle.png" "$(getHeimdallOrderFromSub $SUB_DOZZLE $USERTYPE_DOZZLE)" restartAllCaddyContainers fi } function outputConfigDozzle() { cat < $HOME/dozzle-compose.yml $STACK_VERSION_PREFIX dozzle $(getScriptStackVersion dozzle) services: dozzle: image: $(getScriptImageByContainerName dozzle) container_name: dozzle hostname: dozzle restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/run/docker.sock:/var/run/docker.sock:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dozzle/users.yml:/data/users.yml networks: dock-proxy-net: name: dock-proxy external: true EOFDZ cat < $HOME/dozzle.env DOZZLE_NO_ANALYTICS=true DOZZLE_AUTH_PROVIDER=simple EOFDZ cat < $HSHQ_STACKS_DIR/dozzle/users.yml users: $DOZZLE_USERNAME: name: "$HOMESERVER_NAME Admin" password: "$(echo -n $DOZZLE_PASSWORD | shasum -a 256 | xargs | cut -d' ' -f1)" EOFDZ } function performUpdateDozzle() { perform_stack_name=dozzle prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v7 curImageList=amir20/dozzle:v4.10.26 image_update_map[0]="amir20/dozzle:v4.10.26,mirror.gcr.io/amir20/dozzle:v6.1.1" ;; 2) newVer=v7 curImageList=amir20/dozzle:v5.8.1 image_update_map[0]="amir20/dozzle:v5.8.1,mirror.gcr.io/amir20/dozzle:v6.1.1" ;; 3) newVer=v7 curImageList=amir20/dozzle:v6.0.8 image_update_map[0]="amir20/dozzle:v6.0.8,mirror.gcr.io/amir20/dozzle:v6.1.1" ;; 4) newVer=v7 curImageList=amir20/dozzle:v6.1.1 image_update_map[0]="amir20/dozzle:v6.1.1,mirror.gcr.io/amir20/dozzle:v6.1.1" ;; 5) newVer=v7 curImageList=amir20/dozzle:v8.7.1 image_update_map[0]="amir20/dozzle:v8.7.1,mirror.gcr.io/amir20/dozzle:v6.1.1" ;; 6) # Reverting back to v6.1.1, since v8.7.1 uses a lot more CPU at idle newVer=v7 curImageList=amir20/dozzle:v6.1.1 image_update_map[0]="amir20/dozzle:v6.1.1,mirror.gcr.io/amir20/dozzle:v6.1.1" ;; 7) newVer=v7 curImageList=mirror.gcr.io/amir20/dozzle:v6.1.1 image_update_map[0]="mirror.gcr.io/amir20/dozzle:v6.1.1,mirror.gcr.io/amir20/dozzle:v6.1.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # SearxNG function installSearxNG() { set +e is_integrate_hshq=$1 # If directory exists, then delete it checkDeleteStackAndDirectory searxng "SearxNG" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName searxng-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName searxng-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName searxng-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/searxng mkdir $HSHQ_STACKS_DIR/searxng/caddy mkdir $HSHQ_STACKS_DIR/searxng/web if [ -z "$SEARXNG_REDIS_PASSWORD" ]; then SEARXNG_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SEARXNG_REDIS_PASSWORD $SEARXNG_REDIS_PASSWORD fi if [ -z "$SEARXNG_SECRET_KEY" ]; then SEARXNG_SECRET_KEY=$(openssl rand -hex 32) updateConfigVar SEARXNG_SECRET_KEY $SEARXNG_SECRET_KEY fi generateCert searxng-caddy searxng-caddy outputConfigSearxNG installStack searxng searxng-app "Listening at" $HOME/searxng.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addMCPServerLiteLLM "searxng" "srx" "http://searxng-mcp/mcp" http none "" inner_block="" inner_block=$inner_block">>https://$SUB_SEARXNG.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://searxng-caddy {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>header_up Host {upstream_hostport}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SEARXNG $MANAGETLS_SEARXNG "$is_integrate_hshq" $NETDEFAULT_SEARXNG "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll searxng "$FMLNAME_SEARXNG" $USERTYPE_SEARXNG "https://$SUB_SEARXNG.$HOMESERVER_DOMAIN" "searxng.png" "$(getHeimdallOrderFromSub $SUB_SEARXNG $USERTYPE_SEARXNG)" restartAllCaddyContainers fi } function outputConfigSearxNG() { cat < $HOME/searxng-compose.yml $STACK_VERSION_PREFIX searxng $(getScriptStackVersion searxng) services: searxng-caddy: image: $(getScriptImageByContainerName searxng-caddy) container_name: searxng-caddy hostname: searxng-caddy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-searxng-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/searxng-caddy.crt:/certs/searxng-caddy.crt - \${PORTAINER_HSHQ_SSL_DIR}/searxng-caddy.key:/certs/searxng-caddy.key - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/caddy/Caddyfile:/etc/caddy/Caddyfile - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/caddy/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/caddy/config:/config searxng-app: image: $(getScriptImageByContainerName searxng-app) container_name: searxng-app hostname: searxng-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-searxng-net - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/web:/etc/searxng:rw logging: driver: "json-file" options: max-size: "1m" max-file: "1" searxng-mcp: image: $(getScriptImageByContainerName searxng-mcp) container_name: searxng-mcp hostname: searxng-mcp restart: unless-stopped env_file: stack.env command: python mcp_server.py --http security_opt: - no-new-privileges:true networks: - int-searxng-net - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro searxng-redis: image: $(getScriptImageByContainerName searxng-redis) container_name: searxng-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $SEARXNG_REDIS_PASSWORD --appendonly no networks: - int-searxng-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true int-searxng-net: driver: bridge internal: true ipam: driver: default EOFSE cat < $HOME/searxng.env DESIRED_TIMEZONE=\${PORTAINER_TZ} SEARXNG_BASE_URL=https://$SUB_SEARXNG.$HOMESERVER_DOMAIN/ SEARXNG_ENGINE_API_BASE_URL=http://searxng-app:8080/search MCP_HTTP_HOST=0.0.0.0 MCP_HTTP_PORT=80 EOFSE cat < $HSHQ_STACKS_DIR/searxng/web/settings.yml use_default_settings: true search: formats: - html - json general: instance_name: "$HOMESERVER_NAME Search Engine" donation_url: false contact_url: false enable_metrics: false server: base_url: false bind_address: "0.0.0.0" secret_key: "$SEARXNG_SECRET_KEY" limiter: false image_proxy: true method: "GET" ui: static_use_hash: true theme_args: simple_style: dark center_alignment: true results_on_new_tab: true valkey: url: redis://:$SEARXNG_REDIS_PASSWORD@searxng-redis:6379/0 enabled_plugins: - 'Hash plugin' - 'Search on category select' - 'Tracker URL remover' - 'Open Access DOI rewrite' engines: - name: duckduckgo engine: duckduckgo shortcut: ddg disabled: true - name: brave shortcut: brave engine: xpath paging: true time_range_support: true first_page_num: 0 time_range_url: "&tf={time_range_val}" search_url: https://search.brave.com/search?q={query}&offset={pageno}&spellcheck=1{time_range} url_xpath: //a[@class="result-header"]/@href title_xpath: //span[@class="snippet-title"] content_xpath: //p[1][@class="snippet-description"] suggestion_xpath: //div[@class="text-gray h6"]/a time_range_map: day: 'pd' week: 'pw' month: 'pm' year: 'py' categories: [general, web] disabled: false headers: Accept-Encoding: gzip, deflate about: website: https://brave.com/search/ wikidata_id: Q107355971 use_official_api: false require_api_key: false results: HTML - name: startpage engine: startpage shortcut: sp timeout: 6.0 disabled: false EOFSE cat < $HSHQ_STACKS_DIR/searxng/caddy/Caddyfile { admin off } https://searxng-caddy { tls /certs/searxng-caddy.crt /certs/searxng-caddy.key @api { path /config path /healthz path /stats/errors path /stats/checker } @static { path /static/* } @notstatic { not path /static/* } @imageproxy { path /image_proxy } @notimageproxy { not path /image_proxy } header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-XSS-Protection "1; mode=block" X-Content-Type-Options "nosniff" Permissions-Policy "accelerometer=(),ambient-light-sensor=(),autoplay=(),camera=(),encrypted-media=(),focus-without-user-activation=(),geolocation=(),gyroscope=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(),speaker=(),sync-xhr=(),usb=(),vr=()" Feature-Policy "accelerometer 'none';ambient-light-sensor 'none'; autoplay 'none';camera 'none';encrypted-media 'none';focus-without-user-activation 'none'; geolocation 'none';gyroscope 'none';magnetometer 'none';microphone 'none';midi 'none';payment 'none';picture-in-picture 'none'; speaker 'none';sync-xhr 'none';usb 'none';vr 'none'" Referrer-Policy "no-referrer" X-Robots-Tag "noindex, noarchive, nofollow" -Server } header @api { Access-Control-Allow-Methods "GET, OPTIONS" Access-Control-Allow-Origin "*" } header @static { Cache-Control "public, max-age=31536000" defer } header @notstatic { # No Cache Cache-Control "no-cache, no-store" Pragma "no-cache" } header @imageproxy { Content-Security-Policy "default-src 'none'; img-src 'self' data:" } header @notimageproxy { Content-Security-Policy "upgrade-insecure-requests; default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; form-action 'self' https://github.com/searxng/searxng/issues/new; font-src 'self'; frame-ancestors 'self'; base-uri 'self'; connect-src 'self' https://overpass-api.de; img-src 'self' data: https://*.tile.openstreetmap.org; frame-src https://www.youtube-nocookie.com https://player.vimeo.com https://www.dailymotion.com https://www.deezer.com https://www.mixcloud.com https://w.soundcloud.com https://embed.spotify.com" } handle { encode zstd gzip reverse_proxy searxng-app:8080 { header_up X-Forwarded-Port {http.request.port} header_up X-Forwarded-Proto {http.request.scheme} } } } EOFSE outputSearxNGLimiterConfig } function outputSearxNGLimiterConfig() { sudo tee $HSHQ_STACKS_DIR/searxng/web/limiter.toml >/dev/null < /dev/null 2>&1 cd mcp-searxng-enhanced git remote add origin https://github.com/OvertliDS/mcp-searxng-enhanced.git git fetch --depth=1 origin 31291d968a5c4f4c8c7986877b0c1342daff9db7 git -c advice.detachedHead=false checkout FETCH_HEAD docker image build -t hshq/searxng-mcp:v1 . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/mcp-searxng-enhanced return $rtval } function performUpdateSearxNG() { perform_stack_name=searxng prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=searxng/searxng:2023.8.19-018b0a932,caddy:2.7.6,bitnami/redis:7.0.5 image_update_map[0]="searxng/searxng:2023.8.19-018b0a932,searxng/searxng:2024.10.31-fa108c140" image_update_map[1]="caddy:2.7.6,caddy:2.8.4" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 2) newVer=v5 curImageList=searxng/searxng:2023.12.29-27e26b3d6,caddy:2.7.6,bitnami/redis:7.0.5 image_update_map[0]="searxng/searxng:2023.12.29-27e26b3d6,searxng/searxng:2024.10.31-fa108c140" image_update_map[1]="caddy:2.7.6,caddy:2.8.4" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 3) newVer=v5 curImageList=searxng/searxng:2024.3.15-e2af3e497,caddy:2.7.6,bitnami/redis:7.0.5 image_update_map[0]="searxng/searxng:2024.3.15-e2af3e497,searxng/searxng:2024.10.31-fa108c140" image_update_map[1]="caddy:2.7.6,caddy:2.8.4" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 4) newVer=v5 curImageList=searxng/searxng:2024.7.29-3196e7e86,caddy:2.8.4,bitnami/redis:7.0.5 image_update_map[0]="searxng/searxng:2024.7.29-3196e7e86,searxng/searxng:2024.10.31-fa108c140" image_update_map[1]="caddy:2.8.4,caddy:2.8.4" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 5) newVer=v6 curImageList=searxng/searxng:2024.10.31-fa108c140,caddy:2.8.4,bitnami/redis:7.0.5 image_update_map[0]="searxng/searxng:2024.10.31-fa108c140,searxng/searxng:2025.4.1-e6308b816" image_update_map[1]="caddy:2.8.4,caddy:2.8.4" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfSearxngRemoveStartpage false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v9 curImageList=searxng/searxng:2025.4.1-e6308b816,caddy:2.8.4,bitnami/redis:7.0.5 image_update_map[0]="searxng/searxng:2025.4.1-e6308b816,mirror.gcr.io/searxng/searxng:2025.12.3-1f6ea4127" image_update_map[1]="caddy:2.8.4,mirror.gcr.io/caddy:2.10.2" image_update_map[2]="bitnami/redis:7.0.5,mirror.gcr.io/redis:8.4.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfSearxngFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=searxng/searxng:2025.4.1-e6308b816,caddy:2.9.1,bitnami/redis:7.4.2 image_update_map[0]="searxng/searxng:2025.4.1-e6308b816,mirror.gcr.io/searxng/searxng:2025.8.12-6b1516d" image_update_map[1]="caddy:2.9.1,mirror.gcr.io/caddy:2.10.0" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfSearxngFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 8) newVer=v9 curImageList=mirror.gcr.io/searxng/searxng:2025.8.12-6b1516d,mirror.gcr.io/caddy:2.10.0,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/searxng/searxng:2025.8.12-6b1516d,mirror.gcr.io/searxng/searxng:2025.12.3-1f6ea4127" image_update_map[1]="mirror.gcr.io/caddy:2.10.0,mirror.gcr.io/caddy:2.10.2" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfSearxngFixV9 false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=v10 curImageList=mirror.gcr.io/searxng/searxng:2025.12.3-1f6ea4127,mirror.gcr.io/caddy:2.10.2,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/searxng/searxng:2025.12.3-1f6ea4127,mirror.gcr.io/searxng/searxng:2026.5.13-8e5aa9d39" image_update_map[1]="mirror.gcr.io/caddy:2.10.2,mirror.gcr.io/caddy:2.11.3" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfSearxngUpdateV10 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 10) newVer=v11 curImageList=mirror.gcr.io/searxng/searxng:2026.5.13-8e5aa9d39,mirror.gcr.io/caddy:2.11.3,mirror.gcr.io/valkey/valkey:alpine3.23,hshq/searxng-mcp:v1 image_update_map[0]="mirror.gcr.io/searxng/searxng:2026.5.13-8e5aa9d39,ghcr.io/searxng/searxng:2026.7.12-c19d86faa" image_update_map[1]="mirror.gcr.io/caddy:2.11.3,mirror.gcr.io/caddy:2.11.4" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="hshq/searxng-mcp:v1,hshq/searxng-mcp:v1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfSearxngUpdateV11 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 11) newVer=v12 curImageList=ghcr.io/searxng/searxng:2026.7.12-c19d86faa,mirror.gcr.io/caddy:2.11.4,mirror.gcr.io/valkey/valkey:alpine3.23,hshq/searxng-mcp:v1 image_update_map[0]="ghcr.io/searxng/searxng:2026.7.12-c19d86faa,ghcr.io/searxng/searxng:2026.10.27-cb74e7f81" image_update_map[1]="mirror.gcr.io/caddy:2.11.4,mirror.gcr.io/caddy:2.11.4" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="hshq/searxng-mcp:v1,ghcr.io/homeserverhq/mcp-searxng-enhanced:v1.0" ;; 12) newVer=v12 curImageList=ghcr.io/searxng/searxng:2026.10.27-cb74e7f81,mirror.gcr.io/caddy:2.11.4,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/mcp-searxng-enhanced:v1.0 image_update_map[0]="ghcr.io/searxng/searxng:2026.10.27-cb74e7f81,ghcr.io/searxng/searxng:2026.10.27-cb74e7f81" image_update_map[1]="mirror.gcr.io/caddy:2.11.4,mirror.gcr.io/caddy:2.11.4" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="ghcr.io/homeserverhq/mcp-searxng-enhanced:v1.0,ghcr.io/homeserverhq/mcp-searxng-enhanced:v1.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfSearxngClearRedis false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfSearxngRemoveStartpage() { sed -i "/^search:/,+1d" $HSHQ_STACKS_DIR/searxng/web/settings.yml } function mfSearxngFixRedisCompose() { replaceRedisBlock searxng searxng-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/searxng/redis/* } function mfSearxngFixV9() { outputSearxNGLimiterConfig sudo sed -i "s/^redis:/valkey:/" $HSHQ_STACKS_DIR/searxng/web/settings.yml } function mfSearxngUpdateV10() { set +e buildImageSearxNGMCPV1 if [ $? -ne 0 ]; then return 1 fi set +e cat < $HOME/searxng-compose.yml $STACK_VERSION_PREFIX searxng v10 services: searxng-caddy: image: mirror.gcr.io/caddy:2.11.3 container_name: searxng-caddy hostname: searxng-caddy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-searxng-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/searxng-caddy.crt:/certs/searxng-caddy.crt - \${PORTAINER_HSHQ_SSL_DIR}/searxng-caddy.key:/certs/searxng-caddy.key - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/caddy/Caddyfile:/etc/caddy/Caddyfile - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/caddy/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/caddy/config:/config searxng-app: image: mirror.gcr.io/searxng/searxng:2026.5.13-8e5aa9d39 container_name: searxng-app hostname: searxng-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-searxng-net - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/searxng/web:/etc/searxng:rw logging: driver: "json-file" options: max-size: "1m" max-file: "1" searxng-mcp: image: hshq/searxng-mcp:v1 container_name: searxng-mcp hostname: searxng-mcp restart: unless-stopped env_file: stack.env command: python mcp_server.py --http security_opt: - no-new-privileges:true networks: - int-searxng-net - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro searxng-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: searxng-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $SEARXNG_REDIS_PASSWORD --appendonly no networks: - int-searxng-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true int-searxng-net: driver: bridge internal: true ipam: driver: default EOFSE cat < $HOME/searxng.env DESIRED_TIMEZONE=\${PORTAINER_TZ} SEARXNG_BASE_URL=https://$SUB_SEARXNG.$HOMESERVER_DOMAIN/ SEARXNG_ENGINE_API_BASE_URL=http://searxng-app:8080/search MCP_HTTP_HOST=0.0.0.0 MCP_HTTP_PORT=80 EOFSE addMCPServerLiteLLM "searxng" "srx" "http://searxng-mcp/mcp" http none "" } function mfSearxngUpdateV11() { set +e inner_block="" inner_block=$inner_block">>https://$SUB_SEARXNG.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://searxng-caddy {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>header_up Host {upstream_hostport}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SEARXNG $MANAGETLS_SEARXNG "$is_integrate_hshq" $NETDEFAULT_SEARXNG "$inner_block" } function mfSearxngClearRedis() { sudo rm -fr $HSHQ_NONBACKUP_DIR/searxng/redis/* } # Jellyfin function installJellyfin() { set +e is_integrate_hshq=$1 # Should check if directory exists checkDeleteStackAndDirectory jellyfin "Jellyfin" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName jellyfin) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/jellyfin mkdir $HSHQ_STACKS_DIR/jellyfin/config mkdir $HSHQ_STACKS_DIR/jellyfin/cache mkdir $HSHQ_STACKS_DIR/jellyfin/media initServicesCredentials outputConfigJellyfin installStack jellyfin jellyfin "Startup complete" $HOME/jellyfin.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 5 sed -i 's/^.*EncodingThreadCount.*$/ 1<\/EncodingThreadCount>/' $HSHQ_STACKS_DIR/jellyfin/config/config/encoding.xml mv $HOME/jellyfin-ldap.xml $HSHQ_STACKS_DIR/jellyfin/config/plugins/configurations/LDAP-Auth.xml docker container restart jellyfin inner_block="" inner_block=$inner_block">>https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://jellyfin:8096 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_JELLYFIN $MANAGETLS_JELLYFIN "$is_integrate_hshq" $NETDEFAULT_JELLYFIN "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll jellyfin "$FMLNAME_JELLYFIN" $USERTYPE_JELLYFIN "https://$SUB_JELLYFIN.$HOMESERVER_DOMAIN" "jellyfin.png" "$(getHeimdallOrderFromSub $SUB_JELLYFIN $USERTYPE_JELLYFIN)" restartAllCaddyContainers fi } function outputConfigJellyfin() { cat < $HOME/jellyfin-compose.yml $STACK_VERSION_PREFIX jellyfin $(getScriptStackVersion jellyfin) services: jellyfin: image: $(getScriptImageByContainerName jellyfin) container_name: jellyfin hostname: jellyfin restart: unless-stopped env_file: stack.env user: "\${PORTAINER_UID}:\${PORTAINER_GID}" networks: - dock-ext-net - dock-ldap-net ports: # UPnP Conflicts with HomeAssistant #- "$UPNP_PORT:$UPNP_PORT/udp" - "$JELLYFIN_PORT:$JELLYFIN_PORT/udp" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/jellyfin/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/jellyfin/cache:/cache - \${PORTAINER_HSHQ_STACKS_DIR}/jellyfin/media:/media - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata/media/audio:/sharedaudio:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata/media/movies:/sharedmovies:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata/media/tv:/sharedtv:ro # devices: # - /dev/dri/renderD128:/dev/dri/renderD128 # - /dev/dri/card0:/dev/dri/card0 networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-ldap-net: name: dock-ldap external: true EOFJF cat < $HOME/jellyfin.env UID=$USERID GID=$GROUPID EOFJF cat < $HOME/jellyfin-ldap.xml ldapserver 389 false true false cn=$LDAP_READONLY_USER_USERNAME,$LDAP_BASE_DN $LDAP_READONLY_USER_PASSWORD $LDAP_BASE_DN (memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN) cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN (memberOf=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN) uid,mail false true false uid userPassword true https://$SUB_OPENLDAP_MANAGER.$HOMESERVER_DOMAIN EOFLD chmod 644 $HOME/jellyfin-ldap.xml } function performUpdateJellyfin() { perform_stack_name=jellyfin prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=jellyfin/jellyfin:10.8.10 image_update_map[0]="jellyfin/jellyfin:10.8.10,jellyfin/jellyfin:10.9.8" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" notifyJellyfinLDAPPluginUpdate false perform_update_report="${perform_update_report}$stack_upgrade_report" sendEmailJellyfinLDAPPluginUpdate return ;; 2) newVer=v3 curImageList=jellyfin/jellyfin:10.8.13 image_update_map[0]="jellyfin/jellyfin:10.8.13,jellyfin/jellyfin:10.9.8" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" notifyJellyfinLDAPPluginUpdate false perform_update_report="${perform_update_report}$stack_upgrade_report" sendEmailJellyfinLDAPPluginUpdate return ;; 3) newVer=v6 curImageList=jellyfin/jellyfin:10.9.8 image_update_map[0]="jellyfin/jellyfin:10.9.8,mirror.gcr.io/jellyfin/jellyfin:10.10.7" ;; 4) newVer=v6 curImageList=jellyfin/jellyfin:10.10.0 image_update_map[0]="jellyfin/jellyfin:10.10.0,mirror.gcr.io/jellyfin/jellyfin:10.10.7" ;; 5) newVer=v6 curImageList=jellyfin/jellyfin:10.10.6 image_update_map[0]="jellyfin/jellyfin:10.10.6,mirror.gcr.io/jellyfin/jellyfin:10.10.7" ;; 6) newVer=v7 curImageList=mirror.gcr.io/jellyfin/jellyfin:10.10.7 image_update_map[0]="mirror.gcr.io/jellyfin/jellyfin:10.10.7,mirror.gcr.io/jellyfin/jellyfin:10.11.4" ;; 7) newVer=v7 curImageList=mirror.gcr.io/jellyfin/jellyfin:10.11.4 image_update_map[0]="mirror.gcr.io/jellyfin/jellyfin:10.11.4,mirror.gcr.io/jellyfin/jellyfin:10.11.4" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function notifyJellyfinLDAPPluginUpdate() { stack_upgrade_report="${stack_upgrade_report}jellyfin: This version update requires the LDAP plugin to be reinstalled. See admin email ($EMAIL_ADMIN_EMAIL_ADDRESS) for more details.\n" } function sendEmailJellyfinLDAPPluginUpdate() { sendEmail -s "Jellyfin Version Update" -b "This new version of Jellyfin requires the LDAP plugin to be updated as well. Login as the Jellyfin administrator ($JELLYFIN_ADMIN_USERNAME), and uninstall and reinstall the LDAP Authentication plugin. The new version of the plugin should be version (19.0.0.0). After reinstalling, ensure to restart the jellyfin stack in Portainer." -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" } # FileBrowser function installFileBrowser() { set +e is_integrate_hshq=$1 # Check if directory exists checkDeleteStackAndDirectory filebrowser "FileBrowser" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName filebrowser) if [ $? -ne 0 ]; then return 1 fi set +e mkdir $HSHQ_STACKS_DIR/filebrowser mkdir $HSHQ_STACKS_DIR/filebrowser/db mkdir $HSHQ_STACKS_DIR/filebrowser/srv mkdir $HSHQ_STACKS_DIR/filebrowser/config initServicesCredentials FILEBROWSER_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $FILEBROWSER_PASSWORD | tr -d ':\n' | sed 's/$2y/$2a/') outputConfigFileBrowser installStack filebrowser filebrowser "Listening on" $HOME/filebrowser.env retval=$? if [ $retval -ne 0 ]; then return $retval fi inner_block="" inner_block=$inner_block">>https://$SUB_FILEBROWSER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://filebrowser {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FILEBROWSER $MANAGETLS_FILEBROWSER "$is_integrate_hshq" $NETDEFAULT_FILEBROWSER "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll filebrowser "$FMLNAME_FILEBROWSER" $USERTYPE_FILEBROWSER "https://$SUB_FILEBROWSER.$HOMESERVER_DOMAIN" "filebrowser.png" "$(getHeimdallOrderFromSub $SUB_FILEBROWSER $USERTYPE_FILEBROWSER)" restartAllCaddyContainers fi } function outputConfigFileBrowser() { outputComposeFileBrowser cat < $HOME/filebrowser.env UID=$USERID GID=$GROUPID FB_USERNAME=$FILEBROWSER_USERNAME FB_PASSWORD='$FILEBROWSER_PASSWORD_HASH' EOFJF cat < $HSHQ_STACKS_DIR/filebrowser/filebrowser.json { "port": 80, "baseURL": "", "address": "", "log": "stdout", "database": "/database/filebrowser.db", "root": "/srv" } EOFJF } function outputComposeFileBrowser() { cat < $HOME/filebrowser-compose.yml $STACK_VERSION_PREFIX filebrowser $(getScriptStackVersion filebrowser) services: filebrowser: image: $(getScriptImageByContainerName filebrowser) container_name: filebrowser hostname: filebrowser restart: unless-stopped env_file: stack.env user: "\${PORTAINER_UID}:\${PORTAINER_GID}" security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/filebrowser/filebrowser.json:/.filebrowser.json - \${PORTAINER_HSHQ_STACKS_DIR}/filebrowser/srv:/srv - \${PORTAINER_HSHQ_STACKS_DIR}/filebrowser/db:/database - \${PORTAINER_HSHQ_STACKS_DIR}/filebrowser/config:/config networks: dock-proxy-net: name: dock-proxy external: true EOFJF } function performUpdateFileBrowser() { perform_stack_name=filebrowser prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v6 curImageList=filebrowser/filebrowser:v2.24.2 image_update_map[0]="filebrowser/filebrowser:v2.24.2,filebrowser/filebrowser:v2.32.0" ;; 2) newVer=v6 curImageList=filebrowser/filebrowser:v2.26.0 image_update_map[0]="filebrowser/filebrowser:v2.26.0,filebrowser/filebrowser:v2.32.0" ;; 3) newVer=v6 curImageList=filebrowser/filebrowser:v2.27.0 image_update_map[0]="filebrowser/filebrowser:v2.27.0,filebrowser/filebrowser:v2.32.0" ;; 4) newVer=v6 curImageList=filebrowser/filebrowser:v2.30.0 image_update_map[0]="filebrowser/filebrowser:v2.30.0,filebrowser/filebrowser:v2.32.0" ;; 5) newVer=v6 curImageList=filebrowser/filebrowser:v2.31.2 image_update_map[0]="filebrowser/filebrowser:v2.31.2,filebrowser/filebrowser:v2.32.0" ;; 6) newVer=v8 curImageList=filebrowser/filebrowser:v2.32.0 image_update_map[0]="filebrowser/filebrowser:v2.32.0,mirror.gcr.io/filebrowser/filebrowser:v2.50.0" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfFileBrowserAddConfigVol perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=mirror.gcr.io/filebrowser/filebrowser:v2.42.3 image_update_map[0]="mirror.gcr.io/filebrowser/filebrowser:v2.42.3,mirror.gcr.io/filebrowser/filebrowser:v2.50.0" ;; 8) newVer=v8 curImageList=mirror.gcr.io/filebrowser/filebrowser:v2.50.0 image_update_map[0]="mirror.gcr.io/filebrowser/filebrowser:v2.50.0,mirror.gcr.io/filebrowser/filebrowser:v2.50.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfFileBrowserAddConfigVol() { mkdir -p $HSHQ_STACKS_DIR/filebrowser/config rm -f $HOME/filebrowser-compose.yml outputComposeFileBrowser } # PhotoPrism function installPhotoPrism() { set +e is_integrate_hshq=$1 # Only install if directory does not exist checkDeleteStackAndDirectory photoprism "PhotoPrism" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName photoprism-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName photoprism-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/photoprism mkdir $HSHQ_STACKS_DIR/photoprism/db mkdir $HSHQ_STACKS_DIR/photoprism/dbexport mkdir $HSHQ_STACKS_DIR/photoprism/app mkdir $HSHQ_STACKS_DIR/photoprism/app/originals mkdir $HSHQ_STACKS_DIR/photoprism/app/import mkdir $HSHQ_STACKS_DIR/photoprism/app/storage chmod 777 $HSHQ_STACKS_DIR/photoprism/dbexport initServicesCredentials outputConfigPhotoPrism if ! [ "$PHOTOPRISM_INIT_ENV" = "true" ]; then sendEmail -s "PhotoPrism Admin Login Info" -b "PhotoPrism Admin Username: $PHOTOPRISM_ADMIN_USERNAME\nPhotoPrism Admin Password: $PHOTOPRISM_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" PHOTOPRISM_INIT_ENV=true updateConfigVar PHOTOPRISM_INIT_ENV $PHOTOPRISM_INIT_ENV fi echo "Starting PhotoPrism. Please be patient, this process takes a few minutes..." cd ~ docker compose -f $HOME/photoprism-compose-tmp.yml up -d search="listening on 0.0.0.0:2342" isFound=false i=0 set +e while [ $i -le 600 ] do findtext=$(docker logs photoprism-app 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 10 seconds, total wait=$i seconds..." sleep 10 i=$((i+10)) done if ! [ "$isFound" = "true" ]; then cd ~ docker compose -f $HOME/photoprism-compose-tmp.yml down -v echo "ERROR: PhotoPrism did not start up correctly..." return 1 fi sleep 5 cd ~ docker compose -f $HOME/photoprism-compose-tmp.yml down -v installStack photoprism photoprism-app "listening on 0.0.0.0:2342" $HOME/photoprism.env 10 retval=$? if [ $retval -ne 0 ]; then return $retval fi rm -f $HOME/photoprism-compose-tmp.yml sleep 3 addReadOnlyUserToDatabase PhotoPrism mysql photoprism-db $PHOTOPRISM_DATABASE_NAME root $PHOTOPRISM_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/photoprism/dbexport $PHOTOPRISM_DATABASE_READONLYUSER $PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_PHOTOPRISM.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://photoprism-app:2342 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PHOTOPRISM $MANAGETLS_PHOTOPRISM "$is_integrate_hshq" $NETDEFAULT_PHOTOPRISM "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll photoprism "$FMLNAME_PHOTOPRISM" $USERTYPE_PHOTOPRISM "https://$SUB_PHOTOPRISM.$HOMESERVER_DOMAIN" "photoprism.png" "$(getHeimdallOrderFromSub $SUB_PHOTOPRISM $USERTYPE_PHOTOPRISM)" restartAllCaddyContainers fi } function outputConfigPhotoPrism() { cat < $HOME/photoprism-compose-tmp.yml services: photoprism-db: image: $(getScriptImageByContainerName photoprism-db) container_name: photoprism-db hostname: photoprism-db restart: unless-stopped security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-photoprism-net volumes: - v-photoprism-db:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD=$PHOTOPRISM_DATABASE_ROOT_PASSWORD - MYSQL_DATABASE=$PHOTOPRISM_DATABASE_NAME - MYSQL_USER=$PHOTOPRISM_DATABASE_USER - MYSQL_PASSWORD=$PHOTOPRISM_DATABASE_USER_PASSWORD photoprism-app: image: $(getScriptImageByContainerName photoprism-app) container_name: photoprism-app hostname: photoprism-app restart: unless-stopped user: "$USERID:$GROUPID" security_opt: - seccomp:unconfined - apparmor:unconfined depends_on: - photoprism-db working_dir: "/photoprism" networks: - int-photoprism-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - $HSHQ_STACKS_DIR/photoprism/app/originals:/photoprism/originals - $HSHQ_STACKS_DIR/photoprism/app/import:/photoprism/import - $HSHQ_STACKS_DIR/photoprism/app/storage:/photoprism/storage environment: - "PHOTOPRISM_INIT=gpu tensorflow" - PHOTOPRISM_UID=$USERID - PHOTOPRISM_GID=$GROUPID - PHOTOPRISM_AUTH_MODE=public - PHOTOPRISM_SITE_URL=http://localhost:2342/ - PHOTOPRISM_SITE_TITLE=PhotoPrism - "PHOTOPRISM_SITE_CAPTION=AI-Powered Photos App" - "PHOTOPRISM_SITE_DESCRIPTION=$HOMESERVER_NAME Photos" - "PHOTOPRISM_SITE_AUTHOR=$HOMESERVER_NAME" - PHOTOPRISM_ORIGINALS_LIMIT=5000 - PHOTOPRISM_HTTP_COMPRESSION=gzip - PHOTOPRISM_LOG_LEVEL=info - PHOTOPRISM_READONLY=false - PHOTOPRISM_EXPERIMENTAL=false - PHOTOPRISM_DISABLE_CHOWN=true - PHOTOPRISM_DISABLE_WEBDAV=false - PHOTOPRISM_DISABLE_SETTINGS=false - PHOTOPRISM_DISABLE_TENSORFLOW=false - PHOTOPRISM_DISABLE_FACES=false - PHOTOPRISM_DISABLE_CLASSIFICATION=false - PHOTOPRISM_DISABLE_RAW=false - PHOTOPRISM_RAW_PRESETS=false - PHOTOPRISM_JPEG_QUALITY=85 - PHOTOPRISM_DETECT_NSFW=false - PHOTOPRISM_UPLOAD_NSFW=true - HOME=/photoprism - PHOTOPRISM_DATABASE_DRIVER=mysql - PHOTOPRISM_DATABASE_SERVER=photoprism-db:3306 - PHOTOPRISM_DATABASE_NAME=$PHOTOPRISM_DATABASE_NAME - PHOTOPRISM_DATABASE_USER=$PHOTOPRISM_DATABASE_USER - PHOTOPRISM_DATABASE_PASSWORD=$PHOTOPRISM_DATABASE_USER_PASSWORD volumes: v-photoprism-db: driver: local driver_opts: type: none o: bind device: $HSHQ_STACKS_DIR/photoprism/db networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true int-photoprism-net: driver: bridge internal: true ipam: driver: default EOFPP cat < $HOME/photoprism-compose.yml $STACK_VERSION_PREFIX photoprism $(getScriptStackVersion photoprism) services: photoprism-db: image: $(getScriptImageByContainerName photoprism-db) container_name: photoprism-db hostname: photoprism-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-photoprism-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-photoprism-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/photoprism/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.photoprism-hourly-db.schedule=@every 1h" - "ofelia.job-exec.photoprism-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.photoprism-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.photoprism-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.photoprism-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.photoprism-hourly-db.email-from=PhotoPrism Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.photoprism-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.photoprism-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.photoprism-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.photoprism-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.photoprism-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.photoprism-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.photoprism-monthly-db.email-from=PhotoPrism Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.photoprism-monthly-db.mail-only-on-error=false" photoprism-app: image: $(getScriptImageByContainerName photoprism-app) container_name: photoprism-app hostname: photoprism-app restart: unless-stopped env_file: stack.env user: "\${PORTAINER_UID}:\${PORTAINER_GID}" security_opt: - seccomp:unconfined - apparmor:unconfined depends_on: - photoprism-db working_dir: "/photoprism" networks: - int-photoprism-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/photoprism/app/originals:/photoprism/originals - \${PORTAINER_HSHQ_STACKS_DIR}/photoprism/app/import:/photoprism/import - \${PORTAINER_HSHQ_STACKS_DIR}/photoprism/app/storage:/photoprism/storage volumes: v-photoprism-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/photoprism/db networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true int-photoprism-net: driver: bridge internal: true ipam: driver: default EOFPP outputPhotoPrismEnv } function outputPhotoPrismEnv() { cat < $HOME/photoprism.env PHOTOPRISM_UID=$USERID PHOTOPRISM_GID=$GROUPID PHOTOPRISM_AUTH_MODE=password PHOTOPRISM_ADMIN_USER=$PHOTOPRISM_ADMIN_USERNAME PHOTOPRISM_ADMIN_PASSWORD=$PHOTOPRISM_ADMIN_PASSWORD PHOTOPRISM_SITE_URL=http://localhost:2342/ PHOTOPRISM_SITE_CAPTION=AI-Powered Photos App PHOTOPRISM_SITE_DESCRIPTION=$HOMESERVER_NAME Photos PHOTOPRISM_SITE_AUTHOR=$HOMESERVER_NAME PHOTOPRISM_HTTP_COMPRESSION=gzip PHOTOPRISM_JPEG_SIZE=7680 PHOTOPRISM_THUMB_FILTER=lanczos PHOTOPRISM_THUMB_UNCACHED=true PHOTOPRISM_THUMB_SIZE=1920 PHOTOPRISM_THUMB_SIZE_UNCACHED=7680 PHOTOPRISM_READONLY=false PHOTOPRISM_EXPERIMENTAL=false PHOTOPRISM_DISABLE_CHOWN=true PHOTOPRISM_DISABLE_WEBDAV=false PHOTOPRISM_DISABLE_SETTINGS=false PHOTOPRISM_DISABLE_TENSORFLOW=false PHOTOPRISM_RAW_PRESETS=false PHOTOPRISM_DETECT_NSFW=false PHOTOPRISM_UPLOAD_NSFW=true HOME=/photoprism PHOTOPRISM_DATABASE_DRIVER=mysql PHOTOPRISM_DATABASE_SERVER=photoprism-db:3306 PHOTOPRISM_DATABASE_NAME=$PHOTOPRISM_DATABASE_NAME PHOTOPRISM_DATABASE_USER=$PHOTOPRISM_DATABASE_USER PHOTOPRISM_DATABASE_PASSWORD=$PHOTOPRISM_DATABASE_USER_PASSWORD MYSQL_DATABASE=$PHOTOPRISM_DATABASE_NAME MYSQL_ROOT_PASSWORD=$PHOTOPRISM_DATABASE_ROOT_PASSWORD MYSQL_USER=$PHOTOPRISM_DATABASE_USER MYSQL_PASSWORD=$PHOTOPRISM_DATABASE_USER_PASSWORD EOFPP } function performUpdatePhotoPrism() { perform_stack_name=photoprism prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mariadb:10.7.3,photoprism/photoprism:220901-bullseye image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="photoprism/photoprism:220901-bullseye,photoprism/photoprism:240915" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdatePhotoPrismV2 perform_update_report="${perform_update_report}$stack_upgrade_report" sendEmail -s "PhotoPrism Admin Login Info" -b "PhotoPrism Admin Username: $PHOTOPRISM_ADMIN_USERNAME\nPhotoPrism Admin Password: $PHOTOPRISM_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" PHOTOPRISM_INIT_ENV=true updateConfigVar PHOTOPRISM_INIT_ENV $PHOTOPRISM_INIT_ENV return ;; 2) newVer=v4 curImageList=mariadb:10.7.3,photoprism/photoprism:240915 image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="photoprism/photoprism:240915,mirror.gcr.io/photoprism/photoprism:251130" ;; 3) newVer=v4 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/photoprism/photoprism:250707 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/photoprism/photoprism:250707,mirror.gcr.io/photoprism/photoprism:251130" ;; 4) newVer=v4 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/photoprism/photoprism:251130 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/photoprism/photoprism:251130,mirror.gcr.io/photoprism/photoprism:251130" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfUpdatePhotoPrismV2() { rm -f $HOME/photoprism.env outputPhotoPrismEnv } # Guacamole function installGuacamole() { set +e is_integrate_hshq=$1 # Don't proceed if directory exists checkDeleteStackAndDirectory guacamole "Guacamole" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName guacamole-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName guacamole-daemon) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName guacamole-web) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/guacamole mkdir $HSHQ_STACKS_DIR/guacamole/db mkdir $HSHQ_STACKS_DIR/guacamole/dbexport mkdir $HSHQ_STACKS_DIR/guacamole/init chmod 777 $HSHQ_STACKS_DIR/guacamole/dbexport initServicesCredentials if [ -z "$GUACAMOLE_DATABASE_ROOT_PASSWORD" ]; then GUACAMOLE_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GUACAMOLE_DATABASE_ROOT_PASSWORD $GUACAMOLE_DATABASE_ROOT_PASSWORD fi outputConfigGuacamole docker run --rm $(getScriptImageByContainerName guacamole-web) /opt/guacamole/bin/initdb.sh --mysql > $HSHQ_STACKS_DIR/guacamole/init/initdb.sql cd ~ docker compose -f $HOME/guacamole-compose-tmp.yml up -d echo "Waiting at least 15 seconds before continuing..." sleep 15 search="ready for connections" isFound=false i=0 set +e while [ $i -le 300 ] do findtext=$(docker logs guacamole-db 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 5 seconds, total wait=$i seconds..." sleep 5 i=$((i+5)) done if ! [ "$isFound" = "true" ]; then echo "Guacamole did not start up correctly..." cd ~ docker compose -f $HOME/guacamole-compose-tmp.yml down -v rm -f $HOME/guacamole-compose-tmp.yml return 1 fi sleep 5 cd ~ docker compose -f $HOME/guacamole-compose-tmp.yml down -v installStack guacamole guacamole-web "Server startup in" $HOME/guacamole.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi rm -f $HOME/guacamole-compose-tmp.yml sleep 3 addReadOnlyUserToDatabase Guacamole mysql guacamole-db $GUACAMOLE_DATABASE_NAME root $GUACAMOLE_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/guacamole/dbexport $GUACAMOLE_DATABASE_READONLYUSER $GUACAMOLE_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_GUACAMOLE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>@notGuac {\n" inner_block=$inner_block">>>>>>>>not path \"/guacamole/*\"\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>redir @notGuac /guacamole/\n" inner_block=$inner_block">>>>>>reverse_proxy http://guacamole-web:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_GUACAMOLE $MANAGETLS_GUACAMOLE "$is_integrate_hshq" $NETDEFAULT_GUACAMOLE "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll guacamole "$FMLNAME_GUACAMOLE" $USERTYPE_GUACAMOLE "https://$SUB_GUACAMOLE.$HOMESERVER_DOMAIN" "guacamole.png" "$(getHeimdallOrderFromSub $SUB_GUACAMOLE $USERTYPE_GUACAMOLE)" restartAllCaddyContainers fi } function outputConfigGuacamole() { cat < $HOME/guacamole-compose-tmp.yml services: guacamole-db: image: $(getScriptImageByContainerName guacamole-db) container_name: guacamole-db hostname: guacamole-db user: "$USERID" restart: unless-stopped security_opt: - no-new-privileges:true networks: - int-guacamole-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - $HSHQ_STACKS_DIR/guacamole/db:/var/lib/mysql - $HSHQ_STACKS_DIR/guacamole/init:/docker-entrypoint-initdb.d:ro environment: - MYSQL_ROOT_PASSWORD=$GUACAMOLE_DATABASE_ROOT_PASSWORD - MYSQL_DATABASE=$GUACAMOLE_DATABASE_NAME - MYSQL_USER=$GUACAMOLE_DATABASE_USER - MYSQL_PASSWORD=$GUACAMOLE_DATABASE_USER_PASSWORD networks: int-guacamole-net: driver: bridge internal: true ipam: driver: default EOFGC cat < $HOME/guacamole-compose.yml $STACK_VERSION_PREFIX guacamole $(getScriptStackVersion guacamole) services: guacamole-db: image: $(getScriptImageByContainerName guacamole-db) container_name: guacamole-db hostname: guacamole-db user: \${PORTAINER_UID} restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-guacamole-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/guacamole/db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/guacamole/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.guacamole-hourly-db.schedule=@every 1h" - "ofelia.job-exec.guacamole-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.guacamole-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.guacamole-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.guacamole-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.guacamole-hourly-db.email-from=Guacamole Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.guacamole-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.guacamole-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.guacamole-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.guacamole-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.guacamole-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.guacamole-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.guacamole-monthly-db.email-from=Guacamole Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.guacamole-monthly-db.mail-only-on-error=false" guacamole-daemon: image: $(getScriptImageByContainerName guacamole-daemon) container_name: guacamole-daemon hostname: guacamole-daemon restart: unless-stopped security_opt: - no-new-privileges:true networks: - int-guacamole-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro guacamole-web: image: $(getScriptImageByContainerName guacamole-web) container_name: guacamole-web hostname: guacamole-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - guacamole-db - guacamole-daemon networks: - int-guacamole-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-guacamole-net: driver: bridge internal: true ipam: driver: default EOFGC cat < $HOME/guacamole.env UID=$USERID GID=$GROUPID GUACD_HOSTNAME=guacamole-daemon MYSQL_HOSTNAME=guacamole-db MYSQL_DATABASE=$GUACAMOLE_DATABASE_NAME MYSQL_USER=$GUACAMOLE_DATABASE_USER MYSQL_PASSWORD=$GUACAMOLE_DATABASE_USER_PASSWORD EOFGC } function performUpdateGuacamole() { perform_stack_name=guacamole prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=guacamole/guacd:1.5.3,guacamole/guacamole:1.5.3,mariadb:10.7.3 image_update_map[0]="guacamole/guacd:1.5.3,mirror.gcr.io/guacamole/guacd:1.6.0" image_update_map[1]="guacamole/guacamole:1.5.3,mirror.gcr.io/guacamole/guacamole:1.6.0" image_update_map[3]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" ;; 2) newVer=v4 curImageList=guacamole/guacd:1.5.4,guacamole/guacamole:1.5.4,mariadb:10.7.3 image_update_map[0]="guacamole/guacd:1.5.4,mirror.gcr.io/guacamole/guacd:1.6.0" image_update_map[1]="guacamole/guacamole:1.5.4,mirror.gcr.io/guacamole/guacamole:1.6.0" image_update_map[3]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" ;; 3) newVer=v4 curImageList=guacamole/guacd:1.5.5,guacamole/guacamole:1.5.5,mariadb:10.7.3 image_update_map[0]="guacamole/guacd:1.5.5,mirror.gcr.io/guacamole/guacd:1.6.0" image_update_map[1]="guacamole/guacamole:1.5.5,mirror.gcr.io/guacamole/guacamole:1.6.0" image_update_map[3]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" ;; 4) newVer=v4 curImageList=mirror.gcr.io/guacamole/guacd:1.6.0,mirror.gcr.io/guacamole/guacamole:1.6.0,mirror.gcr.io/mariadb:10.7.3 image_update_map[0]="mirror.gcr.io/guacamole/guacd:1.6.0,mirror.gcr.io/guacamole/guacd:1.6.0" image_update_map[1]="mirror.gcr.io/guacamole/guacamole:1.6.0,mirror.gcr.io/guacamole/guacamole:1.6.0" image_update_map[3]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Authelia function installAuthelia() { set +e is_integrate_hshq=$1 # Exit if directory exists checkDeleteStackAndDirectory authelia "Authelia" cdRes=$? if [ $cdRes -ne 0 ]; then echo "ERROR: Authelia directory exists" exit 1 fi pullImage $(getScriptImageByContainerName authelia) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain Authelia docker image" exit 1 fi pullImage $(getScriptImageByContainerName authelia-redis) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain Authelia Redis docker image" exit 1 fi set -e mkdir $HSHQ_STACKS_DIR/authelia mkdir $HSHQ_STACKS_DIR/authelia/config mkdir $HSHQ_STACKS_DIR/authelia/config/certs mkdir $HSHQ_STACKS_DIR/authelia/config/cacerts mkdir $HSHQ_STACKS_DIR/authelia/config/secrets mkdir $HSHQ_STACKS_DIR/authelia/keys if [ -z "$AUTHELIA_REDIRECTION_URL" ]; then AUTHELIA_REDIRECTION_URL=$SUB_HSHQHOME.$HOMESERVER_DOMAIN updateConfigVar AUTHELIA_REDIRECTION_URL $AUTHELIA_REDIRECTION_URL fi AUTHELIA_JWT_SECRET=$(pwgen -c -n 32 1) rm -f "$HSHQ_SECRETS_DIR/authelia_jwt_secret.txt" echo "$AUTHELIA_JWT_SECRET" > "$HSHQ_SECRETS_DIR/authelia_jwt_secret.txt" chmod 0400 "$HSHQ_SECRETS_DIR/authelia_jwt_secret.txt" AUTHELIA_SESSION_SECRET=$(pwgen -c -n 64 1) rm -f "$HSHQ_SECRETS_DIR/authelia_session_secret.txt" echo "$AUTHELIA_SESSION_SECRET" > "$HSHQ_SECRETS_DIR/authelia_session_secret.txt" chmod 0400 "$HSHQ_SECRETS_DIR/authelia_session_secret.txt" AUTHELIA_STORAGE_ENCRYPTION_KEY=$(pwgen -c -n 64 1) rm -f "$HSHQ_SECRETS_DIR/authelia_storage_encryption_key.txt" echo "$AUTHELIA_STORAGE_ENCRYPTION_KEY" > "$HSHQ_SECRETS_DIR/authelia_storage_encryption_key.txt" chmod 0400 "$HSHQ_SECRETS_DIR/authelia_storage_encryption_key.txt" AUTHELIA_REDIS_PASSWORD=$(pwgen -c -n 64 1) rm -f "$HSHQ_SECRETS_DIR/authelia_redis_password.txt" echo "$AUTHELIA_REDIS_PASSWORD" > "$HSHQ_SECRETS_DIR/authelia_redis_password.txt" chmod 0400 "$HSHQ_SECRETS_DIR/authelia_redis_password.txt" outputAutheliaIDPSecretsV108 generateCert authelia authelia generateCert authelia-redis authelia-redis outputConfigAuthelia installStack authelia authelia "Listening for TLS connections on" $HOME/authelia.env retval=$? if [ $retval -ne 0 ]; then echo "ERROR: There was a problem installing Authelia" exit $retval fi inner_block="" inner_block=$inner_block">>https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AUTHELIA $MANAGETLS_AUTHELIA "$is_integrate_hshq" $NETDEFAULT_AUTHELIA "$inner_block" } function outputConfigAuthelia() { cat < $HOME/authelia-compose.yml $STACK_VERSION_PREFIX authelia $(getScriptStackVersion authelia) services: authelia: image: $(getScriptImageByContainerName authelia) container_name: authelia hostname: authelia restart: unless-stopped env_file: stack.env user: "\${PORTAINER_UID}:\${PORTAINER_GID}" security_opt: - no-new-privileges:true networks: - int-authelia-net - dock-proxy-net - dock-privateip-net - dock-ldap-net - dock-internalmail-net depends_on: - authelia-redis volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/config/cacerts/${CERTS_ROOT_CA_NAME}.crt:ro - \${PORTAINER_HSHQ_STACKS_DIR}/authelia/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/authelia/keys:/keys - \${PORTAINER_HSHQ_SSL_DIR}/authelia.crt:/config/certs/authelia.crt - \${PORTAINER_HSHQ_SSL_DIR}/authelia.key:/config/certs/authelia.key secrets: - authelia_jwt_secret - authelia_session_secret - authelia_storage_encryption_key - ldap_admin_user_password - authelia_redis_password authelia-redis: image: $(getScriptImageByContainerName authelia-redis) container_name: authelia-redis hostname: authelia-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $(cat $HSHQ_SECRETS_DIR/authelia_redis_password.txt) --tls-port 6379 --port 0 --tls-cert-file /tls/authelia-redis.crt --tls-key-file /tls/authelia-redis.key --tls-ca-cert-file /tls/${CERTS_ROOT_CA_NAME}.crt --tls-auth-clients no --appendonly no networks: - int-authelia-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/tls/${CERTS_ROOT_CA_NAME}.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/authelia-redis.crt:/tls/authelia-redis.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/authelia-redis.key:/tls/authelia-redis.key:ro - \${PORTAINER_HSHQ_SSL_DIR}/dhparam.pem:/tls/dhparam.pem:ro secrets: authelia_jwt_secret: file: \${PORTAINER_HSHQ_SECRETS_DIR}/authelia_jwt_secret.txt authelia_session_secret: file: \${PORTAINER_HSHQ_SECRETS_DIR}/authelia_session_secret.txt authelia_storage_encryption_key: file: \${PORTAINER_HSHQ_SECRETS_DIR}/authelia_storage_encryption_key.txt ldap_admin_user_password: file: \${PORTAINER_HSHQ_SECRETS_DIR}/ldap_admin_bind_password.txt authelia_redis_password: file: \${PORTAINER_HSHQ_SECRETS_DIR}/authelia_redis_password.txt networks: dock-proxy-net: name: dock-proxy external: true dock-privateip-net: name: dock-privateip external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true int-authelia-net: driver: bridge internal: true ipam: driver: default EOFAC cat < $HOME/authelia.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/run/secrets/authelia_jwt_secret AUTHELIA_SESSION_SECRET_FILE=/run/secrets/authelia_session_secret AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/run/secrets/authelia_storage_encryption_key AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE=/run/secrets/ldap_admin_user_password AUTHELIA_SESSION_REDIS_PASSWORD_FILE=/run/secrets/authelia_redis_password X_AUTHELIA_CONFIG_FILTERS=template EOFAE cat < $HSHQ_STACKS_DIR/authelia/config/configuration.yml # yamllint disable rule:comments-indentation --- ############################################################################### # Authelia Configuration # ############################################################################### theme: dark certificates_directory: /config/cacerts/ server: address: 'tcp://:9091/' disable_healthcheck: true tls: key: /config/certs/authelia.key certificate: /config/certs/authelia.crt endpoints: enable_pprof: false enable_expvars: false log: level: info totp: disable: false issuer: "$HOMESERVER_NAME" digits: 6 period: 30 skew: 1 webauthn: disable: false timeout: 60s display_name: "$HOMESERVER_NAME" attestation_conveyance_preference: indirect selection_criteria: user_verification: preferred duo_api: disable: true authentication_backend: password_reset: disable: true refresh_interval: 5m ldap: address: $LDAPS_URI implementation: custom timeout: 5s start_tls: false tls: server_name: ldapserver skip_verify: false minimum_version: TLS1.2 base_dn: $LDAP_BASE_DN additional_users_dn: ou=people users_filter: (&(&({username_attribute}={input})(objectClass=person))(memberOf=cn=everybody,ou=groups,$LDAP_BASE_DN)) additional_groups_dn: ou=groups groups_filter: (&(uniquemember={dn})(objectClass=groupOfUniqueNames)) group_search_mode: filter permit_referrals: false permit_unauthenticated_bind: false user: $LDAP_ADMIN_BIND_DN attributes: distinguished_name: cn username: uid display_name: cn mail: mail member_of: memberOf group_name: cn #AC_BEGIN #AC_END session: name: authelia same_site: lax inactivity: 21600 expiration: 43200 remember_me: 21600 cookies: # Authelia session cookies BEGIN - domain: $HOMESERVER_DOMAIN authelia_url: https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN default_redirection_url: https://$AUTHELIA_REDIRECTION_URL name: authelia same_site: lax inactivity: 21600 expiration: 43200 remember_me: 21600 # Authelia session cookies END redis: host: authelia-redis port: 6379 maximum_active_connections: 8 tls: server_name: authelia-redis skip_verify: false minimum_version: TLS1.2 regulation: max_retries: 5 find_time: 10m ban_time: 10m storage: local: path: /config/db.sqlite3 ntp: address: udp://${NET_PRIVATEIP_SUBNET_PREFIX}.1:123 version: 3 max_desync: 3s disable_startup_check: false disable_failure: false notifier: disable_startup_check: true smtp: address: smtp://${SMTP_HOSTNAME}:${SMTP_HOSTPORT} sender: "Authelia $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" identifier: localhost subject: "[Authelia] {title}" startup_check_address: $EMAIL_ADMIN_EMAIL_ADDRESS disable_require_tls: false disable_html_emails: false tls: skip_verify: false minimum_version: TLS1.2 identity_providers: oidc: claims_policies: cp_legacy: id_token: - email - name - groups - preferred_username linkwarden_claim: id_token: - email - name - preferred_username - email_verified full_verified_claim: id_token: - email - name - groups - preferred_username - email_verified authorization_policies: everyone_auth: default_policy: deny rules: - policy: one_factor subject: group:everyone ${LDAP_BASIC_USER_GROUP_NAME}_auth: default_policy: deny rules: - policy: one_factor subject: group:$LDAP_BASIC_USER_GROUP_NAME ${LDAP_PRIMARY_USER_GROUP_NAME}_auth: default_policy: deny rules: - policy: one_factor subject: group:${LDAP_PRIMARY_USER_GROUP_NAME} ${LDAP_ADMIN_USER_GROUP_NAME}_auth: default_policy: deny rules: - policy: one_factor subject: group:${LDAP_ADMIN_USER_GROUP_NAME} hmac_secret: | {{- fileContent "/config/secrets/authelia_identity_providers_oidc_hmac_secret.txt" | nindent 6 }} jwks: - key: | {{- fileContent "/config/secrets/jwks-key.pem" | nindent 10 }} clients: # Authelia OIDC Clients BEGIN # Authelia OIDC Client default BEGIN - client_id: default client_name: Default client_secret: '$(htpasswd -bnBC 10 "" $(pwgen -c -n 64 1) | cut -d":" -f2)' public: false authorization_policy: two_factor redirect_uris: - https://$SUB_HSHQHOME.$HOMESERVER_DOMAIN # Authelia OIDC Client default END # Authelia OIDC Clients END ... EOFAC set +e replaceTextBlockInFile "#AC_BEGIN" "#AC_END" "$(getAutheliaBlock)" $HSHQ_STACKS_DIR/authelia/config/configuration.yml true set -e } function performUpdateAuthelia() { perform_stack_name=authelia prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=authelia/authelia:4.37.5,bitnami/redis:7.0.5 image_update_map[0]="authelia/authelia:4.37.5,authelia/authelia:4.38.2" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateAutheliaConfigV2 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v4 curImageList=authelia/authelia:4.38.2,bitnami/redis:7.0.5 image_update_map[0]="authelia/authelia:4.38.2,authelia/authelia:4.38.17" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 3) newVer=v4 curImageList=authelia/authelia:4.38.9,bitnami/redis:7.0.5 image_update_map[0]="authelia/authelia:4.38.9,authelia/authelia:4.38.17" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 4) newVer=v5 curImageList=authelia/authelia:4.38.17,bitnami/redis:7.0.5 image_update_map[0]="authelia/authelia:4.38.17,authelia/authelia:4.39.1" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfAutheliaFixConfigV133 false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v6 curImageList=authelia/authelia:4.39.1,bitnami/redis:7.4.2 image_update_map[0]="authelia/authelia:4.39.1,mirror.gcr.io/authelia/authelia:4.39.6" image_update_map[1]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfAutheliaFixConfigV133 true mfAutheliaFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v7 curImageList=mirror.gcr.io/authelia/authelia:4.39.6,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/authelia/authelia:4.39.6,mirror.gcr.io/authelia/authelia:4.39.15" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfUpdateAutheliaConfigV7 false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=mirror.gcr.io/authelia/authelia:4.39.15,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/authelia/authelia:4.39.15,mirror.gcr.io/authelia/authelia:4.39.20" image_update_map[1]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" ;; 8) newVer=v8 curImageList=mirror.gcr.io/authelia/authelia:4.39.20,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/authelia/authelia:4.39.20,mirror.gcr.io/authelia/authelia:4.39.20" image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfUpdateAutheliaConfigV2() { configfile=$HSHQ_STACKS_DIR/authelia/config/configuration.yml acblock="$(sed -n "/#AC_BEGIN/,/#AC_END/p" $configfile | sed '1d' | sed '$d')" outputConfigAuthelia replaceTextBlockInFile "#AC_BEGIN" "#AC_END" "$acblock" $configfile true rm -f $HOME/authelia.env cat < $HOME/authelia.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/run/secrets/authelia_jwt_secret AUTHELIA_SESSION_SECRET_FILE=/run/secrets/authelia_session_secret AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/run/secrets/authelia_storage_encryption_key AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE=/run/secrets/ldap_admin_user_password AUTHELIA_SESSION_REDIS_PASSWORD_FILE=/run/secrets/authelia_redis_password ALLOW_EMPTY_PASSWORD=no REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL REDIS_AOF_ENABLED=no REDIS_TLS_ENABLED=yes REDIS_TLS_PORT=6379 REDIS_TLS_CERT_FILE=/tls/authelia-redis.crt REDIS_TLS_KEY_FILE=/tls/authelia-redis.key REDIS_TLS_CA_FILE=/tls/${CERTS_ROOT_CA_NAME}.crt REDIS_TLS_DH_PARAMS_FILE=/tls/dhparam.pem REDIS_TLS_AUTH_CLIENTS=no EOFAE set +e docker ps | grep codeserver > /dev/null 2>&1 if [ $? -eq 0 ]; then docker container restart codeserver > /dev/null 2>&1 fi } function mfAutheliaFixConfigV133() { set +e grep "selection_criteria:" $HSHQ_STACKS_DIR/authelia/config/configuration.yml > /dev/null 2>&1 if [ $? -ne 0 ]; then sed -i "s/user_verification:.*/selection_criteria:\n user_verification: preferred/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml fi } function outputAutheliaIDPSecretsV108() { mkdir -p $HSHQ_STACKS_DIR/authelia/config/secrets if ! [ -f $HSHQ_STACKS_DIR/authelia/config/secrets/authelia_identity_providers_oidc_hmac_secret.txt ]; then echo "$(pwgen -c -n 64 1)" > $HSHQ_STACKS_DIR/authelia/config/secrets/authelia_identity_providers_oidc_hmac_secret.txt chmod 0400 $HSHQ_STACKS_DIR/authelia/config/secrets/authelia_identity_providers_oidc_hmac_secret.txt fi if ! [ -f $HSHQ_STACKS_DIR/authelia/config/secrets/jwks-key.pem ]; then openssl genrsa -out $HSHQ_STACKS_DIR/authelia/config/secrets/jwks-key.pem 2048 fi } function mfAutheliaFixRedisCompose() { lineStart=" authelia-redis:" lineEnd="$(grep REDIS_PASSWORD=.* $HOME/authelia-compose.yml)" rpass="$(echo $lineEnd | cut -d"=" -f2)" replaceText="" replaceText=$replaceText">>authelia-redis:\n" replaceText=$replaceText">>>>image: mirror.gcr.io/redis:8.2.0-bookworm\n" replaceText=$replaceText">>>>container_name: authelia-redis\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>env_file: stack.env\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--requirepass $rpass\n" replaceText=$replaceText">>>>>>--tls-port 6379\n" replaceText=$replaceText">>>>>>--port 0\n" replaceText=$replaceText">>>>>>--tls-cert-file /tls/authelia-redis.crt\n" replaceText=$replaceText">>>>>>--tls-key-file /tls/authelia-redis.key\n" replaceText=$replaceText">>>>>>--tls-ca-cert-file /tls/${CERTS_ROOT_CA_NAME}.crt\n" replaceText=$replaceText">>>>>>--tls-auth-clients no\n" replaceText=$replaceText">>>>>>--appendonly no\n" replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- int-authelia-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n" replaceText=$replaceText">>>>>>- /etc/ssl/certs:/etc/ssl/certs:ro\n" replaceText=$replaceText">>>>>>- /usr/share/ca-certificates:/usr/share/ca-certificates:ro\n" replaceText=$replaceText">>>>>>- /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro\n" replaceText=$replaceText">>>>>>- \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/tls/${CERTS_ROOT_CA_NAME}.crt:ro\n" replaceText=$replaceText">>>>>>- \${PORTAINER_HSHQ_SSL_DIR}/authelia-redis.crt:/tls/authelia-redis.crt:ro\n" replaceText=$replaceText">>>>>>- \${PORTAINER_HSHQ_SSL_DIR}/authelia-redis.key:/tls/authelia-redis.key:ro\n" replaceText=$replaceText">>>>>>- \${PORTAINER_HSHQ_SSL_DIR}/dhparam.pem:/tls/dhparam.pem:ro\n" replaceTextBlockInFile "$lineStart" "$lineEnd" "$replaceText" $HOME/authelia-compose.yml false ">" cat -s $HOME/authelia-compose.yml > $HOME/ac.tmp mv $HOME/ac.tmp $HOME/authelia-compose.yml sed -i "/ALLOW_EMPTY_PASSWORD=.*/d" $HOME/authelia.env sed -i "/REDIS_DISABLE_COMMANDS=.*/d" $HOME/authelia.env sed -i "/REDIS_AOF_ENABLED=.*/d" $HOME/authelia.env sed -i "/REDIS_TLS_ENABLED=.*/d" $HOME/authelia.env sed -i "/REDIS_TLS_PORT=.*/d" $HOME/authelia.env sed -i "/REDIS_TLS_CERT_FILE=.*/d" $HOME/authelia.env sed -i "/REDIS_TLS_KEY_FILE=.*/d" $HOME/authelia.env sed -i "/REDIS_TLS_CA_FILE=.*/d" $HOME/authelia.env sed -i "/REDIS_TLS_DH_PARAMS_FILE=.*/d" $HOME/authelia.env sed -i "/REDIS_TLS_AUTH_CLIENTS=.*/d" $HOME/authelia.env } function mfUpdateAutheliaConfigV7() { sed -i "s/address: ldap:\/\/ldapserver:389/address: ldaps:\/\/ldapserver:636/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml sed -i "s/start_tls: true/start_tls: false/" $HSHQ_STACKS_DIR/authelia/config/configuration.yml } # WordPress function installWordPress() { set +e is_integrate_hshq=$1 # Check if directory exists checkDeleteStackAndDirectory wordpress "WordPress" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName wordpress-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName wordpress-web) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName wordpress-cli) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName wordpress-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/wordpress mkdir $HSHQ_STACKS_DIR/wordpress/db mkdir $HSHQ_STACKS_DIR/wordpress/dbexport mkdir $HSHQ_STACKS_DIR/wordpress/web chmod 777 $HSHQ_STACKS_DIR/wordpress/dbexport initServicesCredentials set +e addUserMailu alias $WORDPRESS_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$WORDPRESS_INIT_ENV" = "true" ]; then sendEmail -s "WordPress Admin Login Info" -b "Wordpress Admin Username: $WORDPRESS_ADMIN_USERNAME\nWordPress Admin Password: $WORDPRESS_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" WORDPRESS_INIT_ENV=true updateConfigVar WORDPRESS_INIT_ENV $WORDPRESS_INIT_ENV fi outputConfigWordPress installStack wordpress wordpress-web "apache2 -D FOREGROUND" $HOME/wordpress.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 cd ~ set +e sudo chmod -R 775 $HSHQ_STACKS_DIR/wordpress/web sudo chown -R 33:33 $HSHQ_STACKS_DIR/wordpress/web sleep 1 docker run --user www-data --rm --name wordpress-cli --hostname wordpress-cli -e TZ="$TZ" --env-file wpstack.env -v "/etc/localtime:/etc/localtime:ro" -v "/etc/timezone:/etc/timezone:ro" -v "/etc/ssl/certs:/etc/ssl/certs:ro" -v "/usr/share/ca-certificates:/usr/share/ca-certificates:ro" -v "/usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro" -v "$HSHQ_STACKS_DIR/wordpress/web:/var/www/html" --restart no --network dock-dbs $(getScriptImageByContainerName wordpress-cli) sh -c "sleep 10; wp core install --path=\"/var/www/html\" --url=\"https://$SUB_WORDPRESS.$HOMESERVER_DOMAIN\" --title=\"$HOMESERVER_NAME Blog\" --admin_user=$WORDPRESS_ADMIN_USERNAME --admin_password=$WORDPRESS_ADMIN_PASSWORD --admin_email=$WORDPRESS_ADMIN_EMAIL_ADDRESS --skip-email" sleep 3 docker run --user www-data --rm --name wordpress-cli --env-file wpstack.env -v "$HSHQ_STACKS_DIR/wordpress/web:/var/www/html" --network dock-dbs mirror.gcr.io/wordpress:cli-php8.5 wp core is-installed --path="/var/www/html" rtVal=$? if [ $rtVal -ne 0 ]; then echo "WordPress installation failed. Please remove the stack and try again." return 1 fi WORDPRESS_APP_PASSWORD=$(docker run --user www-data --rm --name wordpress-cli --hostname wordpress-cli -e TZ="$TZ" --env-file wpstack.env -v "/etc/localtime:/etc/localtime:ro" -v "/etc/timezone:/etc/timezone:ro" -v "/etc/ssl/certs:/etc/ssl/certs:ro" -v "/usr/share/ca-certificates:/usr/share/ca-certificates:ro" -v "/usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro" -v "$HSHQ_STACKS_DIR/wordpress/web:/var/www/html" --restart no --network dock-dbs $(getScriptImageByContainerName wordpress-cli) sh -c "wp user application-password create $WORDPRESS_ADMIN_USERNAME testapp --porcelain") updateConfigVar WORDPRESS_APP_PASSWORD $WORDPRESS_APP_PASSWORD rm -f $HOME/wpstack.env addReadOnlyUserToDatabase Wordpress mysql wordpress-db $WORDPRESS_DATABASE_NAME root $WORDPRESS_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/wordpress/dbexport $WORDPRESS_DATABASE_READONLYUSER $WORDPRESS_DATABASE_READONLYUSER_PASSWORD addMCPServerLiteLLM "wordpress" "wp" "http://wordpress-mcp:80/mcp" http none "" updateStackEnv wordpress mfWordpressAddExtraConfig inner_block="" inner_block=$inner_block">>https://$SUB_WORDPRESS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>@insecureadmin {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_NOTHOMESUBNET\n" inner_block=$inner_block">>>>>>>>path /wp-login.php*\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>redir @insecureadmin /\n" inner_block=$inner_block">>>>>>reverse_proxy http://wordpress-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_WORDPRESS $MANAGETLS_WORDPRESS "$is_integrate_hshq" $NETDEFAULT_WORDPRESS "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll wordpress "$FMLNAME_WORDPRESS" $USERTYPE_WORDPRESS "https://$SUB_WORDPRESS.$HOMESERVER_DOMAIN" "wordpress.png" "$(getHeimdallOrderFromSub $SUB_WORDPRESS $USERTYPE_WORDPRESS)" restartAllCaddyContainers fi } function outputConfigWordPress() { cat < $HOME/wordpress-compose.yml $STACK_VERSION_PREFIX wordpress $(getScriptStackVersion wordpress) services: wordpress-db: image: $(getScriptImageByContainerName wordpress-db) container_name: wordpress-db hostname: wordpress-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mariadbd --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-wordpress-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-wordpress-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMariaDB.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wordpress/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.wordpress-hourly-db.schedule=@every 1h" - "ofelia.job-exec.wordpress-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.wordpress-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wordpress-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wordpress-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wordpress-hourly-db.email-from=WordPress Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wordpress-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.wordpress-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.wordpress-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.wordpress-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wordpress-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wordpress-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wordpress-monthly-db.email-from=WordPress Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wordpress-monthly-db.mail-only-on-error=false" wordpress-web: image: $(getScriptImageByContainerName wordpress-web) container_name: wordpress-web hostname: wordpress-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-wordpress-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-wordpress-web:/var/www/html wordpress-mcp: image: $(getScriptImageByContainerName wordpress-mcp) container_name: wordpress-mcp hostname: wordpress-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-wordpress-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-wordpress-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wordpress/db v-wordpress-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wordpress/web networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-wordpress-net: driver: bridge internal: true ipam: driver: default EOFWP cat < $HOME/wordpress.env MARIADB_ROOT_PASSWORD=$WORDPRESS_DATABASE_ROOT_PASSWORD MARIADB_DATABASE=$WORDPRESS_DATABASE_NAME MARIADB_USER=$WORDPRESS_DATABASE_USER MARIADB_PASSWORD=$WORDPRESS_DATABASE_USER_PASSWORD WORDPRESS_DB_HOST=wordpress-db WORDPRESS_DB_NAME=$WORDPRESS_DATABASE_NAME WORDPRESS_DB_USER=$WORDPRESS_DATABASE_USER WORDPRESS_DB_PASSWORD=$WORDPRESS_DATABASE_USER_PASSWORD WORDPRESS_BASE_URL=http://wordpress-web:80 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false WORDPRESS_PUBLIC_URL=https://$SUB_WORDPRESS.$HOMESERVER_DOMAIN EOFWP rm -f $HOME/wpstack.env cp $HOME/wordpress.env $HOME/wpstack.env } function performUpdateWordPress() { perform_stack_name=wordpress prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mariadb:10.7.3,wordpress:php8.2-apache image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="wordpress:php8.2-apache,wordpress:php8.3-apache" ;; 2) newVer=v4 curImageList=mariadb:10.7.3,wordpress:php8.3-apache image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="wordpress:php8.3-apache,mirror.gcr.io/wordpress:php8.5-apache" ;; 3) newVer=v4 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/wordpress:php8.4-apache image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/wordpress:php8.4-apache,mirror.gcr.io/wordpress:php8.5-apache" ;; 4) newVer=v5 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/wordpress:php8.5-apache image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/wordpress:php8.5-apache,mirror.gcr.io/wordpress:php8.5-apache" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfWordpressV5Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v5 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/wordpress:php8.5-apache,ghcr.io/homeserverhq/wordpress-mcp:v2 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/wordpress:php8.5-apache,mirror.gcr.io/wordpress:php8.5-apache" image_update_map[2]="ghcr.io/homeserverhq/wordpress-mcp:v2,ghcr.io/homeserverhq/wordpress-mcp:v2" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Wordpress cannot be upgraded to the next version automatically. To perform the upgrade, follow these steps: 1) Shut down the stack. 2) Add MARIADB_AUTO_UPGRADE=1 to the environment variables. 3) Replace all MYSQL_ env var prefixes with MARIADB_. 4) Replace mirror.gcr.io/mariadb:10.7.3 with mirror.gcr.io/mariadb:11.4.12. 5) Replace the db backup script name in the compose file from exportMySQL.sh to exportMariaDB.sh, e.g. \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMariaDB.sh:/exportDB.sh:ro 6) Restart the stack, and monitor the upgrade process in the logs. 7) After the upgrade has entirely completed, stop the stack, remove the MARIADB_AUTO_UPGRADE=1 env var, then restart the stack." return ;; 6) newVer=v6 curImageList=mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/wordpress:php8.5-apache,ghcr.io/homeserverhq/wordpress-mcp:v2 image_update_map[0]="mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/mariadb:11.4.12" image_update_map[1]="mirror.gcr.io/wordpress:php8.5-apache,mirror.gcr.io/wordpress:php8.5-apache" image_update_map[2]="ghcr.io/homeserverhq/wordpress-mcp:v2,ghcr.io/homeserverhq/wordpress-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfWordpressV5Update() { cat < $HOME/wordpress-compose.yml $STACK_VERSION_PREFIX wordpress v5 services: wordpress-db: image: mirror.gcr.io/mariadb:10.7.3 container_name: wordpress-db hostname: wordpress-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-wordpress-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-wordpress-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wordpress/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.wordpress-hourly-db.schedule=@every 1h" - "ofelia.job-exec.wordpress-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.wordpress-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wordpress-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wordpress-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wordpress-hourly-db.email-from=WordPress Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wordpress-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.wordpress-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.wordpress-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.wordpress-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wordpress-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wordpress-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wordpress-monthly-db.email-from=WordPress Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wordpress-monthly-db.mail-only-on-error=false" wordpress-web: image: mirror.gcr.io/wordpress:php8.5-apache container_name: wordpress-web hostname: wordpress-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-wordpress-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-wordpress-web:/var/www/html wordpress-mcp: image: ghcr.io/homeserverhq/wordpress-mcp:v2 container_name: wordpress-mcp hostname: wordpress-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-wordpress-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-wordpress-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wordpress/db v-wordpress-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wordpress/web networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-wordpress-net: driver: bridge internal: true ipam: driver: default EOFWP cat < $HOME/wordpress.env MARIADB_ROOT_PASSWORD=$WORDPRESS_DATABASE_ROOT_PASSWORD MARIADB_DATABASE=$WORDPRESS_DATABASE_NAME MARIADB_USER=$WORDPRESS_DATABASE_USER MARIADB_PASSWORD=$WORDPRESS_DATABASE_USER_PASSWORD WORDPRESS_DB_HOST=wordpress-db WORDPRESS_DB_NAME=$WORDPRESS_DATABASE_NAME WORDPRESS_DB_USER=$WORDPRESS_DATABASE_USER WORDPRESS_DB_PASSWORD=$WORDPRESS_DATABASE_USER_PASSWORD WORDPRESS_BASE_URL=http://wordpress-web:80 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false WORDPRESS_PUBLIC_URL=https://$SUB_WORDPRESS.$HOMESERVER_DOMAIN WORDPRESS_CONFIG_EXTRA=\$\$_SERVER['HTTPS']='on';define('FORCE_SSL_ADMIN',true);if(isset(\$\$_SERVER['HTTP_X_FORWARDED_HOST'])){\$\$_SERVER['HTTP_HOST']=\$\$_SERVER['HTTP_X_FORWARDED_HOST'];}if(isset(\$\$_SERVER['HTTP_X_FORWARDED_PROTO'])&&strpos(\$\$_SERVER['HTTP_X_FORWARDED_PROTO'],'https')!==false){\$\$_SERVER['HTTPS']='on';}\$\$proto=(isset(\$\$_SERVER['HTTPS'])&&\$\$_SERVER['HTTPS']==='on')?'https://':'http://';define('WP_HOME',\$\$proto.\$\$_SERVER['HTTP_HOST']);define('WP_SITEURL',\$\$proto.\$\$_SERVER['HTTP_HOST']); EOFWP addMCPServerLiteLLM "wordpress" "wp" "http://wordpress-mcp:80/mcp" http none "" } function mfWordpressAddExtraConfig() { set +e grep -q "WORDPRESS_CONFIG_EXTRA=" $HOME/wordpress.env if [ $? -ne 0 ]; then echo "WORDPRESS_CONFIG_EXTRA=\$\$_SERVER['HTTPS']='on';define('FORCE_SSL_ADMIN',true);if(isset(\$\$_SERVER['HTTP_X_FORWARDED_HOST'])){\$\$_SERVER['HTTP_HOST']=\$\$_SERVER['HTTP_X_FORWARDED_HOST'];}if(isset(\$\$_SERVER['HTTP_X_FORWARDED_PROTO'])&&strpos(\$\$_SERVER['HTTP_X_FORWARDED_PROTO'],'https')!==false){\$\$_SERVER['HTTPS']='on';}\$\$proto=(isset(\$\$_SERVER['HTTPS'])&&\$\$_SERVER['HTTPS']==='on')?'https://':'http://';define('WP_HOME',\$\$proto.\$\$_SERVER['HTTP_HOST']);define('WP_SITEURL',\$\$proto.\$\$_SERVER['HTTP_HOST']);" >> $HOME/wordpress.env fi } # Ghost function installGhost() { set +e is_integrate_hshq=$1 # Have to check if directory exits checkDeleteStackAndDirectory ghost "Ghost" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ghost-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ghost-web) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/ghost mkdir $HSHQ_STACKS_DIR/ghost/db mkdir $HSHQ_STACKS_DIR/ghost/dbexport mkdir $HSHQ_STACKS_DIR/ghost/web chmod 777 $HSHQ_STACKS_DIR/ghost/dbexport initServicesCredentials if [ -z "$GHOST_DATABASE_ROOT_PASSWORD" ]; then GHOST_DATABASE_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GHOST_DATABASE_ROOT_PASSWORD $GHOST_DATABASE_ROOT_PASSWORD fi outputConfigGhost installStack ghost ghost-web "Ghost booted in" $HOME/ghost.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase Ghost mysql ghost-db $GHOST_DATABASE_NAME root $GHOST_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/ghost/dbexport $GHOST_DATABASE_READONLYUSER $GHOST_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_GHOST.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>@insecureadmin {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_NOTHOMESUBNET\n" inner_block=$inner_block">>>>>>>>path /ghost/*\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>redir @insecureadmin /\n" inner_block=$inner_block">>>>>>reverse_proxy http://ghost-web:2368 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_GHOST $MANAGETLS_GHOST "$is_integrate_hshq" $NETDEFAULT_GHOST "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll ghost "$FMLNAME_GHOST" $USERTYPE_GHOST "https://$SUB_GHOST.$HOMESERVER_DOMAIN" "ghost.png" "$(getHeimdallOrderFromSub $SUB_GHOST $USERTYPE_GHOST)" restartAllCaddyContainers fi } function outputConfigGhost() { cat < $HOME/ghost-compose.yml $STACK_VERSION_PREFIX ghost $(getScriptStackVersion ghost) services: ghost-db: image: $(getScriptImageByContainerName ghost-db) container_name: ghost-db hostname: ghost-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-ghost-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ghost-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ghost/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.ghost-hourly-db.schedule=@every 1h" - "ofelia.job-exec.ghost-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.ghost-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ghost-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ghost-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ghost-hourly-db.email-from=Ghost Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ghost-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.ghost-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.ghost-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.ghost-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ghost-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ghost-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ghost-monthly-db.email-from=Ghost Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ghost-monthly-db.mail-only-on-error=false" ghost-web: image: $(getScriptImageByContainerName ghost-web) container_name: ghost-web hostname: ghost-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ghost-db networks: - int-ghost-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-ghost-web:/var/lib/ghost/content volumes: v-ghost-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ghost/db v-ghost-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ghost/web networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-ghost-net: driver: bridge internal: true ipam: driver: default EOFGW cat < $HOME/ghost.env MYSQL_DATABASE=$GHOST_DATABASE_NAME MYSQL_ROOT_PASSWORD=$GHOST_DATABASE_ROOT_PASSWORD MYSQL_USER=$GHOST_DATABASE_USER MYSQL_PASSWORD=$GHOST_DATABASE_USER_PASSWORD database__client=mysql database__connection__host=ghost-db database__connection__database=$GHOST_DATABASE_NAME database__connection__user=$GHOST_DATABASE_USER database__connection__password=$GHOST_DATABASE_USER_PASSWORD url=https://$SUB_GHOST.$HOMESERVER_DOMAIN NODE_ENV=development EOFGW } function performUpdateGhost() { perform_stack_name=ghost prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=mariadb:10.7.3,ghost:5.59.1-alpine image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="ghost:5.59.1-alpine,ghost:5.88.2-alpine" ;; 2) newVer=v5 curImageList=mariadb:10.7.3,ghost:5.75.2-alpine image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="ghost:5.75.2-alpine,ghost:5.88.2-alpine" ;; 3) newVer=v5 curImageList=mariadb:10.7.3,ghost:5.78.0-alpine image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="ghost:5.78.0-alpine,ghost:5.88.2-alpine" ;; 4) newVer=v5 curImageList=mariadb:10.7.3,ghost:5.80.3-alpine image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="ghost:5.80.3-alpine,ghost:5.88.2-alpine" ;; 5) newVer=v8 curImageList=mariadb:10.7.3,ghost:5.88.2-alpine image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="ghost:5.88.2-alpine,mirror.gcr.io/ghost:6.0.0-alpine" ;; 6) newVer=v8 curImageList=mariadb:10.7.3,ghost:5.98.1-alpine image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="ghost:5.98.1-alpine,mirror.gcr.io/ghost:6.0.0-alpine" ;; 7) newVer=v8 curImageList=mariadb:10.7.3,ghost:5.115.1-alpine image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="ghost:5.115.1-alpine,mirror.gcr.io/ghost:6.0.0-alpine" ;; 8) newVer=v9 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/ghost:6.0.0-alpine image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/ghost:6.0.0-alpine,mirror.gcr.io/ghost:6.9.3-alpine" ;; 9) newVer=v9 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/ghost:6.9.3-alpine image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/ghost:6.9.3-alpine,mirror.gcr.io/ghost:6.9.3-alpine" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # PeerTube function installPeerTube() { set +e is_integrate_hshq=$1 # Evaulate whether directory exists checkDeleteStackAndDirectory peertube "PeerTube" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName peertube-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName peertube-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName peertube-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/peertube mkdir $HSHQ_STACKS_DIR/peertube/config mkdir $HSHQ_STACKS_DIR/peertube/db mkdir $HSHQ_STACKS_DIR/peertube/dbexport mkdir $HSHQ_STACKS_DIR/peertube/data chmod 777 $HSHQ_STACKS_DIR/peertube/dbexport mkdir $HSHQ_NONBACKUP_DIR/peertube mkdir $HSHQ_NONBACKUP_DIR/peertube/assets initServicesCredentials pt_secret=$(openssl rand -hex 32) set +e addUserMailu alias $ADMIN_USERNAME_BASE"_peertube" $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigPeerTube installStack peertube peertube-app "HTTP server listening on 0.0.0.0" $HOME/peertube.env retval=$? if [ $retval -ne 0 ]; then return $retval fi docker exec peertube-app bash -c "echo $PEERTUBE_ADMIN_PASSWORD | npm run reset-password -- -u root" > /dev/null echo "Installing plugins..." set +e docker exec -u 999 peertube-app bash -c "timeout 60 npm run plugin:install -- --npm-name peertube-plugin-auth-ldap" > /dev/null if [ $? -ne 0 ]; then echo "ERROR: Could not install LDAP plugin, exiting..." return 2 fi docker exec -u 999 peertube-app bash -c "timeout 60 npm run plugin:install -- --npm-name peertube-plugin-livechat" > /dev/null if [ $? -ne 0 ]; then echo "ERROR: Could not install LiveChat plugin, continuing..." fi docker exec -u 999 peertube-app bash -c "timeout 60 npm run plugin:install -- --npm-name peertube-theme-dark" > /dev/null if [ $? -ne 0 ]; then echo "ERROR: Could not install Dark Theme plugin, continuing..." fi set -e docker exec peertube-db /dbexport/setupLDAP.sh rm -f $HSHQ_STACKS_DIR/peertube/dbexport/setupLDAP.sh addReadOnlyUserToDatabase PeerTube postgres peertube-db $PEERTUBE_DATABASE_NAME $PEERTUBE_DATABASE_USER $PEERTUBE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/peertube/dbexport $PEERTUBE_DATABASE_READONLYUSER $PEERTUBE_DATABASE_READONLYUSER_PASSWORD docker container restart peertube-app inner_block="" inner_block=$inner_block">>https://$SUB_PEERTUBE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://peertube-app:9000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PEERTUBE $MANAGETLS_PEERTUBE "$is_integrate_hshq" $NETDEFAULT_PEERTUBE "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll peertube "$FMLNAME_PEERTUBE" $USERTYPE_PEERTUBE "https://$SUB_PEERTUBE.$HOMESERVER_DOMAIN" "peertube.png" "$(getHeimdallOrderFromSub $SUB_PEERTUBE $USERTYPE_PEERTUBE)" restartAllCaddyContainers fi } function outputConfigPeerTube() { cat < $HOME/peertube-compose.yml $STACK_VERSION_PREFIX peertube $(getScriptStackVersion peertube) services: peertube-db: image: $(getScriptImageByContainerName peertube-db) container_name: peertube-db hostname: peertube-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb depends_on: - peertube-redis networks: - int-peertube-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/peertube/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/peertube/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.peertube-hourly-db.schedule=@every 1h" - "ofelia.job-exec.peertube-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.peertube-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.peertube-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.peertube-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.peertube-hourly-db.email-from=PeerTube Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.peertube-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.peertube-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.peertube-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.peertube-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.peertube-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.peertube-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.peertube-monthly-db.email-from=PeerTube Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.peertube-monthly-db.mail-only-on-error=false" peertube-app: image: $(getScriptImageByContainerName peertube-app) container_name: peertube-app hostname: peertube-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-peertube-net - dock-ext-net - dock-ldap-net - dock-internalmail-net ports: - "$PEERTUBE_RDP_PORT:$PEERTUBE_RDP_PORT" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/peertube/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/peertube/config:/config - v-peertube-assets:/app/client/dist environment: - PEERTUBE_TRUST_PROXY=["127.0.0.1", "loopback", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] peertube-redis: image: $(getScriptImageByContainerName peertube-redis) container_name: peertube-redis hostname: peertube-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --appendonly no networks: - int-peertube-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-peertube-assets: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/peertube/assets networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true int-peertube-net: driver: bridge internal: true ipam: driver: default EOFPT cat < $HOME/peertube.env UID=$USERID GID=$GROUPID POSTGRES_DB=$PEERTUBE_DATABASE_NAME POSTGRES_USER=$PEERTUBE_DATABASE_USER POSTGRES_PASSWORD=$PEERTUBE_DATABASE_USER_PASSWORD PEERTUBE_DB_SUFFIX=db PEERTUBE_DB_HOSTNAME=peertube-db PEERTUBE_DB_USERNAME=$PEERTUBE_DATABASE_USER PEERTUBE_DB_PASSWORD=$PEERTUBE_DATABASE_USER_PASSWORD PEERTUBE_DB_SSL=false PEERTUBE_SECRET=$pt_secret PEERTUBE_WEBSERVER_HOSTNAME=$SUB_PEERTUBE.$HOMESERVER_DOMAIN PEERTUBE_WEBSERVER_PORT=443 PEERTUBE_WEBSERVER_HTTPS=true PEERTUBE_SMTP_HOSTNAME=$SMTP_HOSTNAME PEERTUBE_SMTP_PORT=$SMTP_HOSTPORT PEERTUBE_SMTP_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS PEERTUBE_SMTP_TLS=false PEERTUBE_SMTP_DISABLE_STARTTLS=false PEERTUBE_ADMIN_EMAIL=$PEERTUBE_ADMIN_EMAIL_ADDRESS PEERTUBE_REDIS_HOSTNAME=peertube-redis PEERTUBE_REDIS_PORT=6379 EOFPT cat < $HSHQ_STACKS_DIR/peertube/config/production.yaml smtp: ca_file: /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt theme: default: 'dark' EOFPT cat < $HSHQ_STACKS_DIR/peertube/dbexport/setupLDAP.sh #!/bin/bash PGPASSWORD=$PEERTUBE_DATABASE_USER_PASSWORD sqlcmd="update plugin set settings='{\"url\": \"ldaps://ldapserver:636\",\"weight\": 100,\"bind-dn\": \"$LDAP_READONLY_USER_BIND_DN\",\"custom-ca\": \"/usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt\",\"group-mod\": \"\",\"group-base\": \"\",\"group-user\": \"\",\"group-admin\": \"\",\"search-base\": \"$LDAP_BASE_DN\",\"group-filter\": \"\",\"insecure-tls\": false,\"mail-property\": \"mail\",\"search-filter\": \"(&(|(mail={{username}})(uid={{username}}))(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))\",\"bind-credentials\": \"$LDAP_READONLY_USER_PASSWORD\",\"username-property\": \"uid\",\"mail-property-index\": \"0\"}' where name='auth-ldap';" echo "\$sqlcmd" | psql -U $PEERTUBE_DATABASE_USER $PEERTUBE_DATABASE_NAME EOFPT chmod +x $HSHQ_STACKS_DIR/peertube/dbexport/setupLDAP.sh } function performUpdatePeerTube() { perform_stack_name=peertube prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v5.2.0-bullseye,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="chocobozzz/peertube:v5.2.0-bullseye,chocobozzz/peertube:v6.2.0-bookworm" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 2) newVer=v6 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v6.0.2-bookworm,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="chocobozzz/peertube:v6.0.2-bookworm,chocobozzz/peertube:v7.1.0-bookworm" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 3) newVer=v6 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v6.0.3-bookworm,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="chocobozzz/peertube:v6.0.3-bookworm,chocobozzz/peertube:v7.1.0-bookworm" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 4) newVer=v6 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v6.2.0-bookworm,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="chocobozzz/peertube:v6.2.0-bookworm,chocobozzz/peertube:v7.1.0-bookworm" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v6.3.3-bookworm,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="chocobozzz/peertube:v6.3.3-bookworm,chocobozzz/peertube:v7.1.0-bookworm" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 6) newVer=v8 curImageList=postgres:15.0-bullseye,chocobozzz/peertube:v7.1.0-bookworm,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="chocobozzz/peertube:v7.1.0-bookworm,mirror.gcr.io/chocobozzz/peertube:v7.3.0-bookworm" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.4.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPeerTubeFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/chocobozzz/peertube:v7.2.3-bookworm,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/chocobozzz/peertube:v7.2.3-bookworm,mirror.gcr.io/chocobozzz/peertube:v7.3.0-bookworm" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 8) newVer=v8 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/chocobozzz/peertube:v7.3.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/chocobozzz/peertube:v7.3.0-bookworm,mirror.gcr.io/chocobozzz/peertube:v7.3.0-bookworm" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfPeerTubeFixRedisCompose() { lineStart=" peertube-redis:" lineEnd=" - ALLOW_EMPTY_PASSWORD=yes" replaceText="" replaceText=$replaceText">>peertube-redis:\n" replaceText=$replaceText">>>>image: mirror.gcr.io/redis:8.2.0-bookworm\n" replaceText=$replaceText">>>>container_name: peertube-redis\n" replaceText=$replaceText">>>>hostname: peertube-redis\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--appendonly no\n" replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- int-peertube-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n\n" replaceTextBlockInFile "$lineStart" "$lineEnd" "$replaceText" $HOME/peertube-compose.yml false ">" cat -s $HOME/peertube-compose.yml > $HOME/ac.tmp mv $HOME/ac.tmp $HOME/peertube-compose.yml } function fixPeertubeV7() { set +e peertube_stack_id=$(getStackID peertube) if [ -z "$peertube_stack_id" ]; then return fi peertube_compose=$HSHQ_STACKS_DIR/portainer/compose/$peertube_stack_id/docker-compose.yml peertube_stack_firstline=$(sudo sed -n 1p $peertube_compose) peertube_stack_ver=$(getVersionFromComposeLine "$peertube_stack_firstline") sudo grep -q "chocobozzz/peertube:v7.1.0-bookworm" $peertube_compose rtVal=$? if ! [ "$peertube_stack_ver" = "7" ] || ! [ $rtVal -eq 0 ]; then return fi echo "Fixing peertube stack container images..." pullImage mirror.gcr.io/chocobozzz/peertube:v7.2.3-bookworm retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi updateStackEnv peertube mfPeertubeV7Fix } function mfPeertubeV7Fix() { sed -i "s/chocobozzz\/peertube:v7.1.0-bookworm/mirror.gcr.io\/chocobozzz\/peertube:v7.2.3-bookworm/g" $HOME/peertube-compose.yml } # HomeAssistant function installHomeAssistant() { set +e is_integrate_hshq=$1 # Evaulate whether directory exists checkDeleteStackAndDirectory homeassistant "HomeAssistant" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName homeassistant-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName homeassistant-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName homeassistant-nodered) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName homeassistant-configurator) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName homeassistant-tasmoadmin) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName homeassistant-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/homeassistant mkdir $HSHQ_STACKS_DIR/homeassistant/config mkdir $HSHQ_STACKS_DIR/homeassistant/config/www mkdir $HSHQ_STACKS_DIR/homeassistant/config/www/images mkdir $HSHQ_STACKS_DIR/homeassistant/config/www/plugins mkdir $HSHQ_STACKS_DIR/homeassistant/config/www/themes mkdir $HSHQ_STACKS_DIR/homeassistant/config/www/panels mkdir $HSHQ_STACKS_DIR/homeassistant/configurator mkdir $HSHQ_STACKS_DIR/homeassistant/db mkdir $HSHQ_STACKS_DIR/homeassistant/dbexport mkdir $HSHQ_STACKS_DIR/homeassistant/media mkdir $HSHQ_STACKS_DIR/homeassistant/nodered mkdir $HSHQ_STACKS_DIR/homeassistant/mcp chmod 777 $HSHQ_STACKS_DIR/homeassistant/dbexport sudo chown 1000:1000 $HSHQ_STACKS_DIR/homeassistant/nodered initServicesCredentials generateCert homeassistant-app "homeassistant-app,host.docker.internal" generateCert homeassistant-configurator homeassistant-configurator generateCert homeassistant-nodered homeassistant-nodered outputConfigHomeAssistant # Ensure NodeRed data directory and settings file permissions are set to 1000:1000 sudo chown 1000:1000 $HOME/noderedsettings.conf sudo mv $HOME/noderedsettings.conf $HSHQ_STACKS_DIR/homeassistant/nodered/settings.js installStack homeassistant homeassistant-app "legacy-services successfully started" $HOME/homeassistant.env retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Waiting 10 seconds for HomeAssistant to load" sleep 10 addReadOnlyUserToDatabase HomeAssistant postgres homeassistant-db $HOMEASSISTANT_DATABASE_NAME $HOMEASSISTANT_DATABASE_USER $HOMEASSISTANT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/homeassistant/dbexport $HOMEASSISTANT_DATABASE_READONLYUSER $HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD docker container stop homeassistant-app sudo mv $HSHQ_STACKS_DIR/homeassistant/configuration.yaml $HSHQ_STACKS_DIR/homeassistant/config/configuration.yaml docker container start homeassistant-app inner_block="" inner_block=$inner_block">>https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com; img-src 'self' cdnjs.cloudflare.com *.githubusercontent.com *.$HOMESERVER_DOMAIN data:; frame-src 'self' *.$HOMESERVER_DOMAIN data: blob:; connect-src 'self' catalogue.nodered.org api.github.com cdnjs.cloudflare.com *.$HOMESERVER_DOMAIN wss://*.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests; font-src 'self' cdnjs.cloudflare.com data:;\"\n" inner_block=$inner_block"}>>>>\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://host.docker.internal:$HOMEASSISTANT_LOCALHOST_PORT {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMEASSISTANT_APP $MANAGETLS_HOMEASSISTANT_APP "$is_integrate_hshq" $NETDEFAULT_HOMEASSISTANT_APP "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_HOMEASSISTANT_CONFIGURATOR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com; img-src 'self' cdnjs.cloudflare.com *.githubusercontent.com *.$HOMESERVER_DOMAIN data:; frame-src 'self' *.$HOMESERVER_DOMAIN data: blob:; connect-src 'self' catalogue.nodered.org api.github.com cdnjs.cloudflare.com *.$HOMESERVER_DOMAIN wss://*.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests; font-src 'self' cdnjs.cloudflare.com data:;\"\n" inner_block=$inner_block"}>>>>\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://homeassistant-configurator {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMEASSISTANT_CONFIGURATOR $MANAGETLS_HOMEASSISTANT_CONFIGURATOR "$is_integrate_hshq" $NETDEFAULT_HOMEASSISTANT_CONFIGURATOR "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_HOMEASSISTANT_NODERED.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://homeassistant-nodered {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMEASSISTANT_NODERED $MANAGETLS_HOMEASSISTANT_NODERED "$is_integrate_hshq" $NETDEFAULT_HOMEASSISTANT_NODERED "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com; img-src 'self' cdnjs.cloudflare.com *.githubusercontent.com *.$HOMESERVER_DOMAIN data:; frame-src 'self' *.$HOMESERVER_DOMAIN data: blob:; connect-src 'self' catalogue.nodered.org api.github.com cdnjs.cloudflare.com *.$HOMESERVER_DOMAIN wss://*.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests; font-src 'self' cdnjs.cloudflare.com data:;\"\n" inner_block=$inner_block"}>>>>\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://homeassistant-tasmoadmin {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMEASSISTANT_TASMOADMIN $MANAGETLS_HOMEASSISTANT_TASMOADMIN "$is_integrate_hshq" $NETDEFAULT_HOMEASSISTANT_TASMOADMIN "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll homeassistant "$FMLNAME_HOMEASSISTANT_APP" $USERTYPE_HOMEASSISTANT_APP "https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN" "homeassistant.png" "$(getHeimdallOrderFromSub $SUB_HOMEASSISTANT_APP $USERTYPE_HOMEASSISTANT_APP)" insertEnableSvcUptimeKuma homeassistant "$FMLNAME_HOMEASSISTANT_NODERED" $USERTYPE_HOMEASSISTANT_NODERED "https://$SUB_HOMEASSISTANT_NODERED.$HOMESERVER_DOMAIN" true insertEnableSvcUptimeKuma homeassistant "$FMLNAME_HOMEASSISTANT_TASMOADMIN" $USERTYPE_HOMEASSISTANT_TASMOADMIN "https://$SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN" true restartAllCaddyContainers fi } function outputConfigHomeAssistant() { outputComposeHomeAssistant cat < $HOME/homeassistant.env UID=$USERID GID=$GROUPID PYTHON_VER=python3.14 HOMEASSISTANT_URL=https://host.docker.internal:$HOMEASSISTANT_LOCALHOST_PORT HOMEASSISTANT_TOKEN=your_long_lived_access_token_here HOMEASSISTANT_VERIFY_SSL=false SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt HA_VERIFY_SSL=false HA_TIMEOUT=30 HA_MAX_RETRIES=3 FUZZY_THRESHOLD=60 ENTITY_SEARCH_LIMIT=20 ENABLE_WEBSOCKET=true DEBUG=false LOG_LEVEL=INFO ENABLE_TOOL_SEARCH=false ENABLE_YAML_CONFIG_EDITING=false DISABLED_TOOLS= PINNED_TOOLS= MCP_SERVER_NAME=homeassistant-mcp MCP_TRANSPORT=http MCP_HOST=0.0.0.0 EOFHA cat < $HSHQ_STACKS_DIR/homeassistant/configurator/settings.conf { "PORT": 443, "BASEPATH": "/hass-config", "HASS_API": "", "USERNAME": "$HOMEASSISTANT_CONFIGURATOR_USER", "PASSWORD": "{sha256}$(echo -n $HOMEASSISTANT_CONFIGURATOR_USER_PASSWORD | openssl dgst -sha256 | cut -d"=" -f2 | xargs)", "SSL_CERTIFICATE": "/certs/homeassistant-configurator.crt", "SSL_KEY": "/certs/homeassistant-configurator.key" } EOFCF cat < $HOME/noderedsettings.conf module.exports = { flowFile: 'flows.json', flowFilePretty: true, credentialSecret: "$(pwgen -c -n 32 1)", https: { key: require("fs").readFileSync('/data/homeassistant-nodered.key'), cert: require("fs").readFileSync('/data/homeassistant-nodered.crt') }, requireHttps: true, uiPort: 443, logging: { console: { level: "info", metrics: false, audit: false } }, exportGlobalContextKeys: false, externalModules: { }, editorTheme: { theme: "dark", tours: false }, functionExternalModules: true, functionGlobalContext: { }, debugMaxLength: 1000, mqttReconnectTime: 15000, serialReconnectTime: 15000, } EOFCF outputHASSPanels outputHASSConfigYaml } function outputComposeHomeAssistant() { rm -f $HOME/homeassistant-compose.yml cat < $HOME/homeassistant-compose.yml $STACK_VERSION_PREFIX homeassistant $(getScriptStackVersion homeassistant) services: homeassistant-db: image: $(getScriptImageByContainerName homeassistant-db) container_name: homeassistant-db hostname: homeassistant-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - dock-privateip-net - dock-dbs-net ports: - "$HOMEASSISTANT_DB_LOCALHOST_PORT:5432" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/dbexport:/dbexport environment: - POSTGRES_DB=$HOMEASSISTANT_DATABASE_NAME - POSTGRES_USER=$HOMEASSISTANT_DATABASE_USER - POSTGRES_PASSWORD=$HOMEASSISTANT_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.homeassistant-hourly-db.schedule=@every 1h" - "ofelia.job-exec.homeassistant-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.homeassistant-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.homeassistant-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.homeassistant-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.homeassistant-hourly-db.email-from=Home Assistant Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.homeassistant-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.homeassistant-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.homeassistant-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.homeassistant-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.homeassistant-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.homeassistant-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.homeassistant-monthly-db.email-from=Home Assistant Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.homeassistant-monthly-db.mail-only-on-error=false" homeassistant-app: image: $(getScriptImageByContainerName homeassistant-app) container_name: homeassistant-app hostname: homeassistant-app restart: unless-stopped env_file: stack.env privileged: true depends_on: - homeassistant-db network_mode: host volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-app.crt:/certs/homeassistant-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-app.key:/certs/homeassistant-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/media:/media homeassistant-nodered: image: $(getScriptImageByContainerName homeassistant-nodered) container_name: homeassistant-nodered hostname: homeassistant-nodered restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - homeassistant-app networks: - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/nodered:/data - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-nodered.crt:/data/homeassistant-nodered.crt - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-nodered.key:/data/homeassistant-nodered.key homeassistant-configurator: image: $(getScriptImageByContainerName homeassistant-configurator) container_name: homeassistant-configurator hostname: homeassistant-configurator restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - homeassistant-app extra_hosts: - host.docker.internal:host-gateway networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-configurator.crt:/certs/homeassistant-configurator.crt - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-configurator.key:/certs/homeassistant-configurator.key - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/configurator:/config - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/config:/hass-config homeassistant-tasmoadmin: image: $(getScriptImageByContainerName homeassistant-tasmoadmin) container_name: homeassistant-tasmoadmin hostname: homeassistant-tasmoadmin restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - homeassistant-app networks: - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/tasmoadmin:/data homeassistant-mcp: image: $(getScriptImageByContainerName homeassistant-mcp) container_name: homeassistant-mcp hostname: homeassistant-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: ha-mcp-web extra_hosts: - host.docker.internal:host-gateway networks: - dock-privateip-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/python3.13/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/mcp:/home/mcpuser/.ha-mcp networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true EOFHA } function outputHASSConfigYaml() { cat < $HSHQ_STACKS_DIR/homeassistant/configuration.yaml default_config: homeassistant: time_zone: $TZ external_url: "https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN" internal_url: "https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN" country: US frontend: themes: !include_dir_merge_named themes tts: - platform: picotts language: 'en-US' notify: - name: "LOCAL_SMTP" platform: smtp server: "$SUB_POSTFIX.$HOMESERVER_DOMAIN" port: 25 timeout: 15 sender: "$EMAIL_SMTP_EMAIL_ADDRESS" encryption: starttls verify_ssl: false recipient: - "$EMAIL_ADMIN_EMAIL_ADDRESS" sender_name: "HomeAssistant $(getAdminEmailName)" http: use_x_forwarded_for: true trusted_proxies: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 ip_ban_enabled: true login_attempts_threshold: 5 server_port: $HOMEASSISTANT_LOCALHOST_PORT ssl_certificate: /certs/homeassistant-app.crt ssl_key: /certs/homeassistant-app.key panel_custom: - name: nodered sidebar_title: "Node Red" sidebar_icon: mdi:file-tree url_path: nodered module_url: /local/panels/nodered_redirect.js embed_iframe: true require_admin: true - name: Configurator sidebar_title: "Configurator" sidebar_icon: mdi:wrench url_path: configurator module_url: /local/panels/configurator_redirect.js embed_iframe: true require_admin: true - name: tasmoadmin sidebar_title: "TasmoAdmin" sidebar_icon: mdi:home-automation url_path: tasmoadmin module_url: /local/panels/tasmoadmin_redirect.js embed_iframe: true require_admin: true recorder: db_url: postgresql://$HOMEASSISTANT_DATABASE_USER:$HOMEASSISTANT_DATABASE_USER_PASSWORD@localhost:$HOMEASSISTANT_DB_LOCALHOST_PORT/$HOMEASSISTANT_DATABASE_NAME influxdb: api_version: 2 ssl: true host: $SUB_INFLUXDB.$HOMESERVER_DOMAIN port: 443 verify_ssl: true ssl_ca_cert: /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt token: "$INFLUXDB_TOKEN" organization: "$INFLUXDB_ORG" bucket: "$INFLUXDB_HA_BUCKET" EOFHC } function outputHASSPanels() { mkdir -p $HSHQ_STACKS_DIR/homeassistant/config/www/panels echo "window.location.replace(\"https://$SUB_HOMEASSISTANT_CONFIGURATOR.$HOMESERVER_DOMAIN/\");" > $HSHQ_STACKS_DIR/homeassistant/config/www/panels/configurator_redirect.js echo "window.location.replace(\"https://$SUB_HOMEASSISTANT_NODERED.$HOMESERVER_DOMAIN/\");" > $HSHQ_STACKS_DIR/homeassistant/config/www/panels/nodered_redirect.js echo "window.location.replace(\"https://$SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN/\");" > $HSHQ_STACKS_DIR/homeassistant/config/www/panels/tasmoadmin_redirect.js } function performUpdateHomeAssistant() { perform_stack_name=homeassistant prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2023.8,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2023.8,homeassistant/home-assistant:2024.3.1" image_update_map[2]="nodered/node-red:3.0.2,nodered/node-red:3.1.7" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v3.1.0,ghcr.io/tasmoadmin/tasmoadmin:v4.0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfV4UpdateHASS false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v5 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.1.3,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2024.1.3,homeassistant/home-assistant:2024.3.1" image_update_map[2]="nodered/node-red:3.0.2,nodered/node-red:3.1.7" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v3.1.0,ghcr.io/tasmoadmin/tasmoadmin:v4.0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfV4UpdateHASS false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v5 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.1.5,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2024.1.5,homeassistant/home-assistant:2024.3.1" image_update_map[2]="nodered/node-red:3.0.2,nodered/node-red:3.1.7" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v3.1.0,ghcr.io/tasmoadmin/tasmoadmin:v4.0.1" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.1.6,nodered/node-red:3.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v3.1.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2024.1.6,homeassistant/home-assistant:2024.3.1" image_update_map[2]="nodered/node-red:3.0.2,nodered/node-red:3.1.7" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v3.1.0,ghcr.io/tasmoadmin/tasmoadmin:v4.0.1" ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.3.1,nodered/node-red:3.1.7,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v4.0.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2024.3.1,homeassistant/home-assistant:2024.7.4" image_update_map[2]="nodered/node-red:3.1.7,nodered/node-red:4.0.2" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v4.0.1,ghcr.io/tasmoadmin/tasmoadmin:v4.0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfUpdateHASSiFramesConfig false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.7.4,nodered/node-red:4.0.2,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v4.0.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2024.7.4,homeassistant/home-assistant:2024.10.4" image_update_map[2]="nodered/node-red:4.0.2,nodered/node-red:4.0.5" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v4.0.1,ghcr.io/tasmoadmin/tasmoadmin:v4.1.3" ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2024.10.4,nodered/node-red:4.0.5,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v4.1.3 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2024.10.4,homeassistant/home-assistant:2025.4.0" image_update_map[2]="nodered/node-red:4.0.5,nodered/node-red:4.0.9-22" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v4.1.3,ghcr.io/tasmoadmin/tasmoadmin:v4.1.3" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfHomeAssistantUpdatePythonCertPath perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 8) newVer=v10 curImageList=postgres:15.0-bullseye,homeassistant/home-assistant:2025.4.0,nodered/node-red:4.0.9-22,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v4.1.3 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="homeassistant/home-assistant:2025.4.0,mirror.gcr.io/homeassistant/home-assistant:2025.12.0" image_update_map[2]="nodered/node-red:4.0.9-22,mirror.gcr.io/nodered/node-red:4.1.2-22" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v4.1.3,ghcr.io/tasmoadmin/tasmoadmin:v4.3.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfHomeAssistantV10Update false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/homeassistant/home-assistant:2025.8.1,mirror.gcr.io/nodered/node-red:4.1.0-22,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v4.3.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/homeassistant/home-assistant:2025.8.1,mirror.gcr.io/homeassistant/home-assistant:2025.12.0" image_update_map[2]="mirror.gcr.io/nodered/node-red:4.1.0-22,mirror.gcr.io/nodered/node-red:4.1.2-22" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,causticlab/hass-configurator-docker:0.5.2" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v4.3.1,ghcr.io/tasmoadmin/tasmoadmin:v4.3.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfHomeAssistantV10Update false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 10) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/homeassistant/home-assistant:2025.12.0,mirror.gcr.io/nodered/node-red:4.1.2-22,causticlab/hass-configurator-docker:0.5.2,ghcr.io/tasmoadmin/tasmoadmin:v4.3.2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/homeassistant/home-assistant:2025.12.0,mirror.gcr.io/homeassistant/home-assistant:2026.7.2" image_update_map[2]="mirror.gcr.io/nodered/node-red:4.1.2-22,mirror.gcr.io/nodered/node-red:5.0.1-24" image_update_map[3]="causticlab/hass-configurator-docker:0.5.2,mirror.gcr.io/causticlab/hass-configurator-docker:0.6.0" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v4.3.2,ghcr.io/tasmoadmin/tasmoadmin:v5.0.0" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfHomeAssistantV11Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 11) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/homeassistant/home-assistant:2026.7.2,mirror.gcr.io/nodered/node-red:5.0.1-24,mirror.gcr.io/causticlab/hass-configurator-docker:0.6.0,ghcr.io/tasmoadmin/tasmoadmin:v5.0.0,ghcr.io/homeassistant-ai/ha-mcp:7.12.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/homeassistant/home-assistant:2026.7.2,mirror.gcr.io/homeassistant/home-assistant:2026.7.2" image_update_map[2]="mirror.gcr.io/nodered/node-red:5.0.1-24,mirror.gcr.io/nodered/node-red:5.0.1-24" image_update_map[3]="mirror.gcr.io/causticlab/hass-configurator-docker:0.6.0,mirror.gcr.io/causticlab/hass-configurator-docker:0.6.0" image_update_map[4]="ghcr.io/tasmoadmin/tasmoadmin:v5.0.0,ghcr.io/tasmoadmin/tasmoadmin:v5.0.0" image_update_map[5]="ghcr.io/homeassistant-ai/ha-mcp:7.12.1,ghcr.io/homeassistant-ai/ha-mcp:7.12.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfV4UpdateHASS() { # Replace the config and compose files... # Typically, we would be more surgical with the changes, # but in this case, the amount of users affected is minimal. outputHASSConfigYaml outputComposeHomeAssistant chmod 600 $HOME/homeassistant-compose.yml sudo chown root:root $HOME/homeassistant-compose.yml sudo mv $HOME/homeassistant-compose.yml $upgrade_compose_file insertEnableSvcUptimeKuma homeassistant "$FMLNAME_HOMEASSISTANT_TASMOADMIN" $USERTYPE_HOMEASSISTANT_TASMOADMIN "https://$SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN" true } function mfUpdateHASSiFramesConfig() { outputHASSPanels if [ -f $HSHQ_STACKS_DIR/homeassistant/config/configuration.yaml ]; then sudo mv $HSHQ_STACKS_DIR/homeassistant/config/configuration.yaml $HSHQ_STACKS_DIR/homeassistant/config/configuration.old fi outputHASSConfigYaml sudo mv $HSHQ_STACKS_DIR/homeassistant/configuration.yaml $HSHQ_STACKS_DIR/homeassistant/config/configuration.yaml } function mfHomeAssistantUpdatePythonCertPath() { # This will address the python version cert path correctly # by moving the version to an environment variable. # Much easier to update in the future. sed -i "s|\/etc\/ssl\/certs\/ca-certificates.crt:\/usr\/local\/lib\/python.*|\/etc\/ssl\/certs\/ca-certificates.crt:\/usr\/local\/lib\/\${PYTHON_VER}\/site-packages\/certifi\/cacert.pem:ro|g" $HOME/homeassistant-compose.yml grep "PYTHON_VER=" $HOME/homeassistant.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "PYTHON_VER=python3.13" >> $HOME/homeassistant.env fi } function mfHomeAssistantV10Update() { sed -i "/server_host/d" $HSHQ_STACKS_DIR/homeassistant/config/configuration.yaml } function mfHomeAssistantV11Update() { mkdir -p $HSHQ_STACKS_DIR/homeassistant/mcp rm -f $HOME/homeassistant-compose.yml cat < $HOME/homeassistant-compose.yml $STACK_VERSION_PREFIX homeassistant v11 services: homeassistant-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: homeassistant-db hostname: homeassistant-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - dock-privateip-net - dock-dbs-net ports: - "$HOMEASSISTANT_DB_LOCALHOST_PORT:5432" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/dbexport:/dbexport environment: - POSTGRES_DB=$HOMEASSISTANT_DATABASE_NAME - POSTGRES_USER=$HOMEASSISTANT_DATABASE_USER - POSTGRES_PASSWORD=$HOMEASSISTANT_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.homeassistant-hourly-db.schedule=@every 1h" - "ofelia.job-exec.homeassistant-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.homeassistant-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.homeassistant-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.homeassistant-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.homeassistant-hourly-db.email-from=Home Assistant Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.homeassistant-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.homeassistant-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.homeassistant-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.homeassistant-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.homeassistant-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.homeassistant-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.homeassistant-monthly-db.email-from=Home Assistant Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.homeassistant-monthly-db.mail-only-on-error=false" homeassistant-app: image: mirror.gcr.io/homeassistant/home-assistant:2026.7.2 container_name: homeassistant-app hostname: homeassistant-app restart: unless-stopped env_file: stack.env privileged: true depends_on: - homeassistant-db network_mode: host volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-app.crt:/certs/homeassistant-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-app.key:/certs/homeassistant-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/media:/media homeassistant-nodered: image: mirror.gcr.io/nodered/node-red:5.0.1-24 container_name: homeassistant-nodered hostname: homeassistant-nodered restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - homeassistant-app networks: - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/nodered:/data - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-nodered.crt:/data/homeassistant-nodered.crt - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-nodered.key:/data/homeassistant-nodered.key homeassistant-configurator: image: mirror.gcr.io/causticlab/hass-configurator-docker:0.6.0 container_name: homeassistant-configurator hostname: homeassistant-configurator restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - homeassistant-app extra_hosts: - host.docker.internal:host-gateway networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-configurator.crt:/certs/homeassistant-configurator.crt - \${PORTAINER_HSHQ_SSL_DIR}/homeassistant-configurator.key:/certs/homeassistant-configurator.key - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/configurator:/config - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/config:/hass-config homeassistant-tasmoadmin: image: ghcr.io/tasmoadmin/tasmoadmin:v5.0.0 container_name: homeassistant-tasmoadmin hostname: homeassistant-tasmoadmin restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - homeassistant-app networks: - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/tasmoadmin:/data homeassistant-mcp: image: ghcr.io/homeassistant-ai/ha-mcp:7.12.1 container_name: homeassistant-mcp hostname: homeassistant-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: ha-mcp-web extra_hosts: - host.docker.internal:host-gateway networks: - dock-privateip-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/python3.13/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homeassistant/mcp:/home/mcpuser/.ha-mcp networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true EOFHA cat < $HOME/homeassistant.env UID=$USERID GID=$GROUPID PYTHON_VER=python3.14 HOMEASSISTANT_URL=https://host.docker.internal:$HOMEASSISTANT_LOCALHOST_PORT HOMEASSISTANT_TOKEN=your_long_lived_access_token_here HOMEASSISTANT_VERIFY_SSL=false SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt HA_VERIFY_SSL=false HA_TIMEOUT=30 HA_MAX_RETRIES=3 FUZZY_THRESHOLD=60 ENTITY_SEARCH_LIMIT=20 ENABLE_WEBSOCKET=true DEBUG=false LOG_LEVEL=INFO ENABLE_TOOL_SEARCH=false ENABLE_YAML_CONFIG_EDITING=false DISABLED_TOOLS= PINNED_TOOLS= MCP_SERVER_NAME=homeassistant-mcp MCP_TRANSPORT=http MCP_HOST=0.0.0.0 EOFHA inner_block="" inner_block=$inner_block">>https://$SUB_HOMEASSISTANT_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com; img-src 'self' cdnjs.cloudflare.com *.githubusercontent.com *.$HOMESERVER_DOMAIN data:; frame-src 'self' *.$HOMESERVER_DOMAIN data: blob:; connect-src 'self' catalogue.nodered.org api.github.com cdnjs.cloudflare.com *.$HOMESERVER_DOMAIN wss://*.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests; font-src 'self' cdnjs.cloudflare.com data:;\"\n" inner_block=$inner_block"}>>>>\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://host.docker.internal:$HOMEASSISTANT_LOCALHOST_PORT {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMEASSISTANT_APP $MANAGETLS_HOMEASSISTANT_APP "$is_integrate_hshq" $NETDEFAULT_HOMEASSISTANT_APP "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_HOMEASSISTANT_CONFIGURATOR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com; img-src 'self' cdnjs.cloudflare.com *.githubusercontent.com *.$HOMESERVER_DOMAIN data:; frame-src 'self' *.$HOMESERVER_DOMAIN data: blob:; connect-src 'self' catalogue.nodered.org api.github.com cdnjs.cloudflare.com *.$HOMESERVER_DOMAIN wss://*.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests; font-src 'self' cdnjs.cloudflare.com data:;\"\n" inner_block=$inner_block"}>>>>\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://homeassistant-configurator {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMEASSISTANT_CONFIGURATOR $MANAGETLS_HOMEASSISTANT_CONFIGURATOR "$is_integrate_hshq" $NETDEFAULT_HOMEASSISTANT_CONFIGURATOR "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_HOMEASSISTANT_TASMOADMIN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com; img-src 'self' cdnjs.cloudflare.com *.githubusercontent.com *.$HOMESERVER_DOMAIN data:; frame-src 'self' *.$HOMESERVER_DOMAIN data: blob:; connect-src 'self' catalogue.nodered.org api.github.com cdnjs.cloudflare.com *.$HOMESERVER_DOMAIN wss://*.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests; font-src 'self' cdnjs.cloudflare.com data:;\"\n" inner_block=$inner_block"}>>>>\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://homeassistant-tasmoadmin {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMEASSISTANT_TASMOADMIN $MANAGETLS_HOMEASSISTANT_TASMOADMIN "$is_integrate_hshq" $NETDEFAULT_HOMEASSISTANT_TASMOADMIN "$inner_block" } # Gitlab function installGitlab() { set +e is_integrate_hshq=$1 # Remember to check if exists checkDeleteStackAndDirectory gitlab "Gitlab" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName gitlab-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName gitlab-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName gitlab-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/gitlab mkdir $HSHQ_STACKS_DIR/gitlab/app mkdir $HSHQ_STACKS_DIR/gitlab/app/config mkdir $HSHQ_STACKS_DIR/gitlab/app/data mkdir $HSHQ_STACKS_DIR/gitlab/app/logs mkdir $HSHQ_STACKS_DIR/gitlab/db mkdir $HSHQ_STACKS_DIR/gitlab/dbexport chmod 777 $HSHQ_STACKS_DIR/gitlab/dbexport initServicesCredentials if [ -z "$GITLAB_ROOT_PASSWORD" ]; then GITLAB_ROOT_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GITLAB_ROOT_PASSWORD $GITLAB_ROOT_PASSWORD fi if [ -z "$GITLAB_REDIS_PASSWORD" ]; then GITLAB_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar GITLAB_REDIS_PASSWORD $GITLAB_REDIS_PASSWORD fi outputConfigGitlab generateCert gitlab-app gitlab-app installStack gitlab gitlab-app "" $HOME/gitlab.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 1 startStopStack gitlab stop sudo rm -fr $HSHQ_STACKS_DIR/gitlab sudo rm -fr $HSHQ_NONBACKUP_DIR/gitlab mkdir $HSHQ_STACKS_DIR/gitlab mkdir $HSHQ_STACKS_DIR/gitlab/app mkdir $HSHQ_STACKS_DIR/gitlab/app/config mkdir $HSHQ_STACKS_DIR/gitlab/app/data mkdir $HSHQ_STACKS_DIR/gitlab/app/logs mkdir $HSHQ_STACKS_DIR/gitlab/db mkdir $HSHQ_STACKS_DIR/gitlab/dbexport chmod 777 $HSHQ_STACKS_DIR/gitlab/dbexport mkdir $HSHQ_NONBACKUP_DIR/gitlab mkdir $HSHQ_NONBACKUP_DIR/gitlab/redis mv $HOME/gitlab.rb $HSHQ_STACKS_DIR/gitlab/app/config/gitlab.rb mv $HOME/gitlab-postconfigure.sh $HSHQ_STACKS_DIR/gitlab/app/config/gitlab-postconfigure.sh chmod +x $HSHQ_STACKS_DIR/gitlab/app/config/gitlab-postconfigure.sh startStopStack gitlab start echo -e "\n\n========================================================================" echo -e " WARNING: User telemetry and event tracking has been forcebly enabled" echo -e " by Gitlab! To disable it, go to Admin -> Settings -> Metrics and" echo -e " profiling -> Event tracking. For more details, see this link:" echo -e " https://docs.gitlab.com/17.11/administration/settings/event_data/" echo -e "" echo -e " Go to this link in your Gitlab instance to disable it:" echo -e " https://$SUB_GITLAB.$HOMESERVER_DOMAIN/admin/application_settings/metrics_and_profiling#js-product-usage-data-settings" echo -e "========================================================================\n" sleep 3 addReadOnlyUserToDatabase Gitlab postgres gitlab-db $GITLAB_DATABASE_NAME $GITLAB_DATABASE_USER $GITLAB_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/gitlab/dbexport $GITLAB_DATABASE_READONLYUSER $GITLAB_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_GITLAB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://gitlab-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_GITLAB $MANAGETLS_GITLAB "$is_integrate_hshq" $NETDEFAULT_GITLAB "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll gitlab "$FMLNAME_GITLAB" $USERTYPE_GITLAB "https://$SUB_GITLAB.$HOMESERVER_DOMAIN" "gitlab.png" "$(getHeimdallOrderFromSub $SUB_GITLAB $USERTYPE_GITLAB)" restartAllCaddyContainers fi } function outputConfigGitlab() { cat < $HOME/gitlab-compose.yml $STACK_VERSION_PREFIX gitlab $(getScriptStackVersion gitlab) services: gitlab-db: image: $(getScriptImageByContainerName gitlab-db) container_name: gitlab-db hostname: gitlab-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb depends_on: - gitlab-redis networks: - int-gitlab-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitlab/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitlab/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.gitlab-hourly-db.schedule=@every 1h" - "ofelia.job-exec.gitlab-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.gitlab-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.gitlab-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.gitlab-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.gitlab-hourly-db.email-from=Gitlab Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.gitlab-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.gitlab-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.gitlab-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.gitlab-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.gitlab-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.gitlab-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.gitlab-monthly-db.email-from=Gitlab Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.gitlab-monthly-db.mail-only-on-error=false" gitlab-app: image: $(getScriptImageByContainerName gitlab-app) container_name: gitlab-app hostname: gitlab-app restart: unless-stopped security_opt: - no-new-privileges:true networks: - int-gitlab-net - dock-ext-net - dock-ldap-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/gitlab-app.crt:/certs/gitlab-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/gitlab-app.key:/certs/gitlab-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/gitlab/app/config:/etc/gitlab - \${PORTAINER_HSHQ_STACKS_DIR}/gitlab/app/logs:/var/log/gitlab - \${PORTAINER_HSHQ_STACKS_DIR}/gitlab/app/data:/var/opt/gitlab environment: - GITLAB_ROOT_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS - GITLAB_POST_RECONFIGURE_SCRIPT=/etc/gitlab/gitlab-postconfigure.sh gitlab-redis: image: $(getScriptImageByContainerName gitlab-redis) container_name: gitlab-redis hostname: gitlab-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $GITLAB_REDIS_PASSWORD --appendonly no networks: - int-gitlab-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true int-gitlab-net: driver: bridge internal: true ipam: driver: default EOFGL cat < $HOME/gitlab.env UID=$USERID GID=$GROUPID POSTGRES_DB=$GITLAB_DATABASE_NAME POSTGRES_USER=$GITLAB_DATABASE_USER POSTGRES_PASSWORD=$GITLAB_DATABASE_USER_PASSWORD EOFGL cat < $HOME/gitlab.rb external_url 'https://$SUB_GITLAB.$HOMESERVER_DOMAIN' gitlab_rails['smtp_enable'] = true gitlab_rails['smtp_address'] = '$SMTP_HOSTNAME' gitlab_rails['smtp_port'] = $SMTP_HOSTPORT gitlab_rails['smtp_user_name'] = '$EMAIL_SMTP_EMAIL_ADDRESS' gitlab_rails['smtp_password'] = '$EMAIL_SMTP_PASSWORD' gitlab_rails['smtp_domain'] = '$SMTP_HOSTNAME' gitlab_rails['smtp_authentication'] = 'login' gitlab_rails['smtp_enable_starttls_auto'] = true gitlab_rails['gitlab_email_enabled'] = true gitlab_rails['gitlab_email_from'] = '$EMAIL_SMTP_EMAIL_ADDRESS' gitlab_rails['gitlab_email_display_name'] = 'Gitlab $(getAdminEmailName)' gitlab_rails['gitlab_email_reply_to'] = '$EMAIL_ADMIN_EMAIL_ADDRESS' gitlab_rails['incoming_email_enabled'] = false alertmanager['admin_email'] = '$EMAIL_ADMIN_EMAIL_ADDRESS' gitlab_rails['gitlab_default_theme'] = 11 gitlab_rails['trusted_proxies'] = [ '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16' ] gitlab_rails['terraform_state_enabled'] = false gitlab_rails['usage_ping_enabled'] = false gitlab_rails['ldap_enabled'] = true gitlab_rails['ldap_servers'] = YAML.load <<-'EOS' main: label: 'LDAP' host: 'ldapserver' port: 389 uid: 'uid' bind_dn: '$LDAP_READONLY_USER_BIND_DN' password: '$LDAP_READONLY_USER_PASSWORD' encryption: 'start_tls' verify_certificates: true base: '$LDAP_BASE_DN' group_base: 'ou=groups,$LDAP_BASE_DN' admin_group: '$LDAP_ADMIN_USER_GROUP_NAME' lowercase_usernames: true tls_options: ca_file: /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt EOS gitlab_rails['initial_root_password'] = '$GITLAB_ROOT_PASSWORD' gitlab_rails['store_initial_root_password'] = false gitlab_rails['db_adapter'] = 'postgresql' gitlab_rails['db_database'] = '$GITLAB_DATABASE_NAME' gitlab_rails['db_username'] = '$GITLAB_DATABASE_USER' gitlab_rails['db_password'] = '$GITLAB_DATABASE_USER_PASSWORD' gitlab_rails['db_host'] = 'gitlab-db' gitlab_rails['db_port'] = 5432 gitlab_rails['redis_host'] = 'gitlab-redis' gitlab_rails['redis_password'] = '$GITLAB_REDIS_PASSWORD' postgresql['shared_buffers'] = '256MB' postgresql['enable'] = false redis['enable'] = false puma['worker_processes'] = 2 puma['max_threads'] = 4 sidekiq['concurrency'] = 2 sidekiq['max_concurrency'] = 5 prometheus_monitoring['enable'] = false nginx['redirect_http_to_https'] = true nginx['ssl_certificate'] = '/certs/gitlab-app.crt' nginx['ssl_certificate_key'] = '/certs/gitlab-app.key' nginx['real_ip_trusted_addresses'] = [ '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16' ] nginx['real_ip_header'] = 'X-Forwarded-For' nginx['real_ip_recursive'] = 'on' EOFGL cat < $HOME/gitlab-postconfigure.sh #!/bin/bash gitlab-rails runner 'ApplicationSetting.last.update(signup_enabled: false)' gitlab-rails runner 'ApplicationSetting.last.update(password_authentication_enabled_for_web: false)' echo "Gitlab Postconfigure Scirpt Completed" EOFGL } function performUpdateGitlab() { perform_stack_name=gitlab prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.2.4-ce.0,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitlab/gitlab-ce:16.2.4-ce.0,gitlab/gitlab-ce:16.8.4-ce.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 2) newVer=v4 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.7.0-ce.0,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitlab/gitlab-ce:16.7.0-ce.0,gitlab/gitlab-ce:16.8.4-ce.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.8.1-ce.0,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitlab/gitlab-ce:16.8.1-ce.0,gitlab/gitlab-ce:16.8.4-ce.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.8.4-ce.0,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitlab/gitlab-ce:16.8.4-ce.0,gitlab/gitlab-ce:16.10.10-ce.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:16.10.10-ce.0,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitlab/gitlab-ce:16.10.10-ce.0,gitlab/gitlab-ce:17.10.3-ce.0" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,gitlab/gitlab-ce:17.10.3-ce.0,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="gitlab/gitlab-ce:17.10.3-ce.0,mirror.gcr.io/gitlab/gitlab-ce:17.11.6-ce.0" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfGitlabFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" echo -e "\n\n========================================================================" echo -e " WARNING: User telemetry and event tracking has been forcebly enabled" echo -e " by Gitlab! To disable it, go to Admin -> Settings -> Metrics and" echo -e " profiling -> Event tracking. For more details, see this link:" echo -e " https://docs.gitlab.com/17.11/administration/settings/event_data/" echo -e "" echo -e " Go to this link in your Gitlab instance to disable it:" echo -e " https://$SUB_GITLAB.$HOMESERVER_DOMAIN/admin/application_settings/metrics_and_profiling#js-product-usage-data-settings" echo -e "========================================================================\n" return ;; 7) newVer=v8 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gitlab/gitlab-ce:17.11.6-ce.0,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/gitlab/gitlab-ce:17.11.6-ce.0,mirror.gcr.io/gitlab/gitlab-ce:18.2.1-ce.0" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfGitlabUpgradeDB false perform_update_report="${perform_update_report}$stack_upgrade_report" echo -e "\n\n========================================================================" echo -e " WARNING: User telemetry and event tracking has been forcebly enabled" echo -e " by Gitlab! To disable it, go to Admin -> Settings -> Metrics and" echo -e " profiling -> Event tracking. For more details, see this link:" echo -e " https://docs.gitlab.com/17.11/administration/settings/event_data/" echo -e "" echo -e " Go to this link in your Gitlab instance to disable it:" echo -e " https://$SUB_GITLAB.$HOMESERVER_DOMAIN/admin/application_settings/metrics_and_profiling#js-product-usage-data-settings" echo -e "========================================================================\n" return ;; 8) newVer=v8 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/gitlab/gitlab-ce:18.2.1-ce.0,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/gitlab/gitlab-ce:18.2.1-ce.0,mirror.gcr.io/gitlab/gitlab-ce:18.2.1-ce.0" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfGitlabFixRedisCompose() { replaceRedisBlock gitlab gitlab-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/gitlab/redis/* } function mfGitlabUpgradeDB() { isStartStackFromStopped=true upgradeDatabaseInStack gitlab postgres $GITLAB_DATABASE_NAME $GITLAB_DATABASE_USER $GITLAB_DATABASE_USER_PASSWORD na $HSHQ_STACKS_DIR/gitlab/db $HSHQ_STACKS_DIR/gitlab/dbexport /var/lib/postgresql/data mirror.gcr.io/postgres:15.0-bullseye mirror.gcr.io/postgres:16.9-bookworm } # Vaultwarden function installVaultwarden() { set +e is_integrate_hshq=$1 # Same as before, check if directory exists checkDeleteStackAndDirectory vaultwarden "Vaultwarden" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName vaultwarden-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName vaultwarden-ldap) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName vaultwarden-db) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/vaultwarden mkdir $HSHQ_STACKS_DIR/vaultwarden/app mkdir $HSHQ_STACKS_DIR/vaultwarden/db mkdir $HSHQ_STACKS_DIR/vaultwarden/dbexport chmod 777 $HSHQ_STACKS_DIR/vaultwarden/dbexport initServicesCredentials outputConfigVaultwarden generateCert vaultwarden-app vaultwarden-app installStack vaultwarden vaultwarden-app "Rocket has launched" $HOME/vaultwarden.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 echo "Configuring vaultwarden..." docker container restart vaultwarden-ldap > /dev/null 2>&1 addReadOnlyUserToDatabase Vaultwarden postgres vaultwarden-db $VAULTWARDEN_DATABASE_NAME $VAULTWARDEN_DATABASE_USER $VAULTWARDEN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/vaultwarden/dbexport $VAULTWARDEN_DATABASE_READONLYUSER $VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_VAULTWARDEN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>@insecureadmin {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_NOTHOMESUBNET\n" inner_block=$inner_block">>>>>>>>path /admin*\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>redir @insecureadmin /\n" inner_block=$inner_block">>>>>>reverse_proxy https://vaultwarden-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>header_up X-Real-IP {remote_host}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_VAULTWARDEN $MANAGETLS_VAULTWARDEN "$is_integrate_hshq" $NETDEFAULT_VAULTWARDEN "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll vaultwarden "$FMLNAME_VAULTWARDEN" $USERTYPE_VAULTWARDEN "https://$SUB_VAULTWARDEN.$HOMESERVER_DOMAIN" "vaultwarden.png" "$(getHeimdallOrderFromSub $SUB_VAULTWARDEN $USERTYPE_VAULTWARDEN)" insertEnableSvcHeimdall vaultwarden "$FMLNAME_VAULTWARDEN Admin" admin "https://$SUB_VAULTWARDEN.$HOMESERVER_DOMAIN/admin" "vaultwarden.png" true "$(getHeimdallOrderFromSub $SUB_VAULTWARDEN admin)" restartAllCaddyContainers fi emailVaultwardenCredentials } function outputConfigVaultwarden() { cat < $HOME/vaultwarden-compose.yml $STACK_VERSION_PREFIX vaultwarden $(getScriptStackVersion vaultwarden) services: vaultwarden-db: image: $(getScriptImageByContainerName vaultwarden-db) container_name: vaultwarden-db hostname: vaultwarden-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-vaultwarden-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/vaultwarden/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/vaultwarden/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.vaultwarden-hourly-db.schedule=@every 1h" - "ofelia.job-exec.vaultwarden-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.vaultwarden-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.vaultwarden-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.vaultwarden-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.vaultwarden-hourly-db.email-from=Vaultwarden Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.vaultwarden-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.vaultwarden-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.vaultwarden-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.vaultwarden-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.vaultwarden-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.vaultwarden-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.vaultwarden-monthly-db.email-from=Vaultwarden Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.vaultwarden-monthly-db.mail-only-on-error=false" vaultwarden-app: image: $(getScriptImageByContainerName vaultwarden-app) container_name: vaultwarden-app hostname: vaultwarden-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - vaultwarden-db networks: - int-vaultwarden-net - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/vaultwarden-app.crt:/certs/vaultwarden-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/vaultwarden-app.key:/certs/vaultwarden-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/vaultwarden/app:/data vaultwarden-ldap: image: $(getScriptImageByContainerName vaultwarden-ldap) container_name: vaultwarden-ldap hostname: vaultwarden-ldap restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - vaultwarden-app networks: - int-vaultwarden-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.der:/cacert/rootca.der:ro environment: - APP_VAULTWARDEN_ADMIN_TOKEN=$VAULTWARDEN_ADMIN_TOKEN - APP_LDAP_BIND_PASSWORD=$LDAP_READONLY_USER_PASSWORD networks: dock-proxy-net: name: dock-proxy external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true int-vaultwarden-net: driver: bridge internal: true ipam: driver: default EOFVW VAULTWARDEN_ADMIN_TOKEN_HASH=$(echo -n "$VAULTWARDEN_ADMIN_TOKEN" | argon2 $(pwgen -c -n 32 1) -id -e -m 16 | sed 's/\$/\$\$/g') cat < $HOME/vaultwarden.env UID=$USERID GID=$GROUPID ADMIN_TOKEN=$VAULTWARDEN_ADMIN_TOKEN_HASH DATABASE_URL=postgresql://$VAULTWARDEN_DATABASE_USER:$VAULTWARDEN_DATABASE_USER_PASSWORD@vaultwarden-db:5432/$VAULTWARDEN_DATABASE_NAME ROCKET_TLS={certs=/certs/vaultwarden-app.crt,key=/certs/vaultwarden-app.key} ROCKET_PORT=443 IP_HEADER=X-Forwarded-For SIGNUPS_ALLOWED=false INVITATION_EXPIRATION_HOURS=720 DOMAIN=https://$SUB_VAULTWARDEN.$HOMESERVER_DOMAIN SMTP_HOST=$SMTP_HOSTNAME SMTP_FROM_NAME=Vaultwarden $(getAdminEmailName) SMTP_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS SMTP_PORT=$SMTP_HOSTPORT SMTP_SECURITY=starttls POSTGRES_DB=$VAULTWARDEN_DATABASE_NAME POSTGRES_USER=$VAULTWARDEN_DATABASE_USER POSTGRES_PASSWORD=$VAULTWARDEN_DATABASE_USER_PASSWORD RUST_BACKTRACE=1 APP_VAULTWARDEN_URL=https://vaultwarden-app:443 APP_LDAP_HOST=ldapserver APP_LDAP_PORT=389 APP_LDAP_BIND_DN=$LDAP_READONLY_USER_BIND_DN APP_LDAP_SEARCH_BASE_DN=$LDAP_BASE_DN APP_LDAP_STARTTLS=true APP_LDAP_NO_TLS_VERIFY=true APP_VAULTWARDEN_ROOT_CERT_FILE=/cacert/rootca.der APP_LDAP_SEARCH_FILTER=(&(objectClass=person)(memberof=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)) APP_LDAP_SYNC_INTERVAL_SECONDS=300 EOFVW } function performUpdateVaultwarden() { perform_stack_name=vaultwarden prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.29.1-alpine,thegeeklab/vaultwarden-ldap:0.6.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="vaultwarden/server:1.29.1-alpine,vaultwarden/server:1.30.5-alpine" image_update_map[2]="thegeeklab/vaultwarden-ldap:0.6.2,vividboarder/vaultwarden_ldap:2.0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfReplaceVaultwardenLDAPConnector perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v5 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.30.1-alpine,thegeeklab/vaultwarden-ldap:0.6.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="vaultwarden/server:1.30.1-alpine,vaultwarden/server:1.30.5-alpine" image_update_map[2]="thegeeklab/vaultwarden-ldap:0.6.2,vividboarder/vaultwarden_ldap:2.0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfReplaceVaultwardenLDAPConnector perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v5 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.30.2-alpine,thegeeklab/vaultwarden-ldap:0.6.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="vaultwarden/server:1.30.2-alpine,vaultwarden/server:1.30.5-alpine" image_update_map[2]="thegeeklab/vaultwarden-ldap:0.6.2,vividboarder/vaultwarden_ldap:2.0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfReplaceVaultwardenLDAPConnector perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.30.5-alpine,thegeeklab/vaultwarden-ldap:0.6.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="vaultwarden/server:1.30.5-alpine,vaultwarden/server:1.30.5-alpine" image_update_map[2]="thegeeklab/vaultwarden-ldap:0.6.2,vividboarder/vaultwarden_ldap:2.0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfReplaceVaultwardenLDAPConnector perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v8 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.30.5-alpine,vividboarder/vaultwarden_ldap:2.0.1 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="vaultwarden/server:1.30.5-alpine,mirror.gcr.io/vaultwarden/server:1.34.3-alpine" image_update_map[2]="vividboarder/vaultwarden_ldap:2.0.1,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2" ;; 6) newVer=v8 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.32.3-alpine,vividboarder/vaultwarden_ldap:2.0.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="vaultwarden/server:1.32.3-alpine,mirror.gcr.io/vaultwarden/server:1.34.3-alpine" image_update_map[2]="vividboarder/vaultwarden_ldap:2.0.2,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2" ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,vaultwarden/server:1.33.2-alpine,vividboarder/vaultwarden_ldap:2.1.1 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="vaultwarden/server:1.33.2-alpine,mirror.gcr.io/vaultwarden/server:1.34.3-alpine" image_update_map[2]="vividboarder/vaultwarden_ldap:2.1.1,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2" ;; 8) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/vaultwarden/server:1.34.3-alpine,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/vaultwarden/server:1.34.3-alpine,mirror.gcr.io/vaultwarden/server:1.35.4-alpine" image_update_map[2]="mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2" ;; 9) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/vaultwarden/server:1.35.4-alpine,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/vaultwarden/server:1.35.4-alpine,mirror.gcr.io/vaultwarden/server:1.37.1-alpine" image_update_map[2]="mirror.gcr.io/vividboarder/vaultwarden_ldap:2.1.2,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1" ;; 10) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/vaultwarden/server:1.36.0-alpine,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/vaultwarden/server:1.36.0-alpine,mirror.gcr.io/vaultwarden/server:1.37.1-alpine" image_update_map[2]="mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1" ;; 11) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/vaultwarden/server:1.37.1-alpine,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/vaultwarden/server:1.37.1-alpine,mirror.gcr.io/vaultwarden/server:1.37.1-alpine" image_update_map[2]="mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1,mirror.gcr.io/vividboarder/vaultwarden_ldap:2.2.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfReplaceVaultwardenLDAPConnector() { outputConfigVaultwarden } # Discourse function installDiscourse() { set +e is_integrate_hshq=$1 # !Check if directory exists checkDeleteStackAndDirectory discourse "Discourse" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName discourse-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName discourse-sidekiq) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName discourse-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName discourse-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/discourse mkdir $HSHQ_STACKS_DIR/discourse/db mkdir $HSHQ_STACKS_DIR/discourse/dbexport mkdir $HSHQ_STACKS_DIR/discourse/app chmod 777 $HSHQ_STACKS_DIR/discourse/dbexport initServicesCredentials if [ -z "$DISCOURSE_DATABASE_USER_PASSWORD" ]; then DISCOURSE_DATABASE_USER_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DISCOURSE_DATABASE_USER_PASSWORD $DISCOURSE_DATABASE_USER_PASSWORD fi if [ -z "$DISCOURSE_REDIS_PASSWORD" ]; then DISCOURSE_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar DISCOURSE_REDIS_PASSWORD $DISCOURSE_REDIS_PASSWORD fi set +e addUserMailu alias $ADMIN_USERNAME_BASE"_discourse" $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS #generateCert discourse-app discourse-app outputConfigDiscourse installStack discourse discourse-app "" $HOME/discourse.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase Discourse postgres discourse-db $DISCOURSE_DATABASE_NAME $DISCOURSE_DATABASE_USER $DISCOURSE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/discourse/dbexport $DISCOURSE_DATABASE_READONLYUSER $DISCOURSE_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_DISCOURSE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://discourse-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DISCOURSE $MANAGETLS_DISCOURSE "$is_integrate_hshq" $NETDEFAULT_DISCOURSE "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll discourse "$FMLNAME_DISCOURSE" $USERTYPE_DISCOURSE "https://$SUB_DISCOURSE.$HOMESERVER_DOMAIN" "discourse.png" "$(getHeimdallOrderFromSub $SUB_DISCOURSE $USERTYPE_DISCOURSE)" restartAllCaddyContainers fi } function outputConfigDiscourse() { cat < $HOME/discourse-compose.yml $STACK_VERSION_PREFIX discourse $(getScriptStackVersion discourse) services: discourse-db: image: $(getScriptImageByContainerName discourse-db) container_name: discourse-db hostname: discourse-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-discourse-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/discourse/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/discourse/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.discourse-hourly-db.schedule=@every 1h" - "ofelia.job-exec.discourse-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.discourse-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.discourse-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.discourse-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.discourse-hourly-db.email-from=Discourse Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.discourse-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.discourse-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.discourse-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.discourse-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.discourse-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.discourse-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.discourse-monthly-db.email-from=Discourse Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.discourse-monthly-db.mail-only-on-error=false" discourse-app: image: $(getScriptImageByContainerName discourse-app) container_name: discourse-app hostname: discourse-app restart: unless-stopped env_file: stack.env depends_on: - discourse-db - discourse-redis networks: - int-discourse-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-discourse-data:/bitnami/discourse discourse-sidekiq: image: $(getScriptImageByContainerName discourse-sidekiq) container_name: discourse-sidekiq hostname: discourse-sidekiq restart: unless-stopped env_file: stack.env command: /opt/bitnami/scripts/discourse-sidekiq/run.sh depends_on: - discourse-db - discourse-redis networks: - int-discourse-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-discourse-data:/bitnami/discourse discourse-redis: image: $(getScriptImageByContainerName discourse-redis) container_name: discourse-redis hostname: discourse-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $DISCOURSE_REDIS_PASSWORD --appendonly no networks: - int-discourse-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-discourse-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/discourse/app networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true int-discourse-net: driver: bridge internal: true ipam: driver: default EOFDC cat < $HOME/discourse.env UID=$USERID GID=$GROUPID DISCOURSE_USERNAME=$DISCOURSE_ADMIN_USERNAME DISCOURSE_PASSWORD=$DISCOURSE_ADMIN_PASSWORD DISCOURSE_EMAIL=$DISCOURSE_ADMIN_EMAIL_ADDRESS DISCOURSE_FIRST_NAME=$(echo $HOMESERVER_ABBREV | tr '[:lower:]' '[:upper:]') DISCOURSE_LAST_NAME=Admin DISCOURSE_SITE_NAME=$HOMESERVER_NAME Discourse DISCOURSE_HOST=$SUB_DISCOURSE.$HOMESERVER_DOMAIN DISCOURSE_DATABASE_HOST=discourse-db DISCOURSE_DATABASE_PORT_NUMBER=5432 DISCOURSE_DATABASE_USER=$DISCOURSE_DATABASE_USER DISCOURSE_DATABASE_NAME=$DISCOURSE_DATABASE_NAME DISCOURSE_DATABASE_PASSWORD=$DISCOURSE_DATABASE_USER_PASSWORD DISCOURSE_REDIS_HOST=discourse-redis DISCOURSE_REDIS_PORT_NUMBER=6379 DISCOURSE_REDIS_PASSWORD=$DISCOURSE_REDIS_PASSWORD POSTGRESQL_CLIENT_DATABASE_HOST=discourse-db POSTGRESQL_CLIENT_DATABASE_PORT_NUMBER=5432 POSTGRESQL_CLIENT_CREATE_DATABASE_NAME=$DISCOURSE_DATABASE_NAME POSTGRESQL_CLIENT_POSTGRES_USER=$DISCOURSE_DATABASE_USER POSTGRESQL_CLIENT_POSTGRES_PASSWORD=$DISCOURSE_DATABASE_USER_PASSWORD POSTGRESQL_CLIENT_CREATE_DATABASE_EXTENSIONS=hstore,pg_trgm POSTGRES_DB=$DISCOURSE_DATABASE_NAME POSTGRES_USER=$DISCOURSE_DATABASE_USER POSTGRES_PASSWORD=$DISCOURSE_DATABASE_USER_PASSWORD DISCOURSE_SMTP_HOST=$SMTP_HOSTNAME DISCOURSE_SMTP_PORT=$SMTP_HOSTPORT DISCOURSE_SMTP_PROTOCOL=tls EOFDC } function performUpdateDiscourse() { perform_stack_name=discourse prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.0.6,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/discourse:3.0.6,bitnami/discourse:3.0.6" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Discourse cannot be upgraded to the next version. You must export your data from this instance, uninstall and reinstall Discourse, then import your data into the new instance." return ;; 2) newVer=v5 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.1.3,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/discourse:3.1.3,bitnami/discourse:3.2.5" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 3) # This is unstable as a fresh installation. It strangely works when upgrading from 3.1.3... newVer=v5 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.1.4,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/discourse:3.1.4,bitnami/discourse:3.2.5" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.2.1,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/discourse:3.2.1,bitnami/discourse:3.2.5" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 5) newVer=v7 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.2.5,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:17.5-bookworm" image_update_map[1]="bitnami/discourse:3.2.5,mirror.gcr.io/bitnami/discourse:3.4.7" image_update_map[2]="bitnami/redis:7.0.5,mirror.gcr.io/redis:8.2.0-bookworm" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Discourse cannot be upgraded to the next version. You must export your data from this instance, uninstall and reinstall Discourse, then import your data into the new instance." return ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,bitnami/discourse:3.3.2,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:17.5-bookworm" image_update_map[1]="bitnami/discourse:3.3.2,mirror.gcr.io/bitnami/discourse:3.4.7" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Discourse cannot be upgraded to the next version. You must export your data from this instance, uninstall and reinstall Discourse, then import your data into the new instance." return ;; 7) newVer=v7 curImageList=mirror.gcr.io/postgres:17.5-bookworm,mirror.gcr.io/bitnami/discourse:3.4.7,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:17.5-bookworm,mirror.gcr.io/postgres:17.5-bookworm" image_update_map[1]="mirror.gcr.io/bitnami/discourse:3.4.7,mirror.gcr.io/bitnami/discourse:3.4.7" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Syncthing function installSyncthing() { set +e is_integrate_hshq=$1 # !Check if directory exists checkDeleteStackAndDirectory syncthing "Syncthing" cdRes=$? if [ $cdRes -ne 0 ]; then echo "ERROR: Syncthing directory exists" exit 1 fi pullImage $(getScriptImageByContainerName syncthing) if [ $? -ne 0 ]; then echo "ERROR: Could not obtain Syncthing docker image" exit 1 fi set -e mkdir $HSHQ_STACKS_DIR/syncthing mkdir $HSHQ_STACKS_DIR/syncthing/config mkdir $HSHQ_STACKS_DIR/syncthing/data initServicesCredentials SYNCTHING_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $SYNCTHING_ADMIN_PASSWORD | tr -d ':\n' | sed 's/$2y/$2a/') SYNCTHING_API_KEY=$(pwgen -c -n 32 1) updateConfigVar SYNCTHING_API_KEY $SYNCTHING_API_KEY generateCert syncthing syncthing outputConfigSyncthing installStack syncthing syncthing "Access the GUI via the following URL" $HOME/syncthing.env retval=$? if [ $retval -ne 0 ]; then echo "ERROR: There was a problem installing Syncthing" exit $retval fi sleep 3 startStopStack syncthing stop alltext=$(sudo cat $HSHQ_STACKS_DIR/syncthing/config/config.xml) replacetext='\n
127.0.0.1:'$SYNCTHING_LOCAL_WEB_PORT'
\n '$SYNCTHING_ADMIN_USERNAME'\n '$SYNCTHING_ADMIN_PASSWORD_HASH'\n '$SYNCTHING_API_KEY'\n dark\n
' echo -e "${alltext%%}" > $HOME/st_config.xml alltext=$(cat $HOME/st_config.xml) replacetext='false' echo -e "${alltext%%}" > $HOME/st_config.xml alltext=$(cat $HOME/st_config.xml) replacetext='false' echo -e "${alltext%%}" > $HOME/st_config.xml alltext=$(cat $HOME/st_config.xml) replacetext='false' echo -e "${alltext%%}" > $HOME/st_config.xml alltext=$(cat $HOME/st_config.xml) replacetext='false' echo -e "${alltext%%}" > $HOME/st_config.xml sudo mv $HOME/st_config.xml $HSHQ_STACKS_DIR/syncthing/config/config.xml startStopStack syncthing start sleep 3 curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X DELETE -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/folders/default inner_block="" inner_block=$inner_block">>https://$SUB_SYNCTHING.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://syncthing:$SYNCTHING_LOCAL_WEB_PORT {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SYNCTHING $MANAGETLS_SYNCTHING "$is_integrate_hshq" $NETDEFAULT_SYNCTHING "$inner_block" } function outputConfigSyncthing() { cat < $HOME/syncthing-compose.yml $STACK_VERSION_PREFIX syncthing $(getScriptStackVersion syncthing) services: syncthing: image: $(getScriptImageByContainerName syncthing) container_name: syncthing hostname: syncthing restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - dock-privateip-net ports: - "$SYNCTHING_SYNC_PORT:$SYNCTHING_SYNC_PORT/tcp" - "$SYNCTHING_SYNC_PORT:$SYNCTHING_SYNC_PORT/udp" - "$SYNCTHING_DISC_PORT:$SYNCTHING_DISC_PORT/udp" - "127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT:$SYNCTHING_LOCAL_WEB_PORT" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/syncthing.crt:/var/syncthing/config/cert.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/syncthing.key:/var/syncthing/config/key.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/syncthing.crt:/var/syncthing/config/https-cert.pem:ro - \${PORTAINER_HSHQ_SSL_DIR}/syncthing.key:/var/syncthing/config/https-key.pem:ro - \${PORTAINER_HSHQ_STACKS_DIR}/syncthing/config:/var/syncthing/config - \${PORTAINER_HSHQ_STACKS_DIR}/syncthing/data:/var/syncthing/data - \${PORTAINER_HSHQ_BACKUP_DIR}:/backup - \${PORTAINER_HSHQ_RELAYSERVER_DIR}/backup:/relayserver networks: dock-proxy-net: name: dock-proxy external: true dock-privateip-net: name: dock-privateip external: true EOFST cat < $HOME/syncthing.env PUID=0 PGID=0 STCONFDIR=/var/syncthing/config STDATADIR=/var/syncthing/data STHOMEDIR= EOFST } function performUpdateSyncthing() { perform_stack_name=syncthing prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=syncthing/syncthing:1.23.7 image_update_map[0]="syncthing/syncthing:1.23.7,syncthing/syncthing:1.27.1" ;; 2) newVer=v5 curImageList=syncthing/syncthing:1.27.1 image_update_map[0]="syncthing/syncthing:1.27.1,syncthing/syncthing:1.27.9" ;; 3) newVer=v5 curImageList=syncthing/syncthing:1.27.2 image_update_map[0]="syncthing/syncthing:1.27.2,syncthing/syncthing:1.27.9" ;; 4) newVer=v5 curImageList=syncthing/syncthing:1.27.4 image_update_map[0]="syncthing/syncthing:1.27.4,syncthing/syncthing:1.27.9" ;; 5) newVer=v7 curImageList=syncthing/syncthing:1.27.9 image_update_map[0]="syncthing/syncthing:1.27.9,syncthing/syncthing:1.29.4" ;; 6) newVer=v8 curImageList=syncthing/syncthing:1.28.0 image_update_map[0]="syncthing/syncthing:1.28.0,mirror.gcr.io/syncthing/syncthing:2.0.0" ;; 7) newVer=v8 curImageList=syncthing/syncthing:1.29.4 image_update_map[0]="syncthing/syncthing:1.29.4,mirror.gcr.io/syncthing/syncthing:2.0.0" ;; 8) newVer=v9 curImageList=mirror.gcr.io/syncthing/syncthing:2.0.0 image_update_map[0]="mirror.gcr.io/syncthing/syncthing:2.0.0,mirror.gcr.io/syncthing/syncthing:2.0.12" ;; 9) newVer=v10 curImageList=mirror.gcr.io/syncthing/syncthing:2.0.12 image_update_map[0]="mirror.gcr.io/syncthing/syncthing:2.0.12,mirror.gcr.io/syncthing/syncthing:2.1.2" ;; 10) newVer=v10 curImageList=mirror.gcr.io/syncthing/syncthing:2.1.2 image_update_map[0]="mirror.gcr.io/syncthing/syncthing:2.1.2,mirror.gcr.io/syncthing/syncthing:2.1.2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # CodeServer function installCodeServer() { set +e is_integrate_hshq=$1 # !Check if directory exists checkDeleteStackAndDirectory codeserver "CodeServer" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName codeserver) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/codeserver mkdir $HSHQ_STACKS_DIR/codeserver/.config mkdir $HSHQ_STACKS_DIR/codeserver/.config/code-server initServicesCredentials CODESERVER_ADMIN_PASSWORD_HASH=$(echo -n "$CODESERVER_ADMIN_PASSWORD" | argon2 $(pwgen -c -n 32 1) -e) generateCert codeserver codeserver outputConfigCodeServer mv $HOME/codeserver.yaml $HSHQ_STACKS_DIR/codeserver/.config/code-server/config.yaml cd ~ docker compose -f $HOME/codeserver-compose-tmp.yml up -d search="HTTPS server listening on https" isFound=false i=0 set +e while [ $i -le 300 ] do findtext=$(docker logs codeserver 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 5 seconds, total wait=$i seconds..." sleep 5 i=$((i+5)) done if ! [ "$isFound" = "true" ]; then echo "CodeServer did not start up correctly..." cd ~ docker compose -f $HOME/codeserver-compose-tmp.yml down -v rm -f $HOME/codeserver-compose-tmp.yml return 1 fi echo "Codeserver installed, sleeping 10 seconds..." sleep 10 docker exec codeserver code-server --install-extension kelvin.vscode-sshfs cd ~ docker compose -f $HOME/codeserver-compose-tmp.yml down -v rm -f $HOME/codeserver-compose-tmp.yml rm -f $HSHQ_STACKS_DIR/codeserver/.local/share/code-server/User/settings.json mv $HOME/codeserver-settings.json $HSHQ_STACKS_DIR/codeserver/.local/share/code-server/User/settings.json installStack codeserver codeserver "HTTPS server listening on https" $HOME/codeserver.env retval=$? if [ $retval -ne 0 ]; then return $retval fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_CODESERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://codeserver:3443 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CODESERVER $MANAGETLS_CODESERVER "$is_integrate_hshq" $NETDEFAULT_CODESERVER "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll codeserver "$FMLNAME_CODESERVER" $USERTYPE_CODESERVER "https://$SUB_CODESERVER.$HOMESERVER_DOMAIN" "codeserver.png" "$(getHeimdallOrderFromSub $SUB_CODESERVER $USERTYPE_CODESERVER)" restartAllCaddyContainers fi } function outputConfigCodeServer() { cat < $HOME/codeserver-compose-tmp.yml services: codeserver: image: $(getScriptImageByContainerName codeserver) container_name: codeserver hostname: codeserver restart: unless-stopped user: "$USERID:$GROUPID" command: --cert /certs/codeserver.crt --cert-key /certs/codeserver.key networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - $HSHQ_STACKS_DIR/codeserver:/home/coder - $HSHQ_SSL_DIR/codeserver.crt:/certs/codeserver.crt - $HSHQ_SSL_DIR/codeserver.key:/certs/codeserver.key environment: - DOCKER_USER=$USERNAME - USER=$USERNAME networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFCS cat < $HOME/codeserver-compose.yml $STACK_VERSION_PREFIX codeserver $(getScriptStackVersion codeserver) services: codeserver: image: $(getScriptImageByContainerName codeserver) container_name: codeserver hostname: codeserver restart: unless-stopped env_file: stack.env user: "\${PORTAINER_UID}:\${PORTAINER_GID}" command: --cert /certs/codeserver.crt --cert-key /certs/codeserver.key networks: - dock-proxy-net #- dock-ext-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/codeserver:/home/coder - \${PORTAINER_HSHQ_STACKS_DIR}/authelia/config/configuration.yml:/home/coder/HSHQ/authelia/configuration.yml - \${PORTAINER_HSHQ_STACKS_DIR}/caddy-common/caddyfiles:/home/coder/HSHQ/caddy/caddyfiles - \${PORTAINER_HSHQ_STACKS_DIR}/caddy-common/snippets:/home/coder/HSHQ/caddy/snippets - \${PORTAINER_HSHQ_SSL_DIR}/codeserver.crt:/certs/codeserver.crt - \${PORTAINER_HSHQ_SSL_DIR}/codeserver.key:/certs/codeserver.key networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-privateip-net: name: dock-privateip external: true EOFCS cat < $HOME/codeserver.env UID=$USERID GID=$GROUPID DOCKER_USER=$USERNAME USER=$USERNAME PORT=3443 EOFCS cat < $HOME/codeserver.yaml bind-addr: 127.0.0.1:3443 auth: password hashed-password: "$CODESERVER_ADMIN_PASSWORD_HASH" cert: false disable-telemetry: true EOFCS cat < $HOME/codeserver-settings.json { "workbench.colorTheme": "Default Dark Modern", "workbench.startupEditor": "none", "telemetry.telemetryLevel": "off", "workbench.editor.enablePreview": false, "files.autoSave": "off" } EOFCS } function performUpdateCodeServer() { perform_stack_name=codeserver prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=codercom/code-server:4.16.1 image_update_map[0]="codercom/code-server:4.16.1,codercom/code-server:4.22.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfAddConfigsMountCodeServer false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v4 curImageList=codercom/code-server:4.20.0 image_update_map[0]="codercom/code-server:4.20.0,codercom/code-server:4.22.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfAddConfigsMountCodeServer false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v4 curImageList=codercom/code-server:4.20.1 image_update_map[0]="codercom/code-server:4.20.1,codercom/code-server:4.22.1" ;; 4) newVer=v7 curImageList=codercom/code-server:4.22.1 image_update_map[0]="codercom/code-server:4.22.1,mirror.gcr.io/codercom/code-server:4.102.2" ;; 5) newVer=v7 curImageList=codercom/code-server:4.93.1 image_update_map[0]="codercom/code-server:4.93.1,mirror.gcr.io/codercom/code-server:4.102.2" ;; 6) newVer=v7 curImageList=codercom/code-server:4.98.2 image_update_map[0]="codercom/code-server:4.98.2,mirror.gcr.io/codercom/code-server:4.102.2" ;; 7) newVer=v8 curImageList=mirror.gcr.io/codercom/code-server:4.102.2 image_update_map[0]="mirror.gcr.io/codercom/code-server:4.102.2,mirror.gcr.io/codercom/code-server:4.106.3" ;; 8) newVer=v8 curImageList=mirror.gcr.io/codercom/code-server:4.106.3 image_update_map[0]="mirror.gcr.io/codercom/code-server:4.106.3,mirror.gcr.io/codercom/code-server:4.106.3" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfAddConfigsMountCodeServer() { sudo grep "authelia/config/configuration.yml" $upgrade_compose_file > /dev/null 2>&1 if [ $? -ne 0 ]; then # Mount some config files into the code server container for easy access to edit sudo sed -i "s|/codeserver:/home/coder|/codeserver:/home/coder\n - \${PORTAINER_HSHQ_STACKS_DIR}/authelia/config/configuration.yml:/home/coder/HSHQ/authelia/configuration.yml\n - \${PORTAINER_HSHQ_STACKS_DIR}/caddy-common/caddyfiles:/home/coder/HSHQ/caddy/caddyfiles\n - \${PORTAINER_HSHQ_STACKS_DIR}/caddy-common/snippets:/home/coder/HSHQ/caddy/snippets|" $upgrade_compose_file fi } # Shlink function installShlink() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory shlink "Shlink" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName shlink-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName shlink-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName shlink-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName shlink-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/shlink mkdir $HSHQ_STACKS_DIR/shlink/db mkdir $HSHQ_STACKS_DIR/shlink/dbexport mkdir $HSHQ_STACKS_DIR/shlink/web chmod 777 $HSHQ_STACKS_DIR/shlink/dbexport initServicesCredentials if [ -z "$SHLINK_REDIS_PASSWORD" ]; then SHLINK_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar SHLINK_REDIS_PASSWORD $SHLINK_REDIS_PASSWORD fi if [ -z "$SHLINK_INITIAL_API_KEY" ]; then SHLINK_INITIAL_API_KEY=$(pwgen -c -n -A 8 1)"-"$(pwgen -c -n -A 4 1)"-"$(pwgen -c -n -A 4 1)"-"$(pwgen -c -n -A 4 1)"-"$(pwgen -c -n -A 12 1) updateConfigVar SHLINK_INITIAL_API_KEY $SHLINK_INITIAL_API_KEY fi outputConfigShlink installStack shlink shlink "shlink" $HOME/shlink.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase Shlink postgres shlink-db $SHLINK_DATABASE_NAME $SHLINK_DATABASE_USER $SHLINK_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/shlink/dbexport $SHLINK_DATABASE_READONLYUSER $SHLINK_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_SHLINK_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://shlink-web:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SHLINK_WEB $MANAGETLS_SHLINK_WEB "$is_integrate_hshq" $NETDEFAULT_SHLINK_WEB "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_SHLINK_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://shlink-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SHLINK_APP $MANAGETLS_SHLINK_APP "$is_integrate_hshq" $NETDEFAULT_SHLINK_APP "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll shlink "$FMLNAME_SHLINK_WEB" $USERTYPE_SHLINK_WEB "https://$SUB_SHLINK_WEB.$HOMESERVER_DOMAIN" "shlink.png" "$(getHeimdallOrderFromSub $SUB_SHLINK_WEB $USERTYPE_SHLINK_WEB)" restartAllCaddyContainers fi } function outputConfigShlink() { cat < $HOME/shlink-compose.yml $STACK_VERSION_PREFIX shlink $(getScriptStackVersion shlink) services: shlink-db: image: $(getScriptImageByContainerName shlink-db) container_name: shlink-db hostname: shlink-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-shlink-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shlink/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shlink/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.shlink-hourly-db.schedule=@every 1h" - "ofelia.job-exec.shlink-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.shlink-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.shlink-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.shlink-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.shlink-hourly-db.email-from=Shlink Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.shlink-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.shlink-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.shlink-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.shlink-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.shlink-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.shlink-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.shlink-monthly-db.email-from=Shlink Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.shlink-monthly-db.mail-only-on-error=false" shlink-app: image: $(getScriptImageByContainerName shlink-app) container_name: shlink-app hostname: shlink-app restart: unless-stopped env_file: stack.env depends_on: - shlink-db networks: - int-shlink-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro shlink-web: image: $(getScriptImageByContainerName shlink-web) container_name: shlink-web hostname: shlink-web restart: unless-stopped env_file: stack.env depends_on: - shlink-db - shlink-app networks: - int-shlink-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shlink/web/servers.json:/usr/share/nginx/html/servers.json shlink-redis: image: $(getScriptImageByContainerName shlink-redis) container_name: shlink-redis hostname: shlink-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $SHLINK_REDIS_PASSWORD --appendonly no networks: - int-shlink-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true int-shlink-net: driver: bridge internal: true ipam: driver: default EOFST cat < $HOME/shlink.env UID=$USERID GID=$GROUPID POSTGRES_DB=$SHLINK_DATABASE_NAME POSTGRES_USER=$SHLINK_DATABASE_USER POSTGRES_PASSWORD=$SHLINK_DATABASE_USER_PASSWORD DB_DRIVER=postgres DB_HOST=shlink-db DB_NAME=$SHLINK_DATABASE_NAME DB_USER=$SHLINK_DATABASE_USER DB_PASSWORD=$SHLINK_DATABASE_USER_PASSWORD DEFAULT_DOMAIN=$SUB_SHLINK_APP.$HOMESERVER_DOMAIN IS_HTTPS_ENABLED=true INITIAL_API_KEY=$SHLINK_INITIAL_API_KEY SHLINK_REDIS_PASSWORD=$SHLINK_REDIS_PASSWORD REDIS_SERVERS=tcp://:$SHLINK_REDIS_PASSWORD@shlink-redis:6379 EOFST cat < $HSHQ_STACKS_DIR/shlink/web/servers.json [ { "name": "$HOMESERVER_NAME", "url": "https://$SUB_SHLINK_APP.$HOMESERVER_DOMAIN", "apiKey": "$SHLINK_INITIAL_API_KEY" } ] EOFST } function performUpdateShlink() { perform_stack_name=shlink prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=postgres:15.0-bullseye,shlinkio/shlink:3.6.3,shlinkio/shlink-web-client:3.10.2,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="shlinkio/shlink:3.6.3,shlinkio/shlink:4.0.3" image_update_map[2]="shlinkio/shlink-web-client:3.10.2,shlinkio/shlink-web-client:4.0.1" image_update_map[3]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfUpdateShlinkInternalWebUIPort false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v3 curImageList=postgres:15.0-bullseye,shlinkio/shlink:3.7.2,shlinkio/shlink-web-client:3.10.2,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="shlinkio/shlink:3.7.2,shlinkio/shlink:4.0.3" image_update_map[2]="shlinkio/shlink-web-client:3.10.2,shlinkio/shlink-web-client:4.0.1" image_update_map[3]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfUpdateShlinkInternalWebUIPort false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,shlinkio/shlink:3.7.3,shlinkio/shlink-web-client:4.0.0,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="shlinkio/shlink:3.7.3,shlinkio/shlink:4.0.3" image_update_map[2]="shlinkio/shlink-web-client:4.0.0,shlinkio/shlink-web-client:4.0.1" image_update_map[3]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" if [ "$stackStatus" = "1" ];then startStopStack shlink stop sleep 3 fi sudo rm -fr $HSHQ_NONBACKUP_DIR/shlink/redis/* upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfShlinkFixRedisUrl if [ "$stackStatus" = "2" ];then startStopStack shlink stop fi perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v6 curImageList=postgres:15.0-bullseye,shlinkio/shlink:4.0.3,shlinkio/shlink-web-client:4.0.1,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="shlinkio/shlink:4.0.3,shlinkio/shlink:4.4.6" image_update_map[2]="shlinkio/shlink-web-client:4.0.1,shlinkio/shlink-web-client:4.3.0" image_update_map[3]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,shlinkio/shlink:4.2.4,shlinkio/shlink-web-client:4.2.2,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="shlinkio/shlink:4.2.4,shlinkio/shlink:4.4.6" image_update_map[2]="shlinkio/shlink-web-client:4.2.2,shlinkio/shlink-web-client:4.3.0" image_update_map[3]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 6) newVer=v8 curImageList=postgres:15.0-bullseye,shlinkio/shlink:4.4.6,shlinkio/shlink-web-client:4.3.0,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="shlinkio/shlink:4.4.6,mirror.gcr.io/shlinkio/shlink:4.5.0" image_update_map[2]="shlinkio/shlink-web-client:4.3.0,mirror.gcr.io/shlinkio/shlink-web-client:4.5.0" image_update_map[3]="bitnami/redis:7.0.5,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfShlinkFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,shlinkio/shlink:4.4.6,shlinkio/shlink-web-client:4.3.0,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="shlinkio/shlink:4.4.6,mirror.gcr.io/shlinkio/shlink:4.5.0" image_update_map[2]="shlinkio/shlink-web-client:4.3.0,mirror.gcr.io/shlinkio/shlink-web-client:4.5.0" image_update_map[3]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfShlinkFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 8) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/shlinkio/shlink:4.5.0,mirror.gcr.io/shlinkio/shlink-web-client:4.5.0,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/shlinkio/shlink:4.5.0,mirror.gcr.io/shlinkio/shlink:4.6.0" image_update_map[2]="mirror.gcr.io/shlinkio/shlink-web-client:4.5.0,mirror.gcr.io/shlinkio/shlink-web-client:4.6.1" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 9) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/shlinkio/shlink:4.6.0,mirror.gcr.io/shlinkio/shlink-web-client:4.6.1,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/shlinkio/shlink:4.6.0,mirror.gcr.io/shlinkio/shlink:4.6.0" image_update_map[2]="mirror.gcr.io/shlinkio/shlink-web-client:4.6.1,mirror.gcr.io/shlinkio/shlink-web-client:4.6.1" image_update_map[3]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfUpdateShlinkInternalWebUIPort() { inner_block="" inner_block=$inner_block">>https://$SUB_SHLINK_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://shlink-web:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SHLINK_WEB $MANAGETLS_SHLINK_WEB true $NETDEFAULT_SHLINK_WEB "$inner_block" restartAllCaddyContainers } function mfShlinkFixRedisUrl() { rm -f $HOME/shlink.env cat < $HOME/shlink.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_DB=$SHLINK_DATABASE_NAME POSTGRES_USER=$SHLINK_DATABASE_USER POSTGRES_PASSWORD=$SHLINK_DATABASE_USER_PASSWORD DB_DRIVER=postgres DB_HOST=shlink-db DB_NAME=$SHLINK_DATABASE_NAME DB_USER=$SHLINK_DATABASE_USER DB_PASSWORD=$SHLINK_DATABASE_USER_PASSWORD DEFAULT_DOMAIN=$SUB_SHLINK_APP.$HOMESERVER_DOMAIN IS_HTTPS_ENABLED=true INITIAL_API_KEY=$SHLINK_INITIAL_API_KEY SHLINK_REDIS_PASSWORD=$SHLINK_REDIS_PASSWORD REDIS_SERVERS=tcp://:$SHLINK_REDIS_PASSWORD@shlink-redis:6379 EOFST } function mfShlinkFixRedisCompose() { replaceRedisBlock shlink shlink-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/shlink/redis/* } # Firefly function installFirefly() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory firefly "Firefly" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firefly-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firefly-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firefly-importer) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firefly-cron) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firefly-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/firefly mkdir $HSHQ_STACKS_DIR/firefly/db mkdir $HSHQ_STACKS_DIR/firefly/dbexport mkdir $HSHQ_STACKS_DIR/firefly/data chmod 777 $HSHQ_STACKS_DIR/firefly/dbexport initServicesCredentials if [ -z "$FIREFLY_REDIS_PASSWORD" ]; then FIREFLY_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar FIREFLY_REDIS_PASSWORD $FIREFLY_REDIS_PASSWORD fi if [ -z "$FIREFLY_INITIAL_API_KEY" ]; then FIREFLY_INITIAL_API_KEY=$(pwgen -c -n 32 1) updateConfigVar FIREFLY_INITIAL_API_KEY $FIREFLY_INITIAL_API_KEY fi outputConfigFirefly installStack firefly firefly-app "ready to handle connections" $HOME/firefly.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase FireflyIII postgres firefly-db $FIREFLY_DATABASE_NAME $FIREFLY_DATABASE_USER $FIREFLY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/firefly/dbexport $FIREFLY_DATABASE_READONLYUSER $FIREFLY_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_FIREFLY_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://firefly-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FIREFLY_APP $MANAGETLS_FIREFLY_APP "$is_integrate_hshq" $NETDEFAULT_FIREFLY_APP "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_FIREFLY_IMPORTER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://firefly-importer:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FIREFLY_IMPORTER $MANAGETLS_FIREFLY_IMPORTER "$is_integrate_hshq" $NETDEFAULT_FIREFLY_IMPORTER "$inner_block" insertSubAuthelia $SUB_FIREFLY_IMPORTER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_FIREFLY_FINTSIMPORTER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://firefly-fints-importer:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FIREFLY_FINTSIMPORTER $MANAGETLS_FIREFLY_FINTSIMPORTER "$is_integrate_hshq" $NETDEFAULT_FIREFLY_FINTSIMPORTER "$inner_block" insertSubAuthelia $SUB_FIREFLY_FINTSIMPORTER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll firefly "$FMLNAME_FIREFLY_APP" $USERTYPE_FIREFLY_APP "https://$SUB_FIREFLY_APP.$HOMESERVER_DOMAIN" "firefly.png" "$(getHeimdallOrderFromSub $SUB_FIREFLY_APP $USERTYPE_FIREFLY_APP)" insertEnableSvcAll firefly "$FMLNAME_FIREFLY_IMPORTER" $USERTYPE_FIREFLY_IMPORTER "https://$SUB_FIREFLY_IMPORTER.$HOMESERVER_DOMAIN" "firefly-importer.png" "$(getHeimdallOrderFromSub $SUB_FIREFLY_IMPORTER $USERTYPE_FIREFLY_IMPORTER)" insertEnableSvcAll firefly "$FMLNAME_FIREFLY_FINTSIMPORTER" $USERTYPE_FIREFLY_FINTSIMPORTER "https://$SUB_FIREFLY_FINTSIMPORTER.$HOMESERVER_DOMAIN" "firefly-importer.png" "$(getHeimdallOrderFromSub $SUB_FIREFLY_FINTSIMPORTER $USERTYPE_FIREFLY_FINTSIMPORTER)" restartAllCaddyContainers fi } function outputConfigFirefly() { outputComposeFirefly cat < $HOME/firefly.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID SITE_OWNER=$EMAIL_ADMIN_EMAIL_ADDRESS APP_URL=https://$SUB_FIREFLY_APP.$HOMESERVER_DOMAIN TRUSTED_PROXIES=$DOCKER_NETWORK_RESERVED_RANGE APP_KEY=$FIREFLY_INITIAL_API_KEY DEFAULT_LANGUAGE=en_US DEFAULT_LOCALE=equal MAIL_MAILER=smtp MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS MAIL_ENCRYPTION=tls DB_CONNECTION=pgsql DB_HOST=firefly-db DB_PORT=5432 DB_DATABASE=$FIREFLY_DATABASE_NAME DB_USERNAME=$FIREFLY_DATABASE_USER DB_PASSWORD=$FIREFLY_DATABASE_USER_PASSWORD REDIS_HOST=firefly-redis REDIS_PASSWORD=$FIREFLY_REDIS_PASSWORD POSTGRES_DB=$FIREFLY_DATABASE_NAME POSTGRES_USER=$FIREFLY_DATABASE_USER POSTGRES_PASSWORD=$FIREFLY_DATABASE_USER_PASSWORD FIREFLY_III_URL=http://firefly-app:8080 VANITY_URL=https://$SUB_FIREFLY_APP.$HOMESERVER_DOMAIN STATIC_CRON_TOKEN=$FIREFLY_STATIC_CRON_TOKEN EOFFF } function outputComposeFirefly() { cat < $HOME/firefly-compose.yml $STACK_VERSION_PREFIX firefly $(getScriptStackVersion firefly) services: firefly-db: image: $(getScriptImageByContainerName firefly-db) container_name: firefly-db hostname: firefly-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb depends_on: - firefly-redis networks: - int-firefly-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/firefly/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/firefly/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.firefly-hourly-db.schedule=@every 1h" - "ofelia.job-exec.firefly-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.firefly-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.firefly-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.firefly-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.firefly-hourly-db.email-from=Firefly Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.firefly-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.firefly-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.firefly-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.firefly-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.firefly-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.firefly-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.firefly-monthly-db.email-from=Firefly Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.firefly-monthly-db.mail-only-on-error=false" firefly-app: image: $(getScriptImageByContainerName firefly-app) container_name: firefly-app hostname: firefly-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - firefly-db networks: - int-firefly-net - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-firefly-data:/var/www/html/storage/upload firefly-importer: image: $(getScriptImageByContainerName firefly-importer) container_name: firefly-importer hostname: firefly-importer restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - firefly-app networks: - int-firefly-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro firefly-fints-importer: image: $(getScriptImageByContainerName firefly-fints-importer) container_name: firefly-fints-importer hostname: firefly-fints-importer restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - firefly-app networks: - int-firefly-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/firefly/fints:/app/configurations firefly-cron: image: $(getScriptImageByContainerName firefly-cron) container_name: firefly-cron hostname: firefly-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: sh -c " echo \"0 3 * * * wget -qO- http://firefly-app:8080/api/v1/cron/$FIREFLY_STATIC_CRON_TOKEN;echo\" | crontab - && crond -f -L /dev/stdout" depends_on: - firefly-app networks: - int-firefly-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro firefly-redis: image: $(getScriptImageByContainerName firefly-redis) container_name: firefly-redis hostname: firefly-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $FIREFLY_REDIS_PASSWORD --appendonly no networks: - int-firefly-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-firefly-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/firefly/db v-firefly-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/firefly/data networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true int-firefly-net: driver: bridge internal: true ipam: driver: default EOFFF } function performUpdateFirefly() { perform_stack_name=firefly prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.0.20,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.0.20,fireflyiii/core:version-6.1.19" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 2) newVer=v5 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.1.1,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.1.1,fireflyiii/core:version-6.1.19" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 3) newVer=v5 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.1.7,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.1.7,fireflyiii/core:version-6.1.19" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.1.10,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.1.10,fireflyiii/core:version-6.1.19" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 5) newVer=v7 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.1.19,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.1.19,fireflyiii/core:version-6.2.10" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.1.21,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.1.21,fireflyiii/core:version-6.2.10" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.2.10,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.2.10,fireflyiii/core:version-6.2.10" image_update_map[2]="bitnami/redis:7.4.2,bitnami/redis:7.4.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfFireflyAddImporterAndCron perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 8) newVer=v9 curImageList=postgres:15.0-bullseye,fireflyiii/core:version-6.2.10,bitnami/redis:7.4.2,fireflyiii/data-importer:version-1.6.1,alpine:3.21.3 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="fireflyiii/core:version-6.2.10,mirror.gcr.io/fireflyiii/core:version-6.2.21" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[3]="fireflyiii/data-importer:version-1.6.1,mirror.gcr.io/fireflyiii/data-importer:version-1.7.9" image_update_map[4]="alpine:3.21.3,mirror.gcr.io/alpine:3.22.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfFireflyFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/fireflyiii/core:version-6.2.21,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/fireflyiii/data-importer:version-1.7.9,mirror.gcr.io/alpine:3.22.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/fireflyiii/core:version-6.2.21,mirror.gcr.io/fireflyiii/core:version-6.4.9" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/fireflyiii/data-importer:version-1.7.9,mirror.gcr.io/fireflyiii/data-importer:version-1.9.1" image_update_map[4]="mirror.gcr.io/alpine:3.22.1,mirror.gcr.io/alpine:3.23.0" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfFireflyAdd2ndImporter perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 10) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/fireflyiii/core:version-6.4.9,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/fireflyiii/data-importer:version-1.9.1,mirror.gcr.io/alpine:3.23.0,mirror.gcr.io/benkl/firefly-iii-fints-importer image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/fireflyiii/core:version-6.4.9,mirror.gcr.io/fireflyiii/core:version-6.4.9" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/fireflyiii/data-importer:version-1.9.1,mirror.gcr.io/fireflyiii/data-importer:version-1.9.1" image_update_map[4]="mirror.gcr.io/alpine:3.23.0,mirror.gcr.io/alpine:3.23.0" image_update_map[5]="mirror.gcr.io/benkl/firefly-iii-fints-importer,mirror.gcr.io/benkl/firefly-iii-fints-importer" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfFireflyAddImporterAndCron() { pullImage $(getScriptImageByContainerName firefly-importer) pullImage $(getScriptImageByContainerName firefly-cron) initServicesCredentials rm -f $HOME/firefly-compose.yml rm -f $HOME/firefly.env outputConfigFirefly inner_block="" inner_block=$inner_block">>https://$SUB_FIREFLY_IMPORTER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://firefly-importer:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FIREFLY_IMPORTER $MANAGETLS_FIREFLY_IMPORTER "$is_integrate_hshq" $NETDEFAULT_FIREFLY_IMPORTER "$inner_block" insertSubAuthelia $SUB_FIREFLY_IMPORTER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} insertEnableSvcAll firefly "$FMLNAME_FIREFLY_IMPORTER" $USERTYPE_FIREFLY_IMPORTER "https://$SUB_FIREFLY_IMPORTER.$HOMESERVER_DOMAIN" "firefly-importer.png" "$(getHeimdallOrderFromSub $SUB_FIREFLY_IMPORTER $USERTYPE_FIREFLY_IMPORTER)" restartAllCaddyContainers } function mfFireflyFixRedisCompose() { replaceRedisBlock firefly firefly-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/firefly/redis/* } function mfFireflyAdd2ndImporter() { pullImage $(getScriptImageByContainerName firefly-fints-importer) mkdir -p $HSHQ_STACKS_DIR/firefly/fints rm -f $HOME/firefly-compose.yml outputComposeFirefly sed -i "s/^TRUSTED_PROXIES=.*/TRUSTED_PROXIES=$DOCKER_NETWORK_RESERVED_RANGE/" $HOME/firefly.env inner_block="" inner_block=$inner_block">>https://$SUB_FIREFLY_FINTSIMPORTER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://firefly-fints-importer:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FIREFLY_FINTSIMPORTER $MANAGETLS_FIREFLY_FINTSIMPORTER "$is_integrate_hshq" $NETDEFAULT_FIREFLY_FINTSIMPORTER "$inner_block" insertSubAuthelia $SUB_FIREFLY_FINTSIMPORTER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} insertEnableSvcAll firefly "$FMLNAME_FIREFLY_FINTSIMPORTER" $USERTYPE_FIREFLY_FINTSIMPORTER "https://$SUB_FIREFLY_FINTSIMPORTER.$HOMESERVER_DOMAIN" "firefly-importer.png" "$(getHeimdallOrderFromSub $SUB_FIREFLY_FINTSIMPORTER $USERTYPE_FIREFLY_FINTSIMPORTER)" restartAllCaddyContainers } # Excalidraw function installExcalidraw() { set +e echo "************************************************************************" echo "This stack has been deprecated, please install WhiteboardRT instead." echo "************************************************************************" return is_integrate_hshq=$1 checkDeleteStackAndDirectory excalidraw "Excalidraw" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName excalidraw-server) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName excalidraw-storage) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName excalidraw-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName excalidraw-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName excalidraw-ai) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName excalidraw-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/excalidraw mkdir $HSHQ_STACKS_DIR/excalidraw/redis if [ -z "$EXCALIDRAW_REDIS_PASSWORD" ]; then EXCALIDRAW_REDIS_PASSWORD=$(pwgen -c -n 64 1) updateConfigVar EXCALIDRAW_REDIS_PASSWORD $EXCALIDRAW_REDIS_PASSWORD fi outputConfigExcalidraw installStack excalidraw excalidraw-web "" $HOME/excalidraw.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addMCPServerLiteLLM "excalidraw" "exd" "http://excalidraw-mcp:3100/mcp" http none "" inner_block="" inner_block=$inner_block">>https://$SUB_EXCALIDRAW_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://excalidraw-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_EXCALIDRAW_WEB $MANAGETLS_EXCALIDRAW_WEB "$is_integrate_hshq" $NETDEFAULT_EXCALIDRAW_WEB "$inner_block" insertSubAuthelia $SUB_EXCALIDRAW_WEB.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://excalidraw-server {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_EXCALIDRAW_SERVER $MANAGETLS_EXCALIDRAW_SERVER "$is_integrate_hshq" $NETDEFAULT_EXCALIDRAW_SERVER "$inner_block" insertSubAuthelia $SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://excalidraw-storage:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_EXCALIDRAW_STORAGE $MANAGETLS_EXCALIDRAW_STORAGE "$is_integrate_hshq" $NETDEFAULT_EXCALIDRAW_STORAGE "$inner_block" insertSubAuthelia $SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>forward_auth https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>uri /api/verify?rd=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN\n" inner_block=$inner_block">>>>>>>>copy_headers Remote-User Remote-Groups Remote-Name Remote-Email\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://excalidraw-ai:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_EXCALIDRAW_AI $MANAGETLS_EXCALIDRAW_AI "$is_integrate_hshq" $NETDEFAULT_EXCALIDRAW_AI "$inner_block" insertSubAuthelia $SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll excalidraw "$FMLNAME_EXCALIDRAW_WEB" $USERTYPE_EXCALIDRAW_WEB "https://$SUB_EXCALIDRAW_WEB.$HOMESERVER_DOMAIN" "excalidraw.png" "$(getHeimdallOrderFromSub $SUB_EXCALIDRAW_WEB $USERTYPE_EXCALIDRAW_WEB)" insertEnableSvcUptimeKuma excalidraw "$FMLNAME_EXCALIDRAW_WEB Server" $USERTYPE_EXCALIDRAW_WEB "https://$SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN" true insertEnableSvcHeimdall excalidraw "$FMLNAME_EXCALIDRAW_AI" $USERTYPE_EXCALIDRAW_AI "https://$SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN" "excalidraw2.png" true "$(getHeimdallOrderFromSub $SUB_EXCALIDRAW_AI $USERTYPE_EXCALIDRAW_AI)" restartAllCaddyContainers fi } function outputConfigExcalidraw() { outputComposeExcalidraw outputJSONConfigExcalidraw outputEnvExcalidraw } function outputComposeExcalidraw() { cat < $HOME/excalidraw-compose.yml $STACK_VERSION_PREFIX excalidraw $(getScriptStackVersion excalidraw) services: excalidraw-storage: image: $(getScriptImageByContainerName excalidraw-storage) container_name: excalidraw-storage hostname: excalidraw-storage restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-server: image: $(getScriptImageByContainerName excalidraw-server) container_name: excalidraw-server hostname: excalidraw-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-web: image: $(getScriptImageByContainerName excalidraw-web) container_name: excalidraw-web hostname: excalidraw-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/excalidraw/env.json:/usr/share/nginx/html/env.json:ro excalidraw-ai: image: $(getScriptImageByContainerName excalidraw-ai) container_name: excalidraw-ai hostname: excalidraw-ai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-mcp: image: $(getScriptImageByContainerName excalidraw-mcp) container_name: excalidraw-mcp hostname: excalidraw-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-excalidraw-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-redis: image: $(getScriptImageByContainerName excalidraw-redis) container_name: excalidraw-redis hostname: excalidraw-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $EXCALIDRAW_REDIS_PASSWORD --appendonly yes networks: - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-excalidraw-redis:/data volumes: v-excalidraw-redis: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/excalidraw/redis networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true int-excalidraw-net: driver: bridge internal: true ipam: driver: default EOFEX } function outputJSONConfigExcalidraw() { cat < $HSHQ_STACKS_DIR/excalidraw/env.json { "VITE_APP_WS_SERVER_URL": "https://$SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN", "VITE_APP_STORAGE_BACKEND": "http", "VITE_APP_HTTP_STORAGE_BACKEND_URL": "https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2", "VITE_APP_BACKEND_V2_GET_URL": "https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2/scenes", "VITE_APP_BACKEND_V2_POST_URL": "https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2/scenes", "VITE_APP_INTERNAL_STORAGE_URL": "http://excalidraw-storage:8080/api/v2" } EOFEX } function outputEnvExcalidraw() { cat < $HOME/excalidraw.env ALLOW_EMPTY_PASSWORD=no REDIS_PASSWORD=$EXCALIDRAW_REDIS_PASSWORD REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL STORAGE_URI=redis://:$EXCALIDRAW_REDIS_PASSWORD@excalidraw-redis:6379 BACKEND_V2_GET_URL=https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2/scenes/ BACKEND_V2_POST_URL=https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2/scenes/ LIBRARY_URL=https://libraries.excalidraw.com LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries/ STORAGE_BACKEND=http HTTP_STORAGE_BACKEND_URL=https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2 SOCKET_SERVER_URL=https://$SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN VITE_APP_WS_SERVER_URL=https://$SUB_EXCALIDRAW_SERVER.$HOMESERVER_DOMAIN VITE_APP_STORAGE_BACKEND=http VITE_APP_HTTP_STORAGE_BACKEND_URL=https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2 VITE_APP_BACKEND_V2_GET_URL=https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2/scenes VITE_APP_BACKEND_V2_POST_URL=https://$SUB_EXCALIDRAW_STORAGE.$HOMESERVER_DOMAIN/api/v2/scenes VITE_APP_INTERNAL_STORAGE_URL=http://excalidraw-storage:8080/api/v2 NODE_ENV=production EXPRESS_SERVER_URL=http://excalidraw-ai:3000 ENABLE_CANVAS_SYNC=true MCP_HTTP_HOST=0.0.0.0 MCP_PORT=3100 MCP_TRANSPORT=http DISABLED_TOOLS=get_canvas_screenshot,export_to_image EXCALIDRAW_PUBLIC_URL=https://$SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN/ EOFEX } function buildImageExcalidrawWebV1() { pullImage mirror.gcr.io/kiliandeca/excalidraw docker run -d --rm --name excal-temp mirror.gcr.io/kiliandeca/excalidraw docker commit excal-temp hshq/excalidraw-web:v1 docker container stop excal-temp } function buildImageExcalidrawAIV1() { set +e echo -e "\n========================================================================" echo -e " The Excalidraw AI images are being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/excalidraw-ai cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 https://github.com/homeserverhq/excalidraw-ai.git cd $HSHQ_BUILD_DIR/excalidraw-ai docker image build -t hshq/excalidraw-mcp:v1 -f ./Dockerfile . rtval1=$? docker image build -t hshq/excalidraw-ai:v1 -f ./Dockerfile.canvas . rtval2=$? cd sudo rm -fr $HSHQ_BUILD_DIR/excalidraw-ai return $(($rtval1 + $rtval2)) } function performUpdateExcalidraw() { perform_stack_name=excalidraw prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=excalidraw/excalidraw-room,kiliandeca/excalidraw-storage-backend,kiliandeca/excalidraw,bitnami/redis:7.0.5 image_update_map[0]="excalidraw/excalidraw-room,mirror.gcr.io/excalidraw/excalidraw-room" image_update_map[1]="kiliandeca/excalidraw-storage-backend,mirror.gcr.io/kiliandeca/excalidraw-storage-backend" image_update_map[2]="kiliandeca/excalidraw,mirror.gcr.io/kiliandeca/excalidraw" image_update_map[3]="bitnami/redis:7.0.5,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfExcalidrawFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v3 curImageList=excalidraw/excalidraw-room,kiliandeca/excalidraw-storage-backend,kiliandeca/excalidraw,bitnami/redis:7.4.2 image_update_map[0]="excalidraw/excalidraw-room,mirror.gcr.io/excalidraw/excalidraw-room" image_update_map[1]="kiliandeca/excalidraw-storage-backend,mirror.gcr.io/kiliandeca/excalidraw-storage-backend" image_update_map[2]="kiliandeca/excalidraw,mirror.gcr.io/kiliandeca/excalidraw" image_update_map[3]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfExcalidrawFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v4 curImageList=mirror.gcr.io/excalidraw/excalidraw-room,mirror.gcr.io/kiliandeca/excalidraw-storage-backend,mirror.gcr.io/kiliandeca/excalidraw,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/excalidraw/excalidraw-room,mirror.gcr.io/excalidraw/excalidraw-room" image_update_map[1]="mirror.gcr.io/kiliandeca/excalidraw-storage-backend,mirror.gcr.io/kiliandeca/excalidraw-storage-backend" image_update_map[2]="mirror.gcr.io/kiliandeca/excalidraw,mirror.gcr.io/kiliandeca/excalidraw" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 4) newVer=v5 curImageList=mirror.gcr.io/excalidraw/excalidraw-room,mirror.gcr.io/kiliandeca/excalidraw-storage-backend,mirror.gcr.io/kiliandeca/excalidraw,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/excalidraw/excalidraw-room,mirror.gcr.io/excalidraw/excalidraw-room" image_update_map[1]="mirror.gcr.io/kiliandeca/excalidraw-storage-backend,mirror.gcr.io/kiliandeca/excalidraw-storage-backend" image_update_map[2]="mirror.gcr.io/kiliandeca/excalidraw,hshq/excalidraw-web:v1" image_update_map[3]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPerformUpdateV5 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v6 curImageList=mirror.gcr.io/excalidraw/excalidraw-room,mirror.gcr.io/kiliandeca/excalidraw-storage-backend,hshq/excalidraw-web:v1,mirror.gcr.io/valkey/valkey:alpine3.23,hshq/excalidraw-ai:v1,hshq/excalidraw-mcp:v1 image_update_map[0]="mirror.gcr.io/excalidraw/excalidraw-room,ghcr.io/homeserverhq/excalidraw-room:v0.1" image_update_map[1]="mirror.gcr.io/kiliandeca/excalidraw-storage-backend,ghcr.io/homeserverhq/excalidraw-storage-backend:v0.1" image_update_map[2]="hshq/excalidraw-web:v1,ghcr.io/homeserverhq/excalidraw:v0.1" image_update_map[3]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[4]="hshq/excalidraw-ai:v1,ghcr.io/homeserverhq/excalidraw-canvas:v0.1" image_update_map[5]="hshq/excalidraw-mcp:v1,ghcr.io/homeserverhq/excalidraw-mcp:v0.1" ;; 6) newVer=v6 curImageList=ghcr.io/homeserverhq/excalidraw-room:v0.1,ghcr.io/homeserverhq/excalidraw-storage-backend:v0.1,ghcr.io/homeserverhq/excalidraw:v0.1,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/excalidraw-canvas:v0.1,ghcr.io/homeserverhq/excalidraw-mcp:v0.1 image_update_map[0]="ghcr.io/homeserverhq/excalidraw-room:v0.1,ghcr.io/homeserverhq/excalidraw-room:v0.1" image_update_map[1]="ghcr.io/homeserverhq/excalidraw-storage-backend:v0.1,ghcr.io/homeserverhq/excalidraw-storage-backend:v0.1" image_update_map[2]="ghcr.io/homeserverhq/excalidraw:v0.1,ghcr.io/homeserverhq/excalidraw:v0.1" image_update_map[3]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[4]="ghcr.io/homeserverhq/excalidraw-canvas:v0.1,ghcr.io/homeserverhq/excalidraw-canvas:v0.1" image_update_map[5]="ghcr.io/homeserverhq/excalidraw-mcp:v0.1,ghcr.io/homeserverhq/excalidraw-mcp:v0.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function outputComposeV5Excalidraw() { cat < $HOME/excalidraw-compose.yml $STACK_VERSION_PREFIX excalidraw v5 services: excalidraw-storage: image: mirror.gcr.io/kiliandeca/excalidraw-storage-backend container_name: excalidraw-storage hostname: excalidraw-storage restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-server: image: mirror.gcr.io/excalidraw/excalidraw-room container_name: excalidraw-server hostname: excalidraw-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-web: image: hshq/excalidraw-web:v1 container_name: excalidraw-web hostname: excalidraw-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/excalidraw/env.json:/usr/share/nginx/html/env.json:ro excalidraw-ai: image: hshq/excalidraw-ai:v1 container_name: excalidraw-ai hostname: excalidraw-ai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-mcp: image: hshq/excalidraw-mcp:v1 container_name: excalidraw-mcp hostname: excalidraw-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-excalidraw-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro excalidraw-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: excalidraw-redis hostname: excalidraw-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $EXCALIDRAW_REDIS_PASSWORD --appendonly yes networks: - int-excalidraw-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-excalidraw-redis:/data volumes: v-excalidraw-redis: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/excalidraw/redis networks: dock-proxy-net: name: dock-proxy external: true dock-aipriv-net: name: dock-aipriv external: true int-excalidraw-net: driver: bridge internal: true ipam: driver: default EOFEX } function mfExcalidrawFixRedisCompose() { lineStart=" excalidraw-redis:" lineEnd=" - v-excalidraw-redis:/bitnami/redis/data" replaceText="" replaceText=$replaceText">>excalidraw-redis:\n" replaceText=$replaceText">>>>image: mirror.gcr.io/redis:8.2.0-bookworm\n" replaceText=$replaceText">>>>container_name: excalidraw-redis\n" replaceText=$replaceText">>>>hostname: excalidraw-redis\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>env_file: stack.env\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--requirepass $EXCALIDRAW_REDIS_PASSWORD\n" replaceText=$replaceText">>>>>>--appendonly yes\n" replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- int-excalidraw-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n" replaceText=$replaceText">>>>>>- /etc/ssl/certs:/etc/ssl/certs:ro\n" replaceText=$replaceText">>>>>>- /usr/share/ca-certificates:/usr/share/ca-certificates:ro\n" replaceText=$replaceText">>>>>>- /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro\n" replaceText=$replaceText">>>>>>- v-excalidraw-redis:/data\n" replaceTextBlockInFile "$lineStart" "$lineEnd" "$replaceText" $HOME/excalidraw-compose.yml false ">" cat -s $HOME/excalidraw-compose.yml > $HOME/ac.tmp mv $HOME/ac.tmp $HOME/excalidraw-compose.yml sed -i "/ALLOW_EMPTY_PASSWORD=.*/d" $HOME/excalidraw.env sed -i "/REDIS_PASSWORD=.*/d" $HOME/excalidraw.env sed -i "/REDIS_DISABLE_COMMANDS=.*/d" $HOME/excalidraw.env sudo rm -fr $HSHQ_STACKS_DIR/excalidraw/redis/* } function mfPerformUpdateV5() { buildImageExcalidrawWebV1 if [ $? -ne 0 ]; then return 1 fi buildImageExcalidrawAIV1 if [ $? -ne 0 ]; then return 1 fi if [ "$stackStatus" = "1" ];then # Stack is running, stop it, then clear data startStopStack excalidraw stop isStartStackFromStopped=true sleep 1 fi sudo rm -fr $HSHQ_STACKS_DIR/excalidraw/redis/* outputComposeV5Excalidraw outputJSONConfigExcalidraw outputEnvExcalidraw inner_block="" inner_block=$inner_block">>https://$SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>forward_auth https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>uri /api/verify?rd=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN\n" inner_block=$inner_block">>>>>>>>copy_headers Remote-User Remote-Groups Remote-Name Remote-Email\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://excalidraw-ai:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_EXCALIDRAW_AI $MANAGETLS_EXCALIDRAW_AI "$is_integrate_hshq" $NETDEFAULT_EXCALIDRAW_AI "$inner_block" insertSubAuthelia $SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} insertEnableSvcAll excalidraw "$FMLNAME_EXCALIDRAW_AI" $USERTYPE_EXCALIDRAW_AI "https://$SUB_EXCALIDRAW_AI.$HOMESERVER_DOMAIN" "excalidraw2.png" "$(getHeimdallOrderFromSub $SUB_EXCALIDRAW_AI $USERTYPE_EXCALIDRAW_AI)" addMCPServerLiteLLM "excalidraw" "exd" "http://excalidraw-mcp:3100/mcp" http none "" } # DrawIO function installDrawIO() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory drawio "Draw.io" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName drawio-plantuml) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName drawio-export) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName drawio-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName drawio-nextai) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/drawio mkdir $HSHQ_STACKS_DIR/drawio/fonts outputConfigDrawIO installStack drawio drawio-web "" $HOME/drawio.env retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Draw.io installed, sleeping 10 seconds..." sleep 10 set +e docker container ps | grep nextcloud-app > /dev/null 2>&1 if [ $? -eq 0 ]; then docker exec -u www-data nextcloud-app php occ --no-warnings app:install drawio docker exec -u www-data nextcloud-app php occ --no-warnings config:app:set drawio DrawioUrl --value="https://$SUB_DRAWIO_WEB.$HOMESERVER_DOMAIN" fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_DRAWIO_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://drawio-web:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DRAWIO_WEB $MANAGETLS_DRAWIO_WEB "$is_integrate_hshq" $NETDEFAULT_DRAWIO_WEB "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_DRAWIO_NEXTAI.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://drawio-nextai:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DRAWIO_NEXTAI $MANAGETLS_DRAWIO_NEXTAI "$is_integrate_hshq" $NETDEFAULT_DRAWIO_NEXTAI "$inner_block" insertSubAuthelia $SUB_DRAWIO_NEXTAI.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll drawio "$FMLNAME_DRAWIO_WEB" $USERTYPE_DRAWIO_WEB "https://$SUB_DRAWIO_WEB.$HOMESERVER_DOMAIN" "drawio.png" "$(getHeimdallOrderFromSub $SUB_DRAWIO_WEB $USERTYPE_DRAWIO_WEB)" insertEnableSvcAll drawio "$FMLNAME_DRAWIO_NEXTAI" $USERTYPE_DRAWIO_NEXTAI "https://$SUB_DRAWIO_NEXTAI.$HOMESERVER_DOMAIN" "nextai-drawio.png" "$(getHeimdallOrderFromSub $SUB_DRAWIO_NEXTAI $USERTYPE_DRAWIO_NEXTAI)" restartAllCaddyContainers fi } function outputConfigDrawIO() { cat < $HOME/drawio-compose.yml $STACK_VERSION_PREFIX drawio $(getScriptStackVersion drawio) services: drawio-plantuml: image: $(getScriptImageByContainerName drawio-plantuml) container_name: drawio-plantuml hostname: drawio-plantuml restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-drawio-fonts:/usr/share/fonts/drawio drawio-export: image: $(getScriptImageByContainerName drawio-export) container_name: drawio-export hostname: drawio-export restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-drawio-fonts:/usr/share/fonts/drawio drawio-web: image: $(getScriptImageByContainerName drawio-web) container_name: drawio-web hostname: drawio-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - drawio-plantuml - drawio-export networks: - dock-proxy-net - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro drawio-nextai: image: $(getScriptImageByContainerName drawio-nextai) container_name: drawio-nextai hostname: drawio-nextai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - drawio-plantuml - drawio-export - drawio-web networks: - dock-proxy-net - dock-aipriv-net - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-drawio-fonts: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/drawio/fonts networks: dock-proxy-net: name: dock-proxy external: true dock-aipriv-net: name: dock-aipriv external: true int-drawio-net: driver: bridge internal: true ipam: driver: default EOFDI cat < $HOME/drawio.env DRAWIO_SELF_CONTAINED=1 PLANTUML_URL=http://drawio-plantuml:8080/ EXPORT_URL=http://drawio-export:8000/ DRAWIO_BASE_URL=https://$SUB_DRAWIO_WEB.$HOMESERVER_DOMAIN NEXT_PUBLIC_DRAWIO_BASE_URL=http://drawio-web:8080 AI_PROVIDER=openai AI_MODEL=Thinking OPENAI_BASE_URL=http://litellm-proxy:4000/v1 OPENAI_API_KEY=$LITELLM_AUTH_KEY OPENAI_REASONING_EFFORT=low OPENAI_REASONING_SUMMARY=detailed TEMPERATURE=0.3 OLLAMA_ENABLE_THINKING=true EOFDI } function performUpdateDrawIO() { perform_stack_name=drawio prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=jgraph/drawio:21.0.2,jgraph/plantuml-server,jgraph/export-server image_update_map[0]="jgraph/drawio:21.0.2,jgraph/drawio:24.7.5" image_update_map[1]="jgraph/plantuml-server,jgraph/plantuml-server" image_update_map[2]="jgraph/export-server,jgraph/export-server" ;; 2) newVer=v4 curImageList=jgraph/drawio:23.1.0,jgraph/plantuml-server,jgraph/export-server image_update_map[0]="jgraph/drawio:23.1.0,jgraph/drawio:24.7.5" image_update_map[1]="jgraph/plantuml-server,jgraph/plantuml-server" image_update_map[2]="jgraph/export-server,jgraph/export-server" ;; 3) newVer=v4 curImageList=jgraph/drawio:24.0.7,jgraph/plantuml-server,jgraph/export-server image_update_map[0]="jgraph/drawio:24.0.7,jgraph/drawio:24.7.5" image_update_map[1]="jgraph/plantuml-server,jgraph/plantuml-server" image_update_map[2]="jgraph/export-server,jgraph/export-server" ;; 4) newVer=v7 curImageList=jgraph/drawio:24.7.5,jgraph/plantuml-server,jgraph/export-server image_update_map[0]="jgraph/drawio:24.7.5,mirror.gcr.io/jgraph/drawio:28.0.7" image_update_map[1]="jgraph/plantuml-server,mirror.gcr.io/jgraph/plantuml-server" image_update_map[2]="jgraph/export-server,mirror.gcr.io/jgraph/export-server" ;; 5) newVer=v7 curImageList=jgraph/drawio:24.7.17,jgraph/plantuml-server,jgraph/export-server image_update_map[0]="jgraph/drawio:24.7.17,mirror.gcr.io/jgraph/drawio:28.0.7" image_update_map[1]="jgraph/plantuml-server,mirror.gcr.io/jgraph/plantuml-server" image_update_map[2]="jgraph/export-server,mirror.gcr.io/jgraph/export-server" ;; 6) newVer=v7 curImageList=jgraph/drawio:26.2.2,jgraph/plantuml-server,jgraph/export-server image_update_map[0]="jgraph/drawio:26.2.2,mirror.gcr.io/jgraph/drawio:28.0.7" image_update_map[1]="jgraph/plantuml-server,mirror.gcr.io/jgraph/plantuml-server" image_update_map[2]="jgraph/export-server,mirror.gcr.io/jgraph/export-server" ;; 7) newVer=v8 curImageList=mirror.gcr.io/jgraph/drawio:28.0.7,mirror.gcr.io/jgraph/plantuml-server,mirror.gcr.io/jgraph/export-server image_update_map[0]="mirror.gcr.io/jgraph/drawio:28.0.7,mirror.gcr.io/jgraph/drawio:29.2.2" image_update_map[1]="mirror.gcr.io/jgraph/plantuml-server,mirror.gcr.io/jgraph/plantuml-server" image_update_map[2]="mirror.gcr.io/jgraph/export-server,mirror.gcr.io/jgraph/export-server" ;; 8) newVer=v9 curImageList=mirror.gcr.io/jgraph/drawio:29.2.2,mirror.gcr.io/jgraph/plantuml-server,mirror.gcr.io/jgraph/export-server image_update_map[0]="mirror.gcr.io/jgraph/drawio:29.2.2,mirror.gcr.io/jgraph/drawio:29.2.2" image_update_map[1]="mirror.gcr.io/jgraph/plantuml-server,mirror.gcr.io/jgraph/plantuml-server" image_update_map[2]="mirror.gcr.io/jgraph/export-server,mirror.gcr.io/jgraph/export-server" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true addNextAIDrawIOV9 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=v9 curImageList=mirror.gcr.io/jgraph/drawio:29.2.2,mirror.gcr.io/jgraph/plantuml-server,mirror.gcr.io/jgraph/export-server,ghcr.io/dayuanjiang/next-ai-draw-io:sha-5007c7b image_update_map[0]="mirror.gcr.io/jgraph/drawio:29.2.2,mirror.gcr.io/jgraph/drawio:29.2.2" image_update_map[1]="mirror.gcr.io/jgraph/plantuml-server,mirror.gcr.io/jgraph/plantuml-server" image_update_map[2]="mirror.gcr.io/jgraph/export-server,mirror.gcr.io/jgraph/export-server" image_update_map[3]="ghcr.io/dayuanjiang/next-ai-draw-io:sha-5007c7b,ghcr.io/dayuanjiang/next-ai-draw-io:sha-5007c7b" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function addNextAIDrawIOV9() { pullImage ghcr.io/dayuanjiang/next-ai-draw-io:sha-5007c7b if [ $? -ne 0 ]; then return 1 fi rm -f $HOME/drawio-compose.yml cat < $HOME/drawio-compose.yml $STACK_VERSION_PREFIX drawio v9 services: drawio-plantuml: image: mirror.gcr.io/jgraph/plantuml-server container_name: drawio-plantuml hostname: drawio-plantuml restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-drawio-fonts:/usr/share/fonts/drawio drawio-export: image: mirror.gcr.io/jgraph/export-server container_name: drawio-export hostname: drawio-export restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-drawio-fonts:/usr/share/fonts/drawio drawio-web: image: mirror.gcr.io/jgraph/drawio:29.2.2 container_name: drawio-web hostname: drawio-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - drawio-plantuml - drawio-export networks: - dock-proxy-net - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro drawio-nextai: image: ghcr.io/dayuanjiang/next-ai-draw-io:sha-5007c7b container_name: drawio-nextai hostname: drawio-nextai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - drawio-plantuml - drawio-export - drawio-web networks: - dock-proxy-net - dock-aipriv-net - int-drawio-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-drawio-fonts: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/drawio/fonts networks: dock-proxy-net: name: dock-proxy external: true dock-aipriv-net: name: dock-aipriv external: true int-drawio-net: driver: bridge internal: true ipam: driver: default EOFDI rm -f $HOME/drawio.env cat < $HOME/drawio.env DRAWIO_SELF_CONTAINED=1 PLANTUML_URL=http://drawio-plantuml:8080/ EXPORT_URL=http://drawio-export:8000/ DRAWIO_BASE_URL=https://$SUB_DRAWIO_WEB.$HOMESERVER_DOMAIN NEXT_PUBLIC_DRAWIO_BASE_URL=http://drawio-web:8080 AI_PROVIDER=openai AI_MODEL=reason OPENAI_BASE_URL=http://litellm-proxy:4000/v1 OPENAI_API_KEY=$LITELLM_AUTH_KEY OPENAI_REASONING_EFFORT=low OPENAI_REASONING_SUMMARY=detailed TEMPERATURE=0.3 OLLAMA_ENABLE_THINKING=true EOFDI inner_block="" inner_block=$inner_block">>https://$SUB_DRAWIO_NEXTAI.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://drawio-nextai:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DRAWIO_NEXTAI $MANAGETLS_DRAWIO_NEXTAI "$is_integrate_hshq" insertSubAuthelia $SUB_DRAWIO_NEXTAI.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} insertEnableSvcAll drawio "$FMLNAME_DRAWIO_NEXTAI" $USERTYPE_DRAWIO_NEXTAI "https://$SUB_DRAWIO_NEXTAI.$HOMESERVER_DOMAIN" "nextai-drawio.png" "$(getHeimdallOrderFromSub $SUB_DRAWIO_NEXTAI $USERTYPE_DRAWIO_NEXTAI)" } # Invidious function installInvidious() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory invidious "Invidious" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invidious-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invidious-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invidious-companion) if [ $? -ne 0 ]; then return 1 fi pullImage $IMG_INVIDIOUS_SESSIONGEN if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/invidious mkdir $HSHQ_STACKS_DIR/invidious/db mkdir $HSHQ_STACKS_DIR/invidious/dbexport mkdir $HSHQ_STACKS_DIR/invidious/companion mkdir $HSHQ_STACKS_DIR/invidious/init chmod 777 $HSHQ_STACKS_DIR/invidious/dbexport initServicesCredentials set +e docker run --rm $IMG_INVIDIOUS_SESSIONGEN > $HOME/invidious-session.txt visitorData="$(grep visitor_data $HOME/invidious-session.txt | cut -d":" -f2 | xargs)" poToken="$(grep po_token $HOME/invidious-session.txt | cut -d":" -f2 | xargs)" rm -f $HOME/invidious-session.txt outputConfigInvidious installStack invidious invidious-web "Invidious is ready to lead at" $HOME/invidious.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase Invidious postgres invidious-db $INVIDIOUS_DATABASE_NAME $INVIDIOUS_DATABASE_USER $INVIDIOUS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/invidious/dbexport $INVIDIOUS_DATABASE_READONLYUSER $INVIDIOUS_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_INVIDIOUS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://invidious-web:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_INVIDIOUS $MANAGETLS_INVIDIOUS "$is_integrate_hshq" $NETDEFAULT_INVIDIOUS "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll invidious "$FMLNAME_INVIDIOUS" $USERTYPE_INVIDIOUS "https://$SUB_INVIDIOUS.$HOMESERVER_DOMAIN" "invidious.png" "$(getHeimdallOrderFromSub $SUB_INVIDIOUS $USERTYPE_INVIDIOUS)" restartAllCaddyContainers fi } function outputConfigInvidious() { outputComposeEnvInvidious cat < $HSHQ_STACKS_DIR/invidious/init/init.sql -- Table: public.channels -- DROP TABLE public.channels; CREATE TABLE IF NOT EXISTS public.channels ( id text NOT NULL, author text, updated timestamp with time zone, deleted boolean, subscribed timestamp with time zone, CONSTRAINT channels_id_key UNIQUE (id) ); GRANT ALL ON TABLE public.channels TO current_user; -- Index: public.channels_id_idx -- DROP INDEX public.channels_id_idx; CREATE INDEX IF NOT EXISTS channels_id_idx ON public.channels USING btree (id COLLATE pg_catalog."default"); -- Table: public.videos -- DROP TABLE public.videos; CREATE UNLOGGED TABLE IF NOT EXISTS public.videos ( id text NOT NULL, info text, updated timestamp with time zone, CONSTRAINT videos_pkey PRIMARY KEY (id) ); GRANT ALL ON TABLE public.videos TO current_user; -- Index: public.id_idx -- DROP INDEX public.id_idx; CREATE UNIQUE INDEX IF NOT EXISTS id_idx ON public.videos USING btree (id COLLATE pg_catalog."default"); -- Table: public.channel_videos -- DROP TABLE public.channel_videos; CREATE TABLE IF NOT EXISTS public.channel_videos ( id text NOT NULL, title text, published timestamp with time zone, updated timestamp with time zone, ucid text, author text, length_seconds integer, live_now boolean, premiere_timestamp timestamp with time zone, views bigint, CONSTRAINT channel_videos_id_key UNIQUE (id) ); GRANT ALL ON TABLE public.channel_videos TO current_user; -- Index: public.channel_videos_ucid_idx -- DROP INDEX public.channel_videos_ucid_idx; CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx ON public.channel_videos USING btree (ucid COLLATE pg_catalog."default"); -- Table: public.users -- DROP TABLE public.users; CREATE TABLE IF NOT EXISTS public.users ( updated timestamp with time zone, notifications text[], subscriptions text[], email text NOT NULL, preferences text, password text, token text, watched text[], feed_needs_update boolean, CONSTRAINT users_email_key UNIQUE (email) ); GRANT ALL ON TABLE public.users TO current_user; -- Index: public.email_unique_idx -- DROP INDEX public.email_unique_idx; CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx ON public.users USING btree (lower(email) COLLATE pg_catalog."default"); -- Table: public.session_ids -- DROP TABLE public.session_ids; CREATE TABLE IF NOT EXISTS public.session_ids ( id text NOT NULL, email text, issued timestamp with time zone, CONSTRAINT session_ids_pkey PRIMARY KEY (id) ); GRANT ALL ON TABLE public.session_ids TO current_user; -- Index: public.session_ids_id_idx -- DROP INDEX public.session_ids_id_idx; CREATE INDEX IF NOT EXISTS session_ids_id_idx ON public.session_ids USING btree (id COLLATE pg_catalog."default"); -- Table: public.nonces -- DROP TABLE public.nonces; CREATE TABLE IF NOT EXISTS public.nonces ( nonce text, expire timestamp with time zone, CONSTRAINT nonces_id_key UNIQUE (nonce) ); GRANT ALL ON TABLE public.nonces TO current_user; -- Index: public.nonces_nonce_idx -- DROP INDEX public.nonces_nonce_idx; CREATE INDEX IF NOT EXISTS nonces_nonce_idx ON public.nonces USING btree (nonce COLLATE pg_catalog."default"); -- Table: public.annotations -- DROP TABLE public.annotations; CREATE TABLE IF NOT EXISTS public.annotations ( id text NOT NULL, annotations xml, CONSTRAINT annotations_id_key UNIQUE (id) ); GRANT ALL ON TABLE public.annotations TO current_user; -- Type: public.privacy -- DROP TYPE public.privacy; CREATE TYPE public.privacy AS ENUM ( 'Public', 'Unlisted', 'Private' ); -- Table: public.playlists -- DROP TABLE public.playlists; CREATE TABLE IF NOT EXISTS public.playlists ( title text, id text primary key, author text, description text, video_count integer, created timestamptz, updated timestamptz, privacy privacy, index int8[] ); GRANT ALL ON public.playlists TO current_user; -- Table: public.playlist_videos -- DROP TABLE public.playlist_videos; CREATE TABLE IF NOT EXISTS public.playlist_videos ( title text, id text, author text, ucid text, length_seconds integer, published timestamptz, plid text references playlists(id), index int8, live_now boolean, PRIMARY KEY (index,plid) ); GRANT ALL ON TABLE public.playlist_videos TO current_user; EOFIV } function outputComposeEnvInvidious() { INVIDIOUS_HMAC_KEY=$(pwgen -n 16 1) INVIDIOUS_COMPANION_KEY=$(pwgen -n 16 1) cat < $HOME/invidious-compose.yml $STACK_VERSION_PREFIX invidious $(getScriptStackVersion invidious) services: invidious-db: image: $(getScriptImageByContainerName invidious-db) container_name: invidious-db hostname: invidious-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-invidious-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invidious/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invidious/dbexport:/dbexport - \${PORTAINER_HSHQ_STACKS_DIR}/invidious/init:/docker-entrypoint-initdb.d environment: - POSTGRES_DB=$INVIDIOUS_DATABASE_NAME - POSTGRES_USER=$INVIDIOUS_DATABASE_USER - POSTGRES_PASSWORD=$INVIDIOUS_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.invidious-hourly-db.schedule=@every 1h" - "ofelia.job-exec.invidious-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.invidious-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invidious-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invidious-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invidious-hourly-db.email-from=Invidious Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invidious-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.invidious-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.invidious-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.invidious-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invidious-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invidious-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invidious-monthly-db.email-from=Invidious Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" invidious-web: image: $(getScriptImageByContainerName invidious-web) container_name: invidious-web hostname: invidious-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - invidious-db networks: - int-invidious-net - dock-ext-net environment: INVIDIOUS_CONFIG: | db: dbname: $INVIDIOUS_DATABASE_NAME user: $INVIDIOUS_DATABASE_USER password: $INVIDIOUS_DATABASE_USER_PASSWORD host: invidious-db port: 5432 check_tables: true https_only: true statistics_enabled: false domain: $SUB_INVIDIOUS.$HOMESERVER_DOMAIN visitor_data: '$visitorData' po_token: '$poToken' hmac_key: "$INVIDIOUS_HMAC_KEY" invidious_companion_key: "$INVIDIOUS_COMPANION_KEY" invidious_companion: - private_url: "http://invidious-companion:8282" public_url: "https://$SUB_INVIDIOUS.$HOMESERVER_DOMAIN" invidious-companion: image: $(getScriptImageByContainerName invidious-companion) container_name: invidious-companion hostname: invidious-companion restart: unless-stopped security_opt: - no-new-privileges:true depends_on: - invidious-db networks: - int-invidious-net - dock-ext-net volumes: - v-invidious-companion:/var/tmp/youtubei.js:rw environment: - SERVER_SECRET_KEY=$INVIDIOUS_COMPANION_KEY volumes: v-invidious-companion: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invidious/companion networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-invidious-net: driver: bridge internal: true ipam: driver: default EOFIV cat < $HOME/invidious.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' SERVER_SECRET_KEY=$INVIDIOUS_COMPANION_KEY RUST_LOG=info EOFIV } function performUpdateInvidious() { perform_stack_name=invidious prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=postgres:15.0-bullseye,quay.io/invidious/invidious:latest image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="quay.io/invidious/invidious:latest,quay.io/invidious/invidious:latest" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfInvidiousAddCompanion perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v2 curImageList=postgres:15.0-bullseye,quay.io/invidious/invidious:master,quay.io/invidious/invidious-companion:latest image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="quay.io/invidious/invidious:master,quay.io/invidious/invidious:master" image_update_map[2]="quay.io/invidious/invidious-companion:latest,quay.io/invidious/invidious-companion:latest" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfInvidiousAddCompanion() { mkdir -p $HSHQ_STACKS_DIR/invidious/companion outputComposeEnvInvidious } # Gitea function installGitea() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory gitea "Gitea" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName gitea-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName gitea-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName gitea-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/gitea mkdir $HSHQ_STACKS_DIR/gitea/app mkdir $HSHQ_STACKS_DIR/gitea/db mkdir $HSHQ_STACKS_DIR/gitea/dbexport chmod 777 $HSHQ_STACKS_DIR/gitea/dbexport initServicesCredentials outputConfigGitea set +e installStack gitea gitea-app "Starting new Web server:" $HOME/gitea.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Gitea installed, sleeping 10 seconds..." sleep 10 set +e addUserMailu alias $GITEA_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS docker exec -u $USERID gitea-app gitea admin user create --admin --username $GITEA_ADMIN_USERNAME --password $GITEA_ADMIN_PASSWORD --email $GITEA_ADMIN_EMAIL_ADDRESS docker exec -u $USERID gitea-app gitea admin auth add-ldap --name OpenLDAP --security-protocol ldaps --host ldapserver --port 636 --bind-dn "$LDAP_READONLY_USER_BIND_DN" --bind-password "$LDAP_READONLY_USER_PASSWORD" --user-search-base "ou=people,$LDAP_BASE_DN" --user-filter "(&(&(uid=%s)(objectClass=person))(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))" --admin-filter "(memberOf=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)" --username-attribute uid --firstname-attribute givenName --surname-attribute sn --email-attribute mail --avatar-attribute jpegPhoto --synchronize-users --active addMCPServerLiteLLM "gitea" "gte" "http://gitea-mcp:80/mcp" http none "" addReadOnlyUserToDatabase Gitea postgres gitea-db $GITEA_DATABASE_NAME $GITEA_DATABASE_USER $GITEA_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/gitea/dbexport $GITEA_DATABASE_READONLYUSER $GITEA_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_GITEA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://gitea-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_GITEA $MANAGETLS_GITEA "$is_integrate_hshq" $NETDEFAULT_GITEA "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll gitea "$FMLNAME_GITEA" $USERTYPE_GITEA "https://$SUB_GITEA.$HOMESERVER_DOMAIN" "gitea.png" "$(getHeimdallOrderFromSub $SUB_GITEA $USERTYPE_GITEA)" restartAllCaddyContainers fi } function outputConfigGitea() { cat < $HOME/gitea-compose.yml $STACK_VERSION_PREFIX gitea $(getScriptStackVersion gitea) services: gitea-db: image: $(getScriptImageByContainerName gitea-db) container_name: gitea-db hostname: gitea-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-gitea-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitea/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitea/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.gitea-hourly-db.schedule=@every 1h" - "ofelia.job-exec.gitea-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.gitea-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.gitea-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.gitea-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.gitea-hourly-db.email-from=Gitea Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.gitea-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.gitea-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.gitea-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.gitea-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.gitea-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.gitea-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.gitea-monthly-db.email-from=Gitea Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.gitea-monthly-db.mail-only-on-error=false" gitea-app: image: $(getScriptImageByContainerName gitea-app) container_name: gitea-app hostname: gitea-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - gitea-db networks: - int-gitea-net - dock-ext-net - dock-ldap-net - dock-internalmail-net #ports: # - "2222:22" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitea/app:/data gitea-mcp: image: $(getScriptImageByContainerName gitea-mcp) container_name: gitea-mcp hostname: gitea-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-gitea-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-gitea-net: driver: bridge internal: true ipam: driver: default EOFGL cat < $HOME/gitea.env USER_UID=$USERID USER_GID=$GROUPID POSTGRES_DB=$GITEA_DATABASE_NAME POSTGRES_USER=$GITEA_DATABASE_USER POSTGRES_PASSWORD=$GITEA_DATABASE_USER_PASSWORD DISABLE_QUERY_AUTH_TOKEN=true GITEA__APP_NAME=$HOMESERVER_NAME GITEA__server__ROOT_URL=https://$SUB_GITEA.$HOMESERVER_DOMAIN/ GITEA__database__DB_TYPE=postgres GITEA__database__HOST=gitea-db:5432 GITEA__database__NAME=$GITEA_DATABASE_NAME GITEA__database__USER=$GITEA_DATABASE_USER GITEA__database__PASSWD=$GITEA_DATABASE_USER_PASSWORD GITEA__mailer__ENABLED=true GITEA__mailer__FROM=Gitea $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> GITEA__mailer__PROTOCOL=smtp+starttls GITEA__mailer__SMTP_ADDR=$SMTP_HOSTNAME GITEA__mailer__SMTP_PORT=$SMTP_HOSTPORT GITEA__service__DISABLE_REGISTRATION=true GITEA__security__INSTALL_LOCK=true GITEA__migrations__ALLOWED_HOST_LIST=loopback,private GITEA_BASE_URL=http://gitea-app:3000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFGL } function performUpdateGitea() { perform_stack_name=gitea prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=postgres:15.0-bullseye,gitea/gitea:1.20.3 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitea/gitea:1.20.3,gitea/gitea:1.22.1" ;; 2) newVer=v5 curImageList=postgres:15.0-bullseye,gitea/gitea:1.21.3 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitea/gitea:1.21.3,gitea/gitea:1.22.1" ;; 3) newVer=v5 curImageList=postgres:15.0-bullseye,gitea/gitea:1.21.4 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitea/gitea:1.21.4,gitea/gitea:1.22.1" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,gitea/gitea:1.21.8 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitea/gitea:1.21.8,gitea/gitea:1.22.1" ;; 5) newVer=v7 curImageList=postgres:15.0-bullseye,gitea/gitea:1.22.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitea/gitea:1.22.1,gitea/gitea:1.23.6" ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,gitea/gitea:1.22.3 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gitea/gitea:1.22.3,gitea/gitea:1.23.6" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfGiteaRemoveDeprecatedEnvVars perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,gitea/gitea:1.23.6 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="gitea/gitea:1.23.6,mirror.gcr.io/gitea/gitea:1.24.4" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfGiteaRemoveDeprecatedEnvVars perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 8) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gitea/gitea:1.24.4 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gitea/gitea:1.24.4,mirror.gcr.io/gitea/gitea:1.25.2" ;; 9) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gitea/gitea:1.25.2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gitea/gitea:1.25.2,mirror.gcr.io/gitea/gitea:28.2.0" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateGiteaV10 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 10) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gitea/gitea:28.2.0 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gitea/gitea:28.2.0,mirror.gcr.io/gitea/gitea:28.2.0" image_update_map[2]="ghcr.io/homeserverhq/gitea-mcp:v1.0.0,ghcr.io/homeserverhq/gitea-mcp:v1.0.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfGiteaRemoveDeprecatedEnvVars() { set +e sed -i "/GITEA__mailer__MAILER_TYPE=/d" $HOME/gitea.env sed -i "/GITEA__mailer__HOST=/d" $HOME/gitea.env sed -i "/GITEA__mailer__MAILER_TYPE=/d" $HOME/gitea.env grep "GITEA__mailer__PROTOCOL=" $HOME/gitea.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "GITEA__mailer__PROTOCOL=smtp+starttls" >> $HOME/gitea.env fi } function mfUpdateGiteaV10() { pullImage $(getScriptImageByContainerName gitea-mcp) if [ $? -ne 0 ]; then return 1 fi cat < $HOME/gitea-compose.yml $STACK_VERSION_PREFIX gitea v10 services: gitea-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: gitea-db hostname: gitea-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-gitea-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitea/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitea/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.gitea-hourly-db.schedule=@every 1h" - "ofelia.job-exec.gitea-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.gitea-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.gitea-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.gitea-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.gitea-hourly-db.email-from=Gitea Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.gitea-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.gitea-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.gitea-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.gitea-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.gitea-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.gitea-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.gitea-monthly-db.email-from=Gitea Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.gitea-monthly-db.mail-only-on-error=false" gitea-app: image: mirror.gcr.io/gitea/gitea:28.2.0 container_name: gitea-app hostname: gitea-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - gitea-db networks: - int-gitea-net - dock-ext-net - dock-ldap-net - dock-internalmail-net #ports: # - "2222:22" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/gitea/app:/data gitea-mcp: image: ghcr.io/homeserverhq/gitea-mcp:v1.0.0 container_name: gitea-mcp hostname: gitea-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-gitea-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-gitea-net: driver: bridge internal: true ipam: driver: default EOFGL tee -a $HOME/gitea.env >/dev/null <>https://$SUB_MEALIE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://mealie-app:9000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MEALIE $MANAGETLS_MEALIE "$is_integrate_hshq" $NETDEFAULT_MEALIE "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll mealie "$FMLNAME_MEALIE" $USERTYPE_MEALIE "https://$SUB_MEALIE.$HOMESERVER_DOMAIN" "mealie.png" "$(getHeimdallOrderFromSub $SUB_MEALIE $USERTYPE_MEALIE)" restartAllCaddyContainers fi } function outputConfigMealie() { cat < $HOME/mealie-compose.yml $STACK_VERSION_PREFIX mealie $(getScriptStackVersion mealie) services: mealie-db: image: $(getScriptImageByContainerName mealie-db) container_name: mealie-db hostname: mealie-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-mealie-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mealie/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mealie/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.mealie-hourly-db.schedule=@every 1h" - "ofelia.job-exec.mealie-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.mealie-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.mealie-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.mealie-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.mealie-hourly-db.email-from=Mealie Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.mealie-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.mealie-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.mealie-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.mealie-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.mealie-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.mealie-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.mealie-monthly-db.email-from=Mealie Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.mealie-monthly-db.mail-only-on-error=false" mealie-app: image: $(getScriptImageByContainerName mealie-app) container_name: mealie-app hostname: mealie-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - mealie-db networks: - dock-ext-net - dock-ldap-net - dock-internalmail-net - dock-aipriv-net - int-mealie-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-mealie-app:/app/data mealie-mcp: image: $(getScriptImageByContainerName mealie-mcp) container_name: mealie-mcp hostname: mealie-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - dock-aipriv-net - int-mealie-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-mealie-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/mealie/app networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true int-mealie-net: driver: bridge internal: true ipam: driver: default EOFGL cat < $HOME/mealie.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID DEFAULT_GROUP=Home PRODUCTION=True API_DOCS=True ALLOW_SIGNUP=false BASE_URL=https://$SUB_MEALIE.$HOMESERVER_DOMAIN DB_ENGINE=postgres POSTGRES_USER=$MEALIE_DATABASE_USER POSTGRES_PASSWORD=$MEALIE_DATABASE_USER_PASSWORD POSTGRES_SERVER=mealie-db POSTGRES_PORT=5432 POSTGRES_DB=$MEALIE_DATABASE_NAME TOKEN_TIME=168 SMTP_HOST=$SMTP_HOSTNAME SMTP_PORT=$MAILU_PORT_5 SMTP_FROM_NAME=Mealie $(getAdminEmailName) SMTP_AUTH_STRATEGY=TLS SMTP_FROM_EMAIL=$EMAIL_SMTP_EMAIL_ADDRESS SMTP_USER=$EMAIL_SMTP_EMAIL_ADDRESS SMTP_PASSWORD=$EMAIL_SMTP_PASSWORD LDAP_AUTH_ENABLED=True LDAP_SERVER_URL=$LDAP_URI LDAP_TLS_INSECURE=False LDAP_TLS_CACERTFILE=/usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt LDAP_ENABLE_STARTTLS=True LDAP_BASE_DN=$LDAP_BASE_DN LDAP_QUERY_BIND=$LDAP_READONLY_USER_BIND_DN LDAP_QUERY_PASSWORD=$LDAP_READONLY_USER_PASSWORD LDAP_USER_FILTER=(&(|({id_attribute}={input})({mail_attribute}={input}))(objectClass=person)(|(memberOf=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))) LDAP_ADMIN_FILTER=(memberOf=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN) LDAP_ID_ATTRIBUTE=uid LDAP_NAME_ATTRIBUTE=cn LDAP_MAIL_ATTRIBUTE=mail OPENAI_API_KEY=$LITELLM_AUTH_KEY OPENAI_MODEL=Vision OPENAI_BASE_URL=http://litellm-proxy:4000/v1 OPENAI_ENABLE_IMAGE_SERVICES=True OPENAI_SEND_DATABASE_DATA=True MEALIE_BASE_URL=http://mealie-app:9000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false MEALIE_PUBLIC_URL=https://$SUB_MEALIE.$HOMESERVER_DOMAIN EOFGL } function addAdminUserMealie() { pwHash=$(htpasswd -bnBC 10 "" $MEALIE_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2b/' | sed 's/\$/\\\$/g') docker exec mealie-db bash -c "PGPASSWORD=$MEALIE_DATABASE_USER_PASSWORD echo \"update users set full_name='$HOMESERVER_NAME Mealie Admin', username='$MEALIE_ADMIN_USERNAME', email='$MEALIE_ADMIN_EMAIL_ADDRESS', password='$pwHash', admin=true where username='admin';\" | psql -U $MEALIE_DATABASE_USER $MEALIE_DATABASE_NAME" MEALIE_ADMIN_API_KEY=$(docker exec -i -e USERNAME="$MEALIE_ADMIN_USERNAME" -e EMAIL="$MEALIE_ADMIN_EMAIL_ADDRESS" -e ADMIN="true" -e FULLNAME="Mealie $(getAdminEmailName)" mealie-app python - <<'PYEOF' import os import sys from datetime import timedelta from sqlalchemy import select from mealie.core.security import create_access_token from mealie.db.db_setup import session_context from mealie.db.models.users.users import AuthMethod, LongLiveToken from mealie.repos.all_repositories import get_repositories username = os.environ["USERNAME"] email = os.environ["EMAIL"] admin = os.environ.get("ADMIN", "false").lower() in ("1", "true", "yes", "y") full_name = os.environ["FULLNAME"] with session_context() as session: repos = get_repositories(session, group_id=None, household_id=None) user = repos.users.get_by_username(username) token = create_access_token( {"long_token": True, "id": str(user.id), "name": "MCP", "integration_id": "generic"}, timedelta(days=32120), ) session.add(LongLiveToken(name="MCP", token=token, user_id=user.id)) session.commit() print(token) PYEOF ) updateConfigVar MEALIE_ADMIN_API_KEY "$MEALIE_ADMIN_API_KEY" } function performUpdateMealie() { perform_stack_name=mealie prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=hkotel/mealie:v0.5.6 image_update_map[0]="hkotel/mealie:v0.5.6,hkotel/mealie:v0.5.6" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Mealie cannot be upgraded to the next version. You must export your data/recipes from this instance, uninstall and reinstall mealie, then import your data/recipes into the new instance. See https://nightly.mealie.io/documentation/getting-started/migrating-to-mealie-v1/ for migration instructions." return ;; 2) newVer=v4 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v1.0.0-RC2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v1.0.0-RC2,ghcr.io/mealie-recipes/mealie:v1.3.2" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v1.1.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v1.1.0,ghcr.io/mealie-recipes/mealie:v1.3.2" ;; 4) newVer=v6 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v1.3.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v1.3.2,ghcr.io/mealie-recipes/mealie:v1.12.0" ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v1.11.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v1.11.0,ghcr.io/mealie-recipes/mealie:v1.12.0" ;; 6) newVer=v7 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v1.12.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v1.12.0,ghcr.io/mealie-recipes/mealie:v2.1.0" ;; 7) newVer=v8 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v2.1.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v2.1.0,ghcr.io/mealie-recipes/mealie:v2.8.0" ;; 8) newVer=v9 curImageList=postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v2.8.0 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v2.8.0,ghcr.io/mealie-recipes/mealie:v3.0.2" ;; 9) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v3.0.2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v3.0.2,ghcr.io/mealie-recipes/mealie:v3.6.1" ;; 10) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v3.6.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v3.6.1,ghcr.io/mealie-recipes/mealie:v3.20.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfMealieV11Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 11) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/mealie-recipes/mealie:v3.20.1,ghcr.io/homeserverhq/mealie-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/mealie-recipes/mealie:v3.20.1,ghcr.io/mealie-recipes/mealie:v3.20.1" image_update_map[2]="ghcr.io/homeserverhq/mealie-mcp:v2,ghcr.io/homeserverhq/mealie-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfMealieV11Update() { cat < $HOME/mealie-compose.yml $STACK_VERSION_PREFIX mealie v11 services: mealie-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: mealie-db hostname: mealie-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-mealie-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mealie/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/mealie/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.mealie-hourly-db.schedule=@every 1h" - "ofelia.job-exec.mealie-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.mealie-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.mealie-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.mealie-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.mealie-hourly-db.email-from=Mealie Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.mealie-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.mealie-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.mealie-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.mealie-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.mealie-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.mealie-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.mealie-monthly-db.email-from=Mealie Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.mealie-monthly-db.mail-only-on-error=false" mealie-app: image: ghcr.io/mealie-recipes/mealie:v3.20.1 container_name: mealie-app hostname: mealie-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - mealie-db networks: - dock-ext-net - dock-ldap-net - dock-internalmail-net - dock-aipriv-net - int-mealie-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-mealie-app:/app/data mealie-mcp: image: ghcr.io/homeserverhq/mealie-mcp:v2 container_name: mealie-mcp hostname: mealie-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - dock-aipriv-net - int-mealie-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-mealie-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/mealie/app networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true int-mealie-net: driver: bridge internal: true ipam: driver: default EOFGL cat < $HOME/mealie.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID DEFAULT_GROUP=Home PRODUCTION=True API_DOCS=True ALLOW_SIGNUP=false BASE_URL=https://$SUB_MEALIE.$HOMESERVER_DOMAIN DB_ENGINE=postgres POSTGRES_USER=$MEALIE_DATABASE_USER POSTGRES_PASSWORD=$MEALIE_DATABASE_USER_PASSWORD POSTGRES_SERVER=mealie-db POSTGRES_PORT=5432 POSTGRES_DB=$MEALIE_DATABASE_NAME TOKEN_TIME=168 SMTP_HOST=$SMTP_HOSTNAME SMTP_PORT=$MAILU_PORT_5 SMTP_FROM_NAME=Mealie $(getAdminEmailName) SMTP_AUTH_STRATEGY=TLS SMTP_FROM_EMAIL=$EMAIL_SMTP_EMAIL_ADDRESS SMTP_USER=$EMAIL_SMTP_EMAIL_ADDRESS SMTP_PASSWORD=$EMAIL_SMTP_PASSWORD LDAP_AUTH_ENABLED=True LDAP_SERVER_URL=$LDAP_URI LDAP_TLS_INSECURE=False LDAP_TLS_CACERTFILE=/usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt LDAP_ENABLE_STARTTLS=True LDAP_BASE_DN=$LDAP_BASE_DN LDAP_QUERY_BIND=$LDAP_READONLY_USER_BIND_DN LDAP_QUERY_PASSWORD=$LDAP_READONLY_USER_PASSWORD LDAP_USER_FILTER=(&(|({id_attribute}={input})({mail_attribute}={input}))(objectClass=person)(|(memberOf=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))) LDAP_ADMIN_FILTER=(memberOf=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN) LDAP_ID_ATTRIBUTE=uid LDAP_NAME_ATTRIBUTE=cn LDAP_MAIL_ATTRIBUTE=mail OPENAI_API_KEY=$LITELLM_AUTH_KEY OPENAI_MODEL=Vision OPENAI_BASE_URL=http://litellm-proxy:4000/v1 OPENAI_ENABLE_IMAGE_SERVICES=True OPENAI_SEND_DATABASE_DATA=True MEALIE_BASE_URL=http://mealie-app:9000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false MEALIE_PUBLIC_URL=https://$SUB_MEALIE.$HOMESERVER_DOMAIN EOFGL addMCPServerLiteLLM "mealie" "mle" "http://mealie-mcp:80/mcp" http none "" } # Kasm function installKasm() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory kasm "Kasm" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName kasm) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/kasm mkdir $HSHQ_STACKS_DIR/kasm/profiles mkdir $HSHQ_NONBACKUP_DIR/kasm mkdir $HSHQ_NONBACKUP_DIR/kasm/data initServicesCredentials outputConfigKasm installStack kasm kasm "" $HOME/kasm.env retval=$? if [ $retval -ne 0 ]; then return $retval fi if ! [ "$KASM_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_KASM Admin Login Info" -b "$FMLNAME_KASM Admin Email Address: $KASM_ADMIN_EMAIL_ADDRESS\n$FMLNAME_KASM Admin Password: $KASM_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" KASM_INIT_ENV=true updateConfigVar KASM_INIT_ENV $KASM_INIT_ENV fi inner_block="" inner_block=$inner_block">>https://$SUB_KASM.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://kasm {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>transport http {\n" inner_block=$inner_block">>>>>>>>>>tls\n" inner_block=$inner_block">>>>>>>>>>tls_insecure_skip_verify\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KASM $MANAGETLS_KASM "$is_integrate_hshq" $NETDEFAULT_KASM "$inner_block" inner_block="" inner_block=$inner_block">>https://$SUB_KASM_WIZARD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://kasm:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>transport http {\n" inner_block=$inner_block">>>>>>>>>>tls\n" inner_block=$inner_block">>>>>>>>>>tls_insecure_skip_verify\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KASM_WIZARD $MANAGETLS_KASM_WIZARD "$is_integrate_hshq" $NETDEFAULT_KASM_WIZARD "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcHeimdall kasm "$FMLNAME_KASM" $USERTYPE_KASM "https://$SUB_KASM.$HOMESERVER_DOMAIN" "kasm.png" true "$(getHeimdallOrderFromSub $SUB_KASM $USERTYPE_KASM)" insertEnableSvcHeimdall kasm "$FMLNAME_KASM_WIZARD" $USERTYPE_KASM_WIZARD "https://$SUB_KASM_WIZARD.$HOMESERVER_DOMAIN" "kasm.png" true "$(getHeimdallOrderFromSub $SUB_KASM_WIZARD $USERTYPE_KASM_WIZARD)" restartAllCaddyContainers fi } function outputConfigKasm() { cat < $HOME/kasm-compose.yml $STACK_VERSION_PREFIX kasm $(getScriptStackVersion kasm) services: kasm: image: $(getScriptImageByContainerName kasm) container_name: kasm privileged: true hostname: kasm restart: unless-stopped env_file: stack.env networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/kasm/data:/opt - \${PORTAINER_HSHQ_STACKS_DIR}/kasm/profiles:/profiles networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFGL cat < $HOME/kasm.env KASM_PORT=443 EOFGL } function performUpdateKasm() { perform_stack_name=kasm prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=lscr.io/linuxserver/kasm:1.13.0 image_update_map[0]="lscr.io/linuxserver/kasm:1.13.0,lscr.io/linuxserver/kasm:1.15.0-ls29" ;; 2) newVer=v4 curImageList=lscr.io/linuxserver/kasm:1.14.0 image_update_map[0]="lscr.io/linuxserver/kasm:1.14.0,lscr.io/linuxserver/kasm:1.15.0-ls29" ;; 3) newVer=v6 curImageList=lscr.io/linuxserver/kasm:1.15.0 image_update_map[0]="lscr.io/linuxserver/kasm:1.15.0,lscr.io/linuxserver/kasm:1.16.1-ls68" ;; 4) newVer=v6 curImageList=lscr.io/linuxserver/kasm:1.15.0-ls29 image_update_map[0]="lscr.io/linuxserver/kasm:1.15.0-ls29,lscr.io/linuxserver/kasm:1.16.1-ls68" ;; 5) newVer=v6 curImageList=lscr.io/linuxserver/kasm:1.16.0-ls44 image_update_map[0]="lscr.io/linuxserver/kasm:1.16.0-ls44,lscr.io/linuxserver/kasm:1.16.1-ls68" ;; 6) newVer=v8 curImageList=lscr.io/linuxserver/kasm:1.16.1-ls68 image_update_map[0]="lscr.io/linuxserver/kasm:1.16.1-ls68,mirror.gcr.io/linuxserver/kasm:1.18.0-ls108" ;; 7) newVer=v8 curImageList=lscr.io/linuxserver/kasm:1.17.0-ls88 image_update_map[0]="lscr.io/linuxserver/kasm:1.17.0-ls88,mirror.gcr.io/linuxserver/kasm:1.18.0-ls108" ;; 8) newVer=v8 curImageList=mirror.gcr.io/linuxserver/kasm:1.18.0-ls108 image_update_map[0]="mirror.gcr.io/linuxserver/kasm:1.18.0-ls108,mirror.gcr.io/linuxserver/kasm:1.18.0-ls108" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # NTFY function installNTFY() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory ntfy "NTFY" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ntfy) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/ntfy mkdir $HSHQ_STACKS_DIR/ntfy/cache mkdir $HSHQ_STACKS_DIR/ntfy/etc outputConfigNTFY installStack ntfy ntfy "" $HOME/ntfy.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 inner_block="" inner_block=$inner_block">>https://$SUB_NTFY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://ntfy {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NTFY $MANAGETLS_NTFY "$is_integrate_hshq" $NETDEFAULT_NTFY "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll ntfy "$FMLNAME_NTFY" $USERTYPE_NTFY "https://$SUB_NTFY.$HOMESERVER_DOMAIN" "ntfy.png" "$(getHeimdallOrderFromSub $SUB_NTFY $USERTYPE_NTFY)" restartAllCaddyContainers fi } function outputConfigNTFY() { cat < $HOME/ntfy-compose.yml $STACK_VERSION_PREFIX ntfy $(getScriptStackVersion ntfy) services: ntfy: image: $(getScriptImageByContainerName ntfy) container_name: ntfy hostname: ntfy restart: unless-stopped env_file: stack.env user: "\${PORTAINER_UID}:\${PORTAINER_GID}" security_opt: - no-new-privileges:true command: - serve networks: - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ntfy/cache:/var/cache/ntfy - \${PORTAINER_HSHQ_STACKS_DIR}/ntfy/etc:/etc/ntfy - \${PORTAINER_HSHQ_STACKS_DIR}/ntfy/etc/server.yml:/etc/ntfy/server.yml networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true EOFNT cat < $HOME/ntfy.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID EOFNT cat < $HSHQ_STACKS_DIR/ntfy/etc/server.yml # ntfy server config file # # Please refer to the documentation at https://ntfy.sh/docs/config/ for details. # All options also support underscores (_) instead of dashes (-) to comply with the YAML spec. # Public facing base URL of the service (e.g. https://ntfy.sh or https://ntfy.example.com) # # This setting is required for any of the following features: # - attachments (to return a download URL) # - e-mail sending (for the topic URL in the email footer) # - iOS push notifications for self-hosted servers (to calculate the Firebase poll_request topic) # - Matrix Push Gateway (to validate that the pushkey is correct) # base-url: https://$SUB_NTFY.$HOMESERVER_DOMAIN # Listen address for the HTTP & HTTPS web server. If "listen-https" is set, you must also # set "key-file" and "cert-file". Format: []:, e.g. "1.2.3.4:8080". # # To listen on all interfaces, you may omit the IP address, e.g. ":443". # To disable HTTP, set "listen-http" to "-". # # listen-http: ":80" # listen-https: # Listen on a Unix socket, e.g. /var/lib/ntfy/ntfy.sock # This can be useful to avoid port issues on local systems, and to simplify permissions. # # listen-unix: # listen-unix-mode: # Path to the private key & cert file for the HTTPS web server. Not used if "listen-https" is not set. # # key-file: # cert-file: # If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. # This is optional and only required to save battery when using the Android app. # # firebase-key-file: # If "cache-file" is set, messages are cached in a local SQLite database instead of only in-memory. # This allows for service restarts without losing messages in support of the since= parameter. # # The "cache-duration" parameter defines the duration for which messages will be buffered # before they are deleted. This is required to support the "since=..." and "poll=1" parameter. # To disable the cache entirely (on-disk/in-memory), set "cache-duration" to 0. # The cache file is created automatically, provided that the correct permissions are set. # # The "cache-startup-queries" parameter allows you to run commands when the database is initialized, # e.g. to enable WAL mode (see https://phiresky.github.io/blog/2020/sqlite-performance-tuning/)). # Example: # cache-startup-queries: | # pragma journal_mode = WAL; # pragma synchronous = normal; # pragma temp_store = memory; # pragma busy_timeout = 15000; # vacuum; # # The "cache-batch-size" and "cache-batch-timeout" parameter allow enabling async batch writing # of messages. If set, messages will be queued and written to the database in batches of the given # size, or after the given timeout. This is only required for high volume servers. # # Debian/RPM package users: # Use /var/cache/ntfy/cache.db as cache file to avoid permission issues. The package # creates this folder for you. # # Check your permissions: # If you are running ntfy with systemd, make sure this cache file is owned by the # ntfy user and group by running: chown ntfy.ntfy . # cache-file: /var/cache/ntfy/cache.db # cache-duration: "12h" # cache-startup-queries: # cache-batch-size: 0 # cache-batch-timeout: "0ms" # If set, access to the ntfy server and API can be controlled on a granular level using # the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs. # # - auth-file is the SQLite user/access database; it is created automatically if it doesn't already exist # - auth-default-access defines the default/fallback access if no access control entry is found; it can be # set to "read-write" (default), "read-only", "write-only" or "deny-all". # - auth-startup-queries allows you to run commands when the database is initialized, e.g. to enable # WAL mode. This is similar to cache-startup-queries. See above for details. # # Debian/RPM package users: # Use /var/lib/ntfy/user.db as user database to avoid permission issues. The package # creates this folder for you. # # Check your permissions: # If you are running ntfy with systemd, make sure this user database file is owned by the # ntfy user and group by running: chown ntfy.ntfy . # # auth-file: # auth-default-access: "read-write" # auth-startup-queries: # If set, the X-Forwarded-For header is used to determine the visitor IP address # instead of the remote address of the connection. # # WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate limited # as if they are one. # behind-proxy: true # If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments # are "attachment-cache-dir" and "base-url". # # - attachment-cache-dir is the cache directory for attached files # - attachment-total-size-limit is the limit of the on-disk attachment cache directory (total size) # - attachment-file-size-limit is the per-file attachment size limit (e.g. 300k, 2M, 100M) # - attachment-expiry-duration is the duration after which uploaded attachments will be deleted (e.g. 3h, 20h) # # attachment-cache-dir: # attachment-total-size-limit: "5G" # attachment-file-size-limit: "15M" # attachment-expiry-duration: "3h" # If enabled, allow outgoing e-mail notifications via the 'X-Email' header. If this header is set, # messages will additionally be sent out as e-mail using an external SMTP server. # # As of today, only SMTP servers with plain text auth (or no auth at all), and STARTLS are supported. # Please also refer to the rate limiting settings below (visitor-email-limit-burst & visitor-email-limit-burst). # # - smtp-sender-addr is the hostname:port of the SMTP server # - smtp-sender-from is the e-mail address of the sender # - smtp-sender-user/smtp-sender-pass are the username and password of the SMTP user (leave blank for no auth) # smtp-sender-addr: $SMTP_HOSTNAME:$SMTP_HOSTPORT smtp-sender-from: $EMAIL_ADMIN_EMAIL_ADDRESS # smtp-sender-user: # smtp-sender-pass: # If enabled, ntfy will launch a lightweight SMTP server for incoming messages. Once configured, users can send # emails to a topic e-mail address to publish messages to a topic. # # - smtp-server-listen defines the IP address and port the SMTP server will listen on, e.g. :25 or 1.2.3.4:25 # - smtp-server-domain is the e-mail domain, e.g. ntfy.sh # - smtp-server-addr-prefix is an optional prefix for the e-mail addresses to prevent spam. If set to "ntfy-", # for instance, only e-mails to ntfy-$topic@ntfy.sh will be accepted. If this is not set, all emails to # $topic@ntfy.sh will be accepted (which may obviously be a spam problem). # # smtp-server-listen: # smtp-server-domain: # smtp-server-addr-prefix: # Web Push support (background notifications for browsers) # # If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users # can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push # endpoint, which will then forward it to the browser. # # You must configure web-push-public/private key, web-push-file, and web-push-email-address below to enable Web Push. # Run "ntfy webpush keys" to generate the keys. # # - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 # - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 # - web-push-file is a database file to keep track of browser subscription endpoints, e.g. /var/cache/ntfy/webpush.db # - web-push-email-address is the admin email address send to the push provider, e.g. sysadmin@example.com # - web-push-startup-queries is an optional list of queries to run on startup # # web-push-public-key: # web-push-private-key: # web-push-file: # web-push-email-address: # web-push-startup-queries: # If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header. # # - twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586 # - twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586 # - twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586 # - twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 # # twilio-account: # twilio-auth-token: # twilio-phone-number: # twilio-verify-service: # Interval in which keepalive messages are sent to the client. This is to prevent # intermediaries closing the connection for inactivity. # # Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. # # keepalive-interval: "45s" # Interval in which the manager prunes old messages, deletes topics # and prints the stats. # # manager-interval: "1m" # Defines topic names that are not allowed, because they are otherwise used. There are a few default topics # that cannot be used (e.g. app, account, settings, ...). To extend the default list, define them here. # # Example: # disallowed-topics: # - about # - pricing # - contact # # disallowed-topics: # Defines the root path of the web app, or disables the web app entirely. # # Can be any simple path, e.g. "/", "/app", or "/ntfy". For backwards-compatibility reasons, # the values "app" (maps to "/"), "home" (maps to "/app"), or "disable" (maps to "") to disable # the web app entirely. # # web-root: / # Various feature flags used to control the web app, and API access, mainly around user and # account management. # # - enable-signup allows users to sign up via the web app, or API # - enable-login allows users to log in via the web app, or API # - enable-reservations allows users to reserve topics (if their tier allows it) # # enable-signup: false # enable-login: false # enable-reservations: false # Server URL of a Firebase/APNS-connected ntfy server (likely "https://ntfy.sh"). # # iOS users: # If you use the iOS ntfy app, you MUST configure this to receive timely notifications. You'll like want this: # upstream-base-url: "https://ntfy.sh" # # If set, all incoming messages will publish a "poll_request" message to the configured upstream server, containing # the message ID of the original message, instructing the iOS app to poll this server for the actual message contents. # This is to prevent the upstream server and Firebase/APNS from being able to read the message. # # - upstream-base-url is the base URL of the upstream server. Should be "https://ntfy.sh". # - upstream-access-token is the token used to authenticate with the upstream server. This is only required # if you exceed the upstream rate limits, or the uptream server requires authentication. # upstream-base-url: "https://ntfy.sh" # upstream-access-token: # Rate limiting: Total number of topics before the server rejects new topics. # # global-topic-limit: 15000 # Rate limiting: Number of subscriptions per visitor (IP address) # # visitor-subscription-limit: 30 # Rate limiting: Allowed GET/PUT/POST requests per second, per visitor: # - visitor-request-limit-burst is the initial bucket of requests each visitor has # - visitor-request-limit-replenish is the rate at which the bucket is refilled # - visitor-request-limit-exempt-hosts is a comma-separated list of hostnames, IPs or CIDRs to be # exempt from request rate limiting. Hostnames are resolved at the time the server is started. # Example: "1.2.3.4,ntfy.example.com,8.7.6.0/24" # # visitor-request-limit-burst: 60 # visitor-request-limit-replenish: "5s" # visitor-request-limit-exempt-hosts: "" # Rate limiting: Hard daily limit of messages per visitor and day. The limit is reset # every day at midnight UTC. If the limit is not set (or set to zero), the request # limit (see above) governs the upper limit. # # visitor-message-daily-limit: 0 # Rate limiting: Allowed emails per visitor: # - visitor-email-limit-burst is the initial bucket of emails each visitor has # - visitor-email-limit-replenish is the rate at which the bucket is refilled # # visitor-email-limit-burst: 16 # visitor-email-limit-replenish: "1h" # Rate limiting: Attachment size and bandwidth limits per visitor: # - visitor-attachment-total-size-limit is the total storage limit used for attachments per visitor # - visitor-attachment-daily-bandwidth-limit is the total daily attachment download/upload traffic limit per visitor # # visitor-attachment-total-size-limit: "100M" # visitor-attachment-daily-bandwidth-limit: "500M" # Rate limiting: Enable subscriber-based rate limiting (mostly used for UnifiedPush) # # If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed # to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume # publishers (e.g. Matrix/Mastodon servers) are allowed to send. # # Once enabled, a client may send a "Rate-Topics: ,,..." header when subscribing to topics via # HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits # to use when publishing on this topic. Note: Setting the rate visitor requires READ-WRITE permission on the topic. # # UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if # no "rate visitor" has been previously registered. This is to avoid burning the publisher's "visitor-message-daily-limit". # # visitor-subscriber-rate-limiting: false # Payments integration via Stripe # # - stripe-secret-key is the key used for the Stripe API communication. Setting this values # enables payments in the ntfy web app (e.g. Upgrade dialog). See https://dashboard.stripe.com/apikeys. # - stripe-webhook-key is the key required to validate the authenticity of incoming webhooks from Stripe. # Webhooks are essential up keep the local database in sync with the payment provider. See https://dashboard.stripe.com/webhooks. # - billing-contact is an email address or website displayed in the "Upgrade tier" dialog to let people reach # out with billing questions. If unset, nothing will be displayed. # # stripe-secret-key: # stripe-webhook-key: # billing-contact: # Metrics # # ntfy can expose Prometheus-style metrics via a /metrics endpoint, or on a dedicated listen IP/port. # Metrics may be considered sensitive information, so before you enable them, be sure you know what you are # doing, and/or secure access to the endpoint in your reverse proxy. # # - enable-metrics enables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket) # - metrics-listen-http exposes the metrics endpoint via a dedicated [IP]:port. If set, this option implicitly # enables metrics as well, e.g. "10.0.1.1:9090" or ":9090" # # enable-metrics: false # metrics-listen-http: # Profiling # # ntfy can expose Go's net/http/pprof endpoints to support profiling of the ntfy server. If enabled, ntfy will listen # on a dedicated listen IP/port, which can be accessed via the web browser on http://:/debug/pprof/. # This can be helpful to expose bottlenecks, and visualize call flows. See https://pkg.go.dev/net/http/pprof for details. # # profile-listen-http: # Logging options # # By default, ntfy logs to the console (stderr), with an "info" log level, and in a human-readable text format. # ntfy supports five different log levels, can also write to a file, log as JSON, and even supports granular # log level overrides for easier debugging. Some options (log-level and log-level-overrides) can be hot reloaded # by calling "kill -HUP $pid" or "systemctl reload ntfy". # # - log-format defines the output format, can be "text" (default) or "json" # - log-file is a filename to write logs to. If this is not set, ntfy logs to stderr. # - log-level defines the default log level, can be one of "trace", "debug", "info" (default), "warn" or "error". # Be aware that "debug" (and particularly "trace") can be VERY CHATTY. Only turn them on briefly for debugging purposes. # - log-level-overrides lets you override the log level if certain fields match. This is incredibly powerful # for debugging certain parts of the system (e.g. only the account management, or only a certain visitor). # This is an array of strings in the format: # - "field=value -> level" to match a value exactly, e.g. "tag=manager -> trace" # - "field -> level" to match any value, e.g. "time_taken_ms -> debug" # Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging. # # Check your permissions: # If you are running ntfy with systemd, make sure this log file is owned by the # ntfy user and group by running: chown ntfy.ntfy . # # Example (good for production): # log-level: info # log-format: json # log-file: /var/log/ntfy.log # # Example level overrides (for debugging, only use temporarily): # log-level-overrides: # - "tag=manager -> trace" # - "visitor_ip=1.2.3.4 -> debug" # - "time_taken_ms -> debug" # # log-level: info # log-level-overrides: # log-format: text # log-file: EOFNT } function performUpdateNTFY() { perform_stack_name=ntfy prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=binwiederhier/ntfy:v2.7.0 image_update_map[0]="binwiederhier/ntfy:v2.7.0,mirror.gcr.io/binwiederhier/ntfy:v2.14.0" ;; 2) newVer=v5 curImageList=binwiederhier/ntfy:v2.8.0 image_update_map[0]="binwiederhier/ntfy:v2.8.0,mirror.gcr.io/binwiederhier/ntfy:v2.14.0" ;; 3) newVer=v5 curImageList=binwiederhier/ntfy:v2.9.0 image_update_map[0]="binwiederhier/ntfy:v2.9.0,mirror.gcr.io/binwiederhier/ntfy:v2.14.0" ;; 4) newVer=v5 curImageList=binwiederhier/ntfy:v2.11.0 image_update_map[0]="binwiederhier/ntfy:v2.11.0,mirror.gcr.io/binwiederhier/ntfy:v2.14.0" ;; 5) newVer=v6 curImageList=mirror.gcr.io/binwiederhier/ntfy:v2.14.0 image_update_map[0]="mirror.gcr.io/binwiederhier/ntfy:v2.14.0,mirror.gcr.io/binwiederhier/ntfy:v2.15.0" ;; 6) newVer=v6 curImageList=mirror.gcr.io/binwiederhier/ntfy:v2.15.0 image_update_map[0]="mirror.gcr.io/binwiederhier/ntfy:v2.15.0,mirror.gcr.io/binwiederhier/ntfy:v2.15.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # ITTools function installITTools() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory ittools "ITTools" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ittools) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/ittools outputConfigITTools installStack ittools ittools "" $HOME/ittools.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 inner_block="" inner_block=$inner_block">>https://$SUB_ITTOOLS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://ittools {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ITTOOLS $MANAGETLS_ITTOOLS "$is_integrate_hshq" $NETDEFAULT_ITTOOLS "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll ittools "$FMLNAME_ITTOOLS" $USERTYPE_ITTOOLS "https://$SUB_ITTOOLS.$HOMESERVER_DOMAIN" "ittools.png" "$(getHeimdallOrderFromSub $SUB_ITTOOLS $USERTYPE_ITTOOLS)" restartAllCaddyContainers fi } function outputConfigITTools() { cat < $HOME/ittools-compose.yml $STACK_VERSION_PREFIX ittools $(getScriptStackVersion ittools) services: ittools: image: $(getScriptImageByContainerName ittools) container_name: ittools hostname: ittools restart: unless-stopped security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true EOFNT cat < $HOME/ittools.env UID=$USERID GID=$GROUPID EOFNT } function performUpdateITTools() { perform_stack_name=ittools prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=ghcr.io/corentinth/it-tools:latest image_update_map[0]="ghcr.io/corentinth/it-tools:latest,ghcr.io/corentinth/it-tools:latest" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Remotely function installRemotely() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory remotely "Remotely" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName remotely) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/remotely initServicesCredentials addUserMailu alias $REMOTELY_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$REMOTELY_INIT_ENV" = "true" ]; then sendEmail -s "Remotely Admin Login Info" -b "Remotely Admin Username: $REMOTELY_ADMIN_EMAIL_ADDRESS\nRemotely Admin Password: $REMOTELY_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" REMOTELY_INIT_ENV=true updateConfigVar REMOTELY_INIT_ENV $REMOTELY_INIT_ENV fi outputConfigRemotely installStack remotely remotely "No XML encryptor configured" $HOME/remotely.env retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Sleeping 10 seconds..." sleep 10 startStopStack remotely stop sudo sqlite3 $HSHQ_STACKS_DIR/remotely/Remotely.db "update KeyValueRecords set Value='{\"AllowApiLogin\":false,\"BannedDevices\":[],\"DataRetentionInDays\":90,\"DbProvider\":\"SQLite\",\"EnableRemoteControlRecording\":false,\"EnableWindowsEventLog\":false,\"EnforceAttendedAccess\":false,\"ForceClientHttps\":true,\"KnownProxies\":[],\"MaxConcurrentUpdates\":10,\"MaxOrganizationCount\":1,\"MessageOfTheDay\":\"\",\"RedirectToHttps\":false,\"RemoteControlNotifyUser\":true,\"RemoteControlRequiresAuthentication\":true,\"RemoteControlSessionLimit\":5,\"Require2FA\":false,\"ServerUrl\":\"https://$SUB_REMOTELY.$HOMESERVER_DOMAIN\",\"SmtpCheckCertificateRevocation\":false,\"SmtpDisplayName\":\"Remotely $(getAdminEmailName)\",\"SmtpEmail\":\"$EMAIL_ADMIN_EMAIL_ADDRESS\",\"SmtpHost\":\"$SMTP_HOSTNAME\",\"SmtpLocalDomain\":\"\",\"SmtpPassword\":\"\",\"SmtpPort\":$SMTP_HOSTPORT,\"SmtpUserName\":\"\",\"Theme\":0,\"TrustedCorsOrigins\":[],\"UseHsts\":false,\"UseHttpLogging\":false}';" startStopStack remotely start inner_block="" inner_block=$inner_block">>https://$SUB_REMOTELY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://remotely:5000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_REMOTELY $MANAGETLS_REMOTELY "$is_integrate_hshq" $NETDEFAULT_REMOTELY "$inner_block" if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll remotely "$FMLNAME_REMOTELY" $USERTYPE_REMOTELY "https://$SUB_REMOTELY.$HOMESERVER_DOMAIN" "remotely.png" "$(getHeimdallOrderFromSub $SUB_REMOTELY $USERTYPE_REMOTELY)" restartAllCaddyContainers fi } function outputConfigRemotely() { cat < $HOME/remotely-compose.yml $STACK_VERSION_PREFIX remotely $(getScriptStackVersion remotely) services: remotely: image: $(getScriptImageByContainerName remotely) container_name: remotely hostname: remotely restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/remotely:/app/AppData networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true EOFRM cat < $HOME/remotely.env TZ=\${PORTAINER_TZ} ASPNETCORE_ENVIRONMENT=Production ASPNETCORE_HTTP_PORTS=5000 Remotely_ApplicationOptions__DbProvider=SQLite Remotely_ConnectionStrings__SQLite=Data Source=/app/AppData/Remotely.db EOFRM } function performUpdateRemotely() { perform_stack_name=remotely prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=immybot/remotely:69 image_update_map[0]="immybot/remotely:69,immybot/remotely:69" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Remotely cannot be upgraded to the next version. You must uninstall your current version and reinstall the new version." return ;; 2) newVer=v3 curImageList=immybot/remotely:88 image_update_map[0]="immybot/remotely:88,immybot/remotely:589" ;; 3) newVer=v4 curImageList=immybot/remotely:589 image_update_map[0]="immybot/remotely:589,immybot/remotely:1037" ;; 4) newVer=v4 curImageList=immybot/remotely:1037 image_update_map[0]="immybot/remotely:1037,immybot/remotely:1037" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Calibre function installCalibre() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory calibre "Calibre" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName calibre-server) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName calibre-web) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/calibre mkdir $HSHQ_STACKS_DIR/calibre/server mkdir $HSHQ_STACKS_DIR/calibre/library mkdir $HSHQ_STACKS_DIR/calibre/web initServicesCredentials set +e addUserMailu alias $CALIBRE_WEB_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$CALIBRE_WEB_INIT_ENV" = "true" ]; then sendEmail -s "Calibre-Web Admin Login Info" -b "Calibre-Web Admin Username: $CALIBRE_WEB_ADMIN_USERNAME\nCalibre-Web Admin Password: $CALIBRE_WEB_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" CALIBRE_WEB_INIT_ENV=true updateConfigVar CALIBRE_WEB_INIT_ENV $CALIBRE_WEB_INIT_ENV fi outputConfigCalibre generateCert calibre-web calibre-web installStack calibre calibre-web "done." $HOME/calibre.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi echo "Calibre stack installed, sleeping 10 seconds..." sleep 10 docker exec -it calibre-web python3 /app/calibre-web/cps.py -p /config/app.db -s admin:$CALIBRE_WEB_ADMIN_PASSWORD encpw=$(docker exec -it calibre-web python3 /config/getencpw.py) encpw=${encpw:2:${#encpw}-4} startStopStack calibre stop cat $HSHQ_STACKS_DIR/calibre/server/.config/calibre/global.py.json | jq '.library_path |= "/library"' > $HOME/calibre_new.json mv -f $HOME/calibre_new.json $HSHQ_STACKS_DIR/calibre/server/.config/calibre/global.py.json rm $HSHQ_STACKS_DIR/calibre/web/getencpw.py sqlite3 $HSHQ_STACKS_DIR/calibre/web/app.db "UPDATE user set name='$CALIBRE_WEB_ADMIN_USERNAME', email='$CALIBRE_WEB_ADMIN_EMAIL_ADDRESS', kindle_mail='$CALIBRE_WEB_ADMIN_EMAIL_ADDRESS' where id=1;" sqlite3 $HSHQ_STACKS_DIR/calibre/web/app.db "UPDATE settings set mail_server='$SMTP_HOSTNAME', mail_port=$SMTP_HOSTPORT, mail_use_ssl=1, mail_from='Calibre-Web $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>', mail_size=26214400, mail_server_type=0, config_calibre_dir='/library', config_theme=1, config_login_type=1, config_ldap_provider_url='ldapserver', config_ldap_port=389, config_ldap_authentication=2, config_ldap_serv_username='$LDAP_READONLY_USER_BIND_DN', config_ldap_serv_password_e='$encpw', config_ldap_encryption=1, config_ldap_cacert_path='/usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt', config_ldap_cert_path='/certs/calibre-web.crt', config_ldap_key_path='/certs/calibre-web.key', config_ldap_dn='$LDAP_BASE_DN', config_ldap_user_object='(&(objectclass=person)(uid=%s))', config_ldap_member_user_object='(&(objectclass=person)(uid=%s))', config_ldap_openldap=1, config_ldap_group_object_filter='(&(objectclass=groupOfUniqueNames)(cn=%s))', config_ldap_group_members_field='uniqueMember', config_ldap_group_name='$LDAP_PRIMARY_USER_GROUP_NAME' where id=1;" startStopStack calibre start inner_block="" inner_block=$inner_block">>https://$SUB_CALIBRE_SERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://calibre-server:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CALIBRE_SERVER $MANAGETLS_CALIBRE_SERVER "$is_integrate_hshq" $NETDEFAULT_CALIBRE_SERVER "$inner_block" insertSubAuthelia $SUB_CALIBRE_SERVER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://calibre-web:8083 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CALIBRE_WEB $MANAGETLS_CALIBRE_WEB "$is_integrate_hshq" $NETDEFAULT_CALIBRE_WEB "$inner_block" insertSubAuthelia $SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll calibre "$FMLNAME_CALIBRE_SERVER" $USERTYPE_CALIBRE_SERVER "https://$SUB_CALIBRE_SERVER.$HOMESERVER_DOMAIN" "calibre-server.png" "$(getHeimdallOrderFromSub $SUB_CALIBRE_SERVER $USERTYPE_CALIBRE_SERVER)" insertEnableSvcAll calibre "$FMLNAME_CALIBRE_WEB" $USERTYPE_CALIBRE_WEB "https://$SUB_CALIBRE_WEB.$HOMESERVER_DOMAIN" "calibre-web.png" "$(getHeimdallOrderFromSub $SUB_CALIBRE_WEB $USERTYPE_CALIBRE_WEB)" restartAllCaddyContainers fi } function outputConfigCalibre() { cat < $HOME/calibre-compose.yml $STACK_VERSION_PREFIX calibre $(getScriptStackVersion calibre) services: calibre-server: image: $(getScriptImageByContainerName calibre-server) container_name: calibre-server hostname: calibre-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/calibre/server:/config - \${PORTAINER_HSHQ_STACKS_DIR}/calibre/library:/library calibre-web: image: $(getScriptImageByContainerName calibre-web) container_name: calibre-web hostname: calibre-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-ldap-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/calibre-web.crt:/certs/calibre-web.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/calibre-web.key:/certs/calibre-web.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/calibre/web:/config - \${PORTAINER_HSHQ_STACKS_DIR}/calibre/library:/library networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true EOFNT cat < $HOME/calibre.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID CALIBRE_LDAP_AUTO_CREATE=true EOFNT cat < $HSHQ_STACKS_DIR/calibre/web/getencpw.py from cryptography.fernet import Fernet import cryptography.exceptions from base64 import urlsafe_b64decode f = open("/config/.key", "r") key = f.read() f.close() crypter = Fernet(key) configpw = "$LDAP_READONLY_USER_PASSWORD" configpw_e=crypter.encrypt(configpw.encode()) print(configpw_e) EOFPY } function performUpdateCalibre() { perform_stack_name=calibre prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=linuxserver/calibre:7.3.0,linuxserver/calibre-web:0.6.21 image_update_map[0]="linuxserver/calibre:7.3.0,linuxserver/calibre:7.20.0" image_update_map[1]="linuxserver/calibre-web:0.6.21,linuxserver/calibre-web:0.6.23" ;; 2) newVer=v5 curImageList=linuxserver/calibre:7.4.0,linuxserver/calibre-web:0.6.21 image_update_map[0]="linuxserver/calibre:7.4.0,linuxserver/calibre:7.20.0" image_update_map[1]="linuxserver/calibre-web:0.6.21,linuxserver/calibre-web:0.6.23" ;; 3) newVer=v5 curImageList=linuxserver/calibre:7.7.0,linuxserver/calibre-web:0.6.21-ls257 image_update_map[0]="linuxserver/calibre:7.7.0,linuxserver/calibre:7.20.0" image_update_map[1]="linuxserver/calibre-web:0.6.21-ls257,linuxserver/calibre-web:0.6.23" ;; 4) newVer=v5 curImageList=linuxserver/calibre:7.16.0,linuxserver/calibre-web:0.6.22 image_update_map[0]="linuxserver/calibre:7.16.0,linuxserver/calibre:7.20.0" image_update_map[1]="linuxserver/calibre-web:0.6.22,linuxserver/calibre-web:0.6.23" ;; 5) newVer=v7 curImageList=linuxserver/calibre:7.20.0,linuxserver/calibre-web:0.6.23 image_update_map[0]="linuxserver/calibre:7.20.0,mirror.gcr.io/linuxserver/calibre:8.8.0" image_update_map[1]="linuxserver/calibre-web:0.6.23,mirror.gcr.io/linuxserver/calibre-web:0.6.24-ls342" ;; 6) newVer=v7 curImageList=linuxserver/calibre:8.2.1,linuxserver/calibre-web:0.6.24 image_update_map[0]="linuxserver/calibre:8.2.1,mirror.gcr.io/linuxserver/calibre:8.8.0" image_update_map[1]="linuxserver/calibre-web:0.6.24,mirror.gcr.io/linuxserver/calibre-web:0.6.24-ls342" ;; 7) newVer=v8 curImageList=mirror.gcr.io/linuxserver/calibre:8.8.0,mirror.gcr.io/linuxserver/calibre-web:0.6.24-ls342 image_update_map[0]="mirror.gcr.io/linuxserver/calibre:8.8.0,mirror.gcr.io/linuxserver/calibre:v8.15.0-ls373" image_update_map[1]="mirror.gcr.io/linuxserver/calibre-web:0.6.24-ls342,mirror.gcr.io/linuxserver/calibre-web:0.6.25-ls359" ;; 8) newVer=v8 curImageList=mirror.gcr.io/linuxserver/calibre:v8.15.0-ls373,mirror.gcr.io/linuxserver/calibre-web:0.6.25-ls359 image_update_map[0]="mirror.gcr.io/linuxserver/calibre:v8.15.0-ls373,mirror.gcr.io/linuxserver/calibre:v8.15.0-ls373" image_update_map[1]="mirror.gcr.io/linuxserver/calibre-web:0.6.25-ls359,mirror.gcr.io/linuxserver/calibre-web:0.6.25-ls359" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Netdata function installNetdata() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory netdata "Netdata" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName netdata) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/netdata mkdir $HSHQ_STACKS_DIR/netdata/config mkdir $HSHQ_STACKS_DIR/netdata/lib mkdir $HSHQ_NONBACKUP_DIR/netdata mkdir $HSHQ_NONBACKUP_DIR/netdata/cache outputConfigNetdata installStack netdata netdata "" $HOME/netdata.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 inner_block="" inner_block=$inner_block">>https://$SUB_NETDATA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://netdata:19999 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NETDATA $MANAGETLS_NETDATA "$is_integrate_hshq" $NETDEFAULT_NETDATA "$inner_block" insertSubAuthelia $SUB_NETDATA.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll netdata "$FMLNAME_NETDATA" $USERTYPE_NETDATA "https://$SUB_NETDATA.$HOMESERVER_DOMAIN" "netdata.png" "$(getHeimdallOrderFromSub $SUB_NETDATA $USERTYPE_NETDATA)" restartAllCaddyContainers fi } function outputConfigNetdata() { cat < $HOME/netdata-compose.yml $STACK_VERSION_PREFIX netdata $(getScriptStackVersion netdata) services: netdata: image: $(getScriptImageByContainerName netdata) container_name: netdata hostname: netdata restart: unless-stopped pid: host cap_add: - SYS_PTRACE - SYS_ADMIN security_opt: - apparmor:unconfined networks: - dock-proxy-net - dock-privateip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/passwd:/host/etc/passwd:ro - /etc/group:/host/etc/group:ro - /proc:/host/proc:ro - /sys:/host/sys:ro - /etc/os-release:/host/etc/os-release:ro - /var/log:/host/var/log:ro - /var/run/docker.sock:/var/run/docker.sock:ro - v-netdata-config:/etc/netdata - v-netdata-lib:/var/lib/netdata - v-netdata-cache:/var/cache/netdata volumes: v-netdata-config: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/netdata/config v-netdata-lib: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/netdata/lib v-netdata-cache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/netdata/cache networks: dock-proxy-net: name: dock-proxy external: true dock-privateip-net: name: dock-privateip external: true EOFDZ cat < $HOME/netdata.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID EOFDZ } function performUpdateNetdata() { perform_stack_name=netdata prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=netdata/netdata:v1.44.1 image_update_map[0]="netdata/netdata:v1.44.1,netdata/netdata:v2.3.2" ;; 2) newVer=v5 curImageList=netdata/netdata:v1.44.3 image_update_map[0]="netdata/netdata:v1.44.3,netdata/netdata:v2.3.2" ;; 3) newVer=v5 curImageList=netdata/netdata:v1.46.3 image_update_map[0]="netdata/netdata:v1.46.3,netdata/netdata:v2.3.2" ;; 4) newVer=v5 curImageList=netdata/netdata:v1.47.5 image_update_map[0]="netdata/netdata:v1.47.5,netdata/netdata:v2.3.2" ;; 5) newVer=v7 curImageList=netdata/netdata:v2.3.2 image_update_map[0]="netdata/netdata:v2.3.2,mirror.gcr.io/netdata/netdata:v2.8.2" ;; 6) newVer=v7 curImageList=mirror.gcr.io/netdata/netdata:v2.6.1 image_update_map[0]="mirror.gcr.io/netdata/netdata:v2.6.1,mirror.gcr.io/netdata/netdata:v2.8.2" ;; 7) newVer=v7 curImageList=mirror.gcr.io/netdata/netdata:v2.8.2 image_update_map[0]="mirror.gcr.io/netdata/netdata:v2.8.2,mirror.gcr.io/netdata/netdata:v2.8.2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Linkwarden function installLinkwarden() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory linkwarden "$FMLNAME_LINKWARDEN" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName linkwarden-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName linkwarden-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName linkwarden-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/linkwarden mkdir $HSHQ_STACKS_DIR/linkwarden/db mkdir $HSHQ_STACKS_DIR/linkwarden/dbexport mkdir $HSHQ_STACKS_DIR/linkwarden/app chmod 777 $HSHQ_STACKS_DIR/linkwarden/dbexport initServicesCredentials if [ -z "$LINKWARDEN_NEXTAUTH_SECRET" ]; then LINKWARDEN_NEXTAUTH_SECRET=$(pwgen -c -n 32 1) updateConfigVar LINKWARDEN_NEXTAUTH_SECRET $LINKWARDEN_NEXTAUTH_SECRET fi outputConfigLinkwarden oidcBlock=$(cat $HOME/linkwarden.oidc) rm -f $HOME/linkwarden.oidc insertOIDCClientAuthelia linkwarden "$oidcBlock" installStack linkwarden linkwarden-app "Starting link processing" $HOME/linkwarden.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addMCPServerLiteLLM "linkwarden" "lw" "http://linkwarden-mcp:80/mcp" http none "" addReadOnlyUserToDatabase Linkwarden postgres linkwarden-db $LINKWARDEN_DATABASE_NAME $LINKWARDEN_DATABASE_USER $LINKWARDEN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/linkwarden/dbexport $LINKWARDEN_DATABASE_READONLYUSER $LINKWARDEN_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://linkwarden-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_LINKWARDEN $MANAGETLS_LINKWARDEN "$is_integrate_hshq" $NETDEFAULT_LINKWARDEN "$inner_block" insertSubAuthelia $SUB_LINKWARDEN.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll linkwarden "$FMLNAME_LINKWARDEN" $USERTYPE_LINKWARDEN "https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN" "linkwarden.png" "$(getHeimdallOrderFromSub $SUB_LINKWARDEN $USERTYPE_LINKWARDEN)" restartAllCaddyContainers checkAddDBConnection true linkwarden "$FMLNAME_LINKWARDEN" postgres linkwarden-db $LINKWARDEN_DATABASE_NAME $LINKWARDEN_DATABASE_USER $LINKWARDEN_DATABASE_USER_PASSWORD fi } function outputConfigLinkwarden() { cat < $HOME/linkwarden-compose.yml $STACK_VERSION_PREFIX linkwarden $(getScriptStackVersion linkwarden) services: linkwarden-db: image: $(getScriptImageByContainerName linkwarden-db) container_name: linkwarden-db hostname: linkwarden-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-linkwarden-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.linkwarden-hourly-db.schedule=@every 1h" - "ofelia.job-exec.linkwarden-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.linkwarden-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.linkwarden-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.linkwarden-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.linkwarden-hourly-db.email-from=Linkwarden Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.linkwarden-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.linkwarden-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.linkwarden-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.linkwarden-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.linkwarden-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.linkwarden-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.linkwarden-monthly-db.email-from=Linkwarden Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.linkwarden-monthly-db.mail-only-on-error=false" linkwarden-app: image: $(getScriptImageByContainerName linkwarden-app) container_name: linkwarden-app hostname: linkwarden-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - linkwarden-db networks: - int-linkwarden-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-linkwarden-app:/data/data - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/provision-user.mjs:/data/data/provision-user.mjs linkwarden-mcp: image: $(getScriptImageByContainerName linkwarden-mcp) container_name: linkwarden-mcp hostname: linkwarden-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-linkwarden-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-linkwarden-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/app networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-linkwarden-net: driver: bridge internal: true ipam: driver: default EOFDZ cat < $HOME/linkwarden.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_DB=$LINKWARDEN_DATABASE_NAME POSTGRES_USER=$LINKWARDEN_DATABASE_USER POSTGRES_PASSWORD=$LINKWARDEN_DATABASE_USER_PASSWORD DATABASE_URL=postgresql://$LINKWARDEN_DATABASE_USER:$LINKWARDEN_DATABASE_USER_PASSWORD@linkwarden-db:5432/$LINKWARDEN_DATABASE_NAME NEXTAUTH_SECRET=$LINKWARDEN_NEXTAUTH_SECRET NEXTAUTH_URL=https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN/api/v1/auth NEXT_PUBLIC_AUTHELIA_ENABLED=true AUTHELIA_WELLKNOWN_URL=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/.well-known/openid-configuration AUTHELIA_CLIENT_ID=linkwarden AUTHELIA_CLIENT_SECRET=$LINKWARDEN_OIDC_CLIENT_SECRET NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt NEXT_PUBLIC_DISABLE_REGISTRATION=true PAGINATION_TAKE_COUNT=250 LINKWARDEN_BASE_URL=http://linkwarden-app:3000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false LINKWARDEN_PUBLIC_URL=https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN EOFDZ cat < $HOME/linkwarden.oidc # Authelia OIDC Client linkwarden BEGIN - client_id: linkwarden client_name: Linkwarden client_secret: '$(htpasswd -bnBC 10 "" $LINKWARDEN_OIDC_CLIENT_SECRET | tr -d ':\n')' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth consent_mode: implicit claims_policy: linkwarden_claim scopes: - openid - email - profile redirect_uris: - https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN/api/v1/auth/callback/authelia userinfo_signed_response_alg: none # Authelia OIDC Client linkwarden END EOFIM outputUserProvisionLinkwarden } function outputUserProvisionLinkwarden() { cat < $HSHQ_STACKS_DIR/linkwarden/provision-user.mjs import { createRequire } from "module"; const require = createRequire(import.meta.url); const { PrismaClient } = require("@prisma/client"); const { encode, decode } = require("next-auth/jwt"); const [emailArg, nameArg, tokenNameArg, usernameArg] = process.argv.slice(2); const email = (emailArg || "").trim().toLowerCase(); const name = nameArg && !["", "_", "null", "none"].includes(nameArg.trim().toLowerCase()) ? nameArg.trim() : undefined; const tokenName = (tokenNameArg || "MCP").trim() || "MCP"; const username = usernameArg && !["", "_", "null", "none"].includes(usernameArg.trim().toLowerCase()) ? usernameArg.trim() : undefined; const secret = process.env.NEXTAUTH_SECRET; function usage() { console.error( \`Usage: node provision-user.mjs [name] [tokenName] [username]\n\` + \` email (required) Authelia user identifier.\n\` + \` name (optional) Display name; use "_" to skip.\n\` + \` tokenName (optional) API token name, default "MCP".\n\` + \` username (optional) Authelia/LDAP username; fallback is random.\` ); } if (!email) { usage(); process.exit(1); } if (!secret) { console.error("NEXTAUTH_SECRET is not set in this environment."); process.exit(1); } const prisma = new PrismaClient(); try { // 1) Upsert the user (OIDC-shaped, mirroring apps/web/pages/api/v1/auth/[...nextauth].ts) let user = await prisma.user.findFirst({ where: { email } }); const desiredUsername = username ?? "user" + Math.round(Math.random() * 1000000000); try { if (!user) { user = await prisma.user.create({ data: { email, name, emailVerified: new Date(), username: desiredUsername, }, }); } else { user = await prisma.user.update({ where: { id: user.id }, data: { username: desiredUsername }, }); } } catch (e) { if (e.code === "P2002") { console.error( \`Username "\${desiredUsername}" is already in use by another user. \` + \`Pass "_" to skip, or use a different username.\` ); process.exit(4); } throw e; } const userId = user.id; const now = Math.floor(Date.now() / 1000); const neverSeconds = 73050 * 24 * 60 * 60; // "never" == 200y. // 2) Reuse an existing token by name if present (re-mint a valid key from its jti). const existing = await prisma.accessToken.findFirst({ where: { name: tokenName, userId }, }); if (existing) { // If it was revoked, un-revoke so the re-minted key works. if (existing.revoked) { await prisma.accessToken.update({ where: { id: existing.id }, data: { revoked: false }, }); } const token = await encode({ token: { id: userId, iat: now, exp: now + neverSeconds, jti: existing.token }, secret, }); console.log(token); process.exit(0); } // 3) Mint a new API key ("never" == 200y, matching TokenExpiry.never in the UI) const expires = new Date(now * 1000 + neverSeconds * 1000); const token = await encode({ token: { id: userId }, maxAge: neverSeconds, secret, }); const tokenBody = await decode({ token, secret }); const jti = tokenBody?.jti; if (typeof jti !== "string") { console.error("Failed to derive token identifier (jti)."); process.exit(3); } // 4) Persist the access token row (token column stores the jti, as the app expects) await prisma.accessToken.create({ data: { name: tokenName, userId, token: jti, expires, revoked: false, }, }); console.log(token); } finally { await prisma.\$disconnect(); } EOFIM } function buildImageLinkwardenMCPV1() { set +e echo -e "\n========================================================================" echo -e " The Linkwarden MCP image is being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/LinkwardenMCP-Multitenant cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 https://github.com/homeserverhq/LinkwardenMCP-Multitenant.git cd $HSHQ_BUILD_DIR/LinkwardenMCP-Multitenant docker image build -t hshq/linkwarden-mcp:v1 . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/LinkwardenMCP-Multitenant return $rtval } function performUpdateLinkwarden() { perform_stack_name=linkwarden prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.4.8 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.4.8,ghcr.io/linkwarden/linkwarden:v2.6.2" ;; 2) newVer=v4 curImageList=postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.4.9 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.4.9,ghcr.io/linkwarden/linkwarden:v2.6.2" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.5.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.5.1,ghcr.io/linkwarden/linkwarden:v2.6.2" ;; 4) newVer=v6 curImageList=postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.6.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.6.2,ghcr.io/linkwarden/linkwarden:v2.9.3" ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.7.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.7.1,ghcr.io/linkwarden/linkwarden:v2.9.3" ;; 6) newVer=v8 curImageList=postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.9.3 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.9.3,ghcr.io/linkwarden/linkwarden:v2.13.1" ;; 7) newVer=v8 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.11.5 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.11.5,ghcr.io/linkwarden/linkwarden:v2.13.1" ;; 8) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.13.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.13.1,ghcr.io/linkwarden/linkwarden:v2.14.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfLinkwardenV9AddMCP perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.14.1,hshq/linkwarden-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.14.1,ghcr.io/linkwarden/linkwarden:v2.15.1" image_update_map[2]="hshq/linkwarden-mcp:v1,ghcr.io/homeserverhq/linkwarden-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfLinkwardenV10Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 10) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.15.1,ghcr.io/homeserverhq/linkwarden-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.15.1,ghcr.io/linkwarden/linkwarden:v2.15.1" image_update_map[2]="ghcr.io/homeserverhq/linkwarden-mcp:v2,ghcr.io/homeserverhq/linkwarden-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfLinkwardenV11Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 11) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/linkwarden/linkwarden:v2.15.1,ghcr.io/homeserverhq/linkwarden-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/linkwarden/linkwarden:v2.15.1,ghcr.io/linkwarden/linkwarden:v2.15.1" image_update_map[2]="ghcr.io/homeserverhq/linkwarden-mcp:v2,ghcr.io/homeserverhq/linkwarden-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfLinkwardenV9AddMCP() { set +e buildImageLinkwardenMCPV1 if [ $? -ne 0 ]; then return 1 fi cat < $HOME/linkwarden-compose.yml $STACK_VERSION_PREFIX linkwarden $(getScriptStackVersion linkwarden) services: linkwarden-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: linkwarden-db hostname: linkwarden-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-linkwarden-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.linkwarden-hourly-db.schedule=@every 1h" - "ofelia.job-exec.linkwarden-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.linkwarden-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.linkwarden-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.linkwarden-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.linkwarden-hourly-db.email-from=Linkwarden Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.linkwarden-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.linkwarden-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.linkwarden-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.linkwarden-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.linkwarden-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.linkwarden-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.linkwarden-monthly-db.email-from=Linkwarden Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.linkwarden-monthly-db.mail-only-on-error=false" linkwarden-app: image: ghcr.io/linkwarden/linkwarden:v2.14.1 container_name: linkwarden-app hostname: linkwarden-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - linkwarden-db networks: - int-linkwarden-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-linkwarden-app:/data/data linkwarden-mcp: image: hshq/linkwarden-mcp:v1 container_name: linkwarden-mcp hostname: linkwarden-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-linkwarden-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-linkwarden-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/app networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-linkwarden-net: driver: bridge internal: true ipam: driver: default EOFDZ set +e grep -q LINKWARDEN_BASE_URL $HOME/linkwarden.env if [ $? -ne 0 ]; then echo "LINKWARDEN_BASE_URL=http://linkwarden-app:3000" >> $HOME/linkwarden.env fi addMCPServerLiteLLM "linkwarden" "lw" "http://linkwarden-mcp:80/mcp" http none "" } function mfLinkwardenV10Update() { cat < $HOME/linkwarden.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_DB=$LINKWARDEN_DATABASE_NAME POSTGRES_USER=$LINKWARDEN_DATABASE_USER POSTGRES_PASSWORD=$LINKWARDEN_DATABASE_USER_PASSWORD DATABASE_URL=postgresql://$LINKWARDEN_DATABASE_USER:$LINKWARDEN_DATABASE_USER_PASSWORD@linkwarden-db:5432/$LINKWARDEN_DATABASE_NAME NEXTAUTH_SECRET=$LINKWARDEN_NEXTAUTH_SECRET NEXTAUTH_URL=https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN/api/v1/auth NEXT_PUBLIC_AUTHELIA_ENABLED=true AUTHELIA_WELLKNOWN_URL=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/.well-known/openid-configuration AUTHELIA_CLIENT_ID=linkwarden AUTHELIA_CLIENT_SECRET=$LINKWARDEN_OIDC_CLIENT_SECRET NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt NEXT_PUBLIC_DISABLE_REGISTRATION=true PAGINATION_TAKE_COUNT=250 LINKWARDEN_BASE_URL=http://linkwarden-app:3000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false LINKWARDEN_PUBLIC_URL=https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN EOFDZ } function mfLinkwardenV11Update() { cat < $HOME/linkwarden.oidc # Authelia OIDC Client linkwarden BEGIN - client_id: linkwarden client_name: Linkwarden client_secret: '$(htpasswd -bnBC 10 "" $LINKWARDEN_OIDC_CLIENT_SECRET | tr -d ':\n')' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth consent_mode: implicit claims_policy: cp_legacy scopes: - openid - groups - email - profile redirect_uris: - https://$SUB_LINKWARDEN.$HOMESERVER_DOMAIN/api/v1/auth/callback/authelia userinfo_signed_response_alg: none # Authelia OIDC Client linkwarden END EOFIM oidcBlock=$(cat $HOME/linkwarden.oidc) rm -f $HOME/linkwarden.oidc insertOIDCClientAuthelia linkwarden "$oidcBlock" outputUserProvisionLinkwarden cat < $HOME/linkwarden-compose.yml $STACK_VERSION_PREFIX linkwarden v11 services: linkwarden-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: linkwarden-db hostname: linkwarden-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-linkwarden-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.linkwarden-hourly-db.schedule=@every 1h" - "ofelia.job-exec.linkwarden-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.linkwarden-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.linkwarden-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.linkwarden-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.linkwarden-hourly-db.email-from=Linkwarden Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.linkwarden-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.linkwarden-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.linkwarden-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.linkwarden-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.linkwarden-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.linkwarden-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.linkwarden-monthly-db.email-from=Linkwarden Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.linkwarden-monthly-db.mail-only-on-error=false" linkwarden-app: image: ghcr.io/linkwarden/linkwarden:v2.15.1 container_name: linkwarden-app hostname: linkwarden-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - linkwarden-db networks: - int-linkwarden-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-linkwarden-app:/data/data - \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/provision-user.mjs:/data/data/provision-user.mjs linkwarden-mcp: image: ghcr.io/homeserverhq/linkwarden-mcp:v2 container_name: linkwarden-mcp hostname: linkwarden-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-linkwarden-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-linkwarden-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/linkwarden/app networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-linkwarden-net: driver: bridge internal: true ipam: driver: default EOFDZ } # StirlingPDF function installStirlingPDF() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory stirlingpdf "StirlingPDF" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName stirlingpdf) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/stirlingpdf mkdir $HSHQ_STACKS_DIR/stirlingpdf/configs mkdir $HSHQ_NONBACKUP_DIR/stirlingpdf mkdir $HSHQ_NONBACKUP_DIR/stirlingpdf/logs mkdir $HSHQ_NONBACKUP_DIR/stirlingpdf/traindata outputConfigStirlingPDF installStack stirlingpdf stirlingpdf "Server PID" $HOME/stirlingpdf.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 inner_block="" inner_block=$inner_block">>https://$SUB_STIRLINGPDF.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://stirlingpdf:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_STIRLINGPDF $MANAGETLS_STIRLINGPDF "$is_integrate_hshq" $NETDEFAULT_STIRLINGPDF "$inner_block" insertSubAuthelia $SUB_STIRLINGPDF.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll stirlingpdf "$FMLNAME_STIRLINGPDF" $USERTYPE_STIRLINGPDF "https://$SUB_STIRLINGPDF.$HOMESERVER_DOMAIN" "stirlingpdf.png" "$(getHeimdallOrderFromSub $SUB_STIRLINGPDF $USERTYPE_STIRLINGPDF)" restartAllCaddyContainers fi } function outputConfigStirlingPDF() { cat < $HOME/stirlingpdf-compose.yml $STACK_VERSION_PREFIX stirlingpdf $(getScriptStackVersion stirlingpdf) services: stirlingpdf: image: $(getScriptImageByContainerName stirlingpdf) container_name: stirlingpdf hostname: stirlingpdf restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/stirlingpdf/configs:/configs - \${PORTAINER_HSHQ_NONBACKUP_DIR}/stirlingpdf/logs:/logs - \${PORTAINER_HSHQ_NONBACKUP_DIR}/stirlingpdf/traindata:/usr/share/tesseract-ocr/5/tessdata networks: dock-proxy-net: name: dock-proxy external: true EOFDZ cat < $HOME/stirlingpdf.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID EOFDZ } function performUpdateStirlingPDF() { perform_stack_name=stirlingpdf prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v6 curImageList=frooodle/s-pdf:0.19.0 image_update_map[0]="frooodle/s-pdf:0.19.0,frooodle/s-pdf:0.45.0" ;; 2) newVer=v6 curImageList=frooodle/s-pdf:0.20.1 image_update_map[0]="frooodle/s-pdf:0.20.1,frooodle/s-pdf:0.45.0" ;; 3) newVer=v6 curImageList=frooodle/s-pdf:0.22.2 image_update_map[0]="frooodle/s-pdf:0.22.2,frooodle/s-pdf:0.45.0" ;; 4) newVer=v6 curImageList=frooodle/s-pdf:0.26.1 image_update_map[0]="frooodle/s-pdf:0.26.1,frooodle/s-pdf:0.45.0" ;; 5) newVer=v7 curImageList=frooodle/s-pdf:0.31.1 image_update_map[0]="frooodle/s-pdf:0.31.1,mirror.gcr.io/stirlingtools/stirling-pdf:1.2.0" ;; 6) newVer=v7 curImageList=frooodle/s-pdf:0.45.0 image_update_map[0]="frooodle/s-pdf:0.45.0,mirror.gcr.io/stirlingtools/stirling-pdf:1.2.0" ;; 7) newVer=v7 curImageList=mirror.gcr.io/stirlingtools/stirling-pdf:1.2.0 image_update_map[0]="mirror.gcr.io/stirlingtools/stirling-pdf:1.2.0,mirror.gcr.io/stirlingtools/stirling-pdf:2.1.0" ;; 8) # Hold off on this update, there are a lot of changes to review. newVer=v8 curImageList=mirror.gcr.io/stirlingtools/stirling-pdf:2.1.0 image_update_map[0]="mirror.gcr.io/stirlingtools/stirling-pdf:2.1.0,mirror.gcr.io/stirlingtools/stirling-pdf:2.1.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # BarAssistant function installBarAssistant() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory bar-assistant "Bar Assistant" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName bar-assistant-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName bar-assistant-web) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName bar-assistant-meilisearch) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName bar-assistant-saltrim) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName bar-assistant-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName bar-assistant-mcp) if [ $? -ne 0 ]; then return 1 fi if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/bar-assistant mkdir $HSHQ_STACKS_DIR/bar-assistant/app mkdir $HSHQ_STACKS_DIR/bar-assistant/meilisearch mkdir $HSHQ_STACKS_DIR/bar-assistant/web initServicesCredentials outputConfigBarAssistant installStack bar-assistant bar-assistant-app "ready to handle connections" $HOME/bar-assistant.env 10 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 performDBConfigBarAssistant addMCPServerLiteLLM "barassistant" "bar" "http://bar-assistant-mcp:80/mcp" http none "" inner_block="" inner_block=$inner_block">>https://$SUB_BARASSISTANT.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://bar-assistant-web:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_BARASSISTANT $MANAGETLS_BARASSISTANT "$is_integrate_hshq" $NETDEFAULT_BARASSISTANT "$inner_block" insertSubAuthelia $SUB_BARASSISTANT.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll bar-assistant "$FMLNAME_BARASSISTANT" $USERTYPE_BARASSISTANT "https://$SUB_BARASSISTANT.$HOMESERVER_DOMAIN" "bar-assistant.png" "$(getHeimdallOrderFromSub $SUB_BARASSISTANT $USERTYPE_BARASSISTANT)" restartAllCaddyContainers fi } function outputConfigBarAssistant() { outputComposeBarAssistant cat < $HOME/bar-assistant.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID BASE_URL=https://$SUB_BARASSISTANT.$HOMESERVER_DOMAIN APP_URL=\${BASE_URL} DEFAULT_LOCALE=en-US LOG_CHANNEL=stderr API_URL=\${BASE_URL}/bar MEILISEARCH_URL=\${BASE_URL}/search MEILI_MASTER_KEY=$BARASSISTANT_MEILISEARCH_KEY MEILI_ENV=production MEILISEARCH_KEY=$BARASSISTANT_MEILISEARCH_KEY MEILISEARCH_HOST=http://bar-assistant-meilisearch:7700 REDIS_HOST=bar-assistant-redis REDIS_PASSWORD=$BARASSISTANT_REDIS_PASSWORD ALLOW_REGISTRATION=true MAIL_MAILER=smtp MAILS_ENABLED=true MAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS MAIL_FROM_NAME=Bar Assistant $(getAdminEmailName) MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_ENCRYPTION=tls MAIL_REQUIRE_CONFIRMATION=true MAIL_CONFIRM_URL=\${BASE_URL}/confirmation/[id]/[hash] MAIL_RESET_URL=\${BASE_URL}/reset-password?token=[token] BAR_ASSISTANT_URL=http://bar-assistant-app:8080 BAR_ASSISTANT_EXT_URL=https://$SUB_BARASSISTANT.$HOMESERVER_DOMAIN BAR_ASSISTANT_TOKEN=$BARASSISTANT_API_KEY BAR_ASSISTANT_BAR_ID=1 MCP_API_KEY=$BARASSISTANT_MCP_API_KEY EOFBA cat < $HSHQ_STACKS_DIR/bar-assistant/web/nginx.conf server { listen 3000 default_server; listen [::]:3000 default_server; server_name _; location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } client_max_body_size 100M; location /bar/ { proxy_pass http://bar-assistant-app:8080/; } location /uploads/ { proxy_pass http://bar-assistant-app:8080; } location /search/ { proxy_pass http://bar-assistant-meilisearch:7700/; } location / { proxy_pass http://bar-assistant-saltrim:8080/; } } EOFBA } function outputComposeBarAssistant() { cat < $HOME/bar-assistant-compose.yml $STACK_VERSION_PREFIX bar-assistant $(getScriptStackVersion bar-assistant) services: bar-assistant-app: image: $(getScriptImageByContainerName bar-assistant-app) container_name: bar-assistant-app hostname: bar-assistant-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - bar-assistant-meilisearch - bar-assistant-redis healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-bar-assistant-app:/var/www/cocktails/storage/bar-assistant bar-assistant-meilisearch: image: $(getScriptImageByContainerName bar-assistant-meilisearch) container_name: bar-assistant-meilisearch hostname: bar-assistant-meilisearch restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7700"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-bar-assistant-meilisearch:/meili_data bar-assistant-redis: image: $(getScriptImageByContainerName bar-assistant-redis) container_name: bar-assistant-redis hostname: bar-assistant-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $BARASSISTANT_REDIS_PASSWORD --appendonly no networks: - int-bar-assistant-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro bar-assistant-saltrim: image: $(getScriptImageByContainerName bar-assistant-saltrim) container_name: bar-assistant-saltrim hostname: bar-assistant-saltrim restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - bar-assistant-app healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro bar-assistant-web: image: $(getScriptImageByContainerName bar-assistant-web) container_name: bar-assistant-web hostname: bar-assistant-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-bar-assistant-net - dock-ext-net depends_on: - bar-assistant-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/web/nginx.conf:/etc/nginx/conf.d/default.conf bar-assistant-mcp: image: $(getScriptImageByContainerName bar-assistant-mcp) container_name: bar-assistant-mcp hostname: bar-assistant-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-bar-assistant-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-bar-assistant-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/app v-bar-assistant-meilisearch: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/meilisearch networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-bar-assistant-net: driver: bridge internal: true ipam: driver: default EOFBA } function performUpdateBarAssistant() { perform_stack_name=bar-assistant prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) # Meilisearch has a more recent version, v1.6. However, moving between versions requires manual migration. # So, we'll use v1.4 until the migration process has been mapped out. # See https://www.meilisearch.com/docs/learn/cookbooks/docker#generating-dumps-and-updating-meilisearch newVer=v3 curImageList=barassistant/server:v3,getmeili/meilisearch:v1.4,bitnami/redis:7.0.5,barassistant/salt-rim:v2,nginx:1.25.3-alpine image_update_map[0]="barassistant/server:v3,barassistant/server:3.11.0" image_update_map[1]="getmeili/meilisearch:v1.4,getmeili/meilisearch:v1.6" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[3]="barassistant/salt-rim:v2,barassistant/salt-rim:2.15.0" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" ;; 2) newVer=v3 curImageList=barassistant/server:3.11.0,getmeili/meilisearch:v1.6,bitnami/redis:7.0.5,barassistant/salt-rim:2.11.0,nginx:1.25.3-alpine image_update_map[0]="barassistant/server:3.11.0,barassistant/server:3.17.1" image_update_map[1]="getmeili/meilisearch:v1.6,getmeili/meilisearch:v1.6" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[3]="barassistant/salt-rim:2.11.0,barassistant/salt-rim:2.15.0" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" ;; 3) newVer=v4 curImageList=barassistant/server:3.17.1,getmeili/meilisearch:v1.6,bitnami/redis:7.0.5,barassistant/salt-rim:2.15.0,nginx:1.25.3-alpine image_update_map[0]="barassistant/server:3.17.1,barassistant/server:4.0.3" image_update_map[1]="getmeili/meilisearch:v1.6,getmeili/meilisearch:v1.6" image_update_map[2]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" image_update_map[3]="barassistant/salt-rim:2.15.0,barassistant/salt-rim:3.2.1" image_update_map[4]="nginx:1.25.3-alpine,nginx:1.25.3-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearMeiliData true mfUpdateBarAssistantV4 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v6 curImageList=barassistant/server:4.0.3,getmeili/meilisearch:v1.6,bitnami/redis:7.0.5,barassistant/salt-rim:3.2.1,nginx:1.25.3-alpine image_update_map[0]="barassistant/server:4.0.3,mirror.gcr.io/barassistant/server:5.6.1" image_update_map[1]="getmeili/meilisearch:v1.6,mirror.gcr.io/getmeili/meilisearch:v1.16.0" image_update_map[2]="bitnami/redis:7.0.5,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[3]="barassistant/salt-rim:3.2.1,mirror.gcr.io/barassistant/salt-rim:4.6.0" image_update_map[4]="nginx:1.25.3-alpine,mirror.gcr.io/nginx:1.28.0-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearMeiliData true mfBarAssistantFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v7 curImageList=barassistant/server:5.2.0,getmeili/meilisearch:v1.6,bitnami/redis:7.4.2,barassistant/salt-rim:4.1.0,nginx:1.27.4-alpine image_update_map[0]="barassistant/server:5.2.0,mirror.gcr.io/barassistant/server:5.6.1" image_update_map[1]="getmeili/meilisearch:v1.6,mirror.gcr.io/getmeili/meilisearch:v1.16.0" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[3]="barassistant/salt-rim:4.1.0,mirror.gcr.io/barassistant/salt-rim:4.6.0" image_update_map[4]="nginx:1.27.4-alpine,mirror.gcr.io/nginx:1.28.0-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearMeiliData true mfBarAssistantFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v7 curImageList=barassistant/server:5.2.0,getmeili/meilisearch:v1.13.3,bitnami/redis:7.4.2,barassistant/salt-rim:4.1.0,nginx:1.27.4-alpine image_update_map[0]="barassistant/server:5.2.0,mirror.gcr.io/barassistant/server:5.6.1" image_update_map[1]="getmeili/meilisearch:v1.13.3,mirror.gcr.io/getmeili/meilisearch:v1.16.0" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[3]="barassistant/salt-rim:4.1.0,mirror.gcr.io/barassistant/salt-rim:4.6.0" image_update_map[4]="nginx:1.27.4-alpine,mirror.gcr.io/nginx:1.28.0-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearMeiliData true mfBarAssistantFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v8 curImageList=mirror.gcr.io/barassistant/server:5.6.1,mirror.gcr.io/getmeili/meilisearch:v1.16.0,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/barassistant/salt-rim:4.6.0,mirror.gcr.io/nginx:1.28.0-alpine image_update_map[0]="mirror.gcr.io/barassistant/server:5.6.1,mirror.gcr.io/barassistant/server:5.9.3" image_update_map[1]="mirror.gcr.io/getmeili/meilisearch:v1.16.0,mirror.gcr.io/getmeili/meilisearch:v1.28.1" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/barassistant/salt-rim:4.6.0,mirror.gcr.io/barassistant/salt-rim:4.11.1" image_update_map[4]="mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/nginx:1.29.3-alpine" ;; 8) newVer=v9 curImageList=mirror.gcr.io/barassistant/server:5.9.3,mirror.gcr.io/getmeili/meilisearch:v1.28.1,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/barassistant/salt-rim:4.11.1,mirror.gcr.io/nginx:1.29.3-alpine image_update_map[0]="mirror.gcr.io/barassistant/server:5.9.3,mirror.gcr.io/barassistant/server:5.13.2" image_update_map[1]="mirror.gcr.io/getmeili/meilisearch:v1.28.1,mirror.gcr.io/getmeili/meilisearch:v1.41.0" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/barassistant/salt-rim:4.11.1,mirror.gcr.io/barassistant/salt-rim:4.14.1" image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearMeiliData true mfBarAssistantAddMCP perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=v10 curImageList=mirror.gcr.io/barassistant/server:5.13.2,mirror.gcr.io/getmeili/meilisearch:v1.41.0,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/barassistant/salt-rim:4.14.1,mirror.gcr.io/nginx:1.29.3-alpine,hshq/barassistant-mcp:v1 image_update_map[0]="mirror.gcr.io/barassistant/server:5.13.2,mirror.gcr.io/barassistant/server:5.15.2" image_update_map[1]="mirror.gcr.io/getmeili/meilisearch:v1.41.0,mirror.gcr.io/getmeili/meilisearch:v1.41.0" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/barassistant/salt-rim:4.14.1,mirror.gcr.io/barassistant/salt-rim:4.15.0" image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[5]="hshq/barassistant-mcp:v1,hshq/barassistant-mcp:v1" ;; 10) newVer=v11 curImageList=mirror.gcr.io/barassistant/server:5.15.2,mirror.gcr.io/getmeili/meilisearch:v1.41.0,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/barassistant/salt-rim:4.15.0,mirror.gcr.io/nginx:1.29.3-alpine,hshq/barassistant-mcp:v1 image_update_map[0]="mirror.gcr.io/barassistant/server:5.15.2,mirror.gcr.io/barassistant/server:5.15.3" image_update_map[1]="mirror.gcr.io/getmeili/meilisearch:v1.41.0,mirror.gcr.io/getmeili/meilisearch:v1.49.0" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/barassistant/salt-rim:4.15.0,mirror.gcr.io/barassistant/salt-rim:4.15.0" image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.31.2-alpine" image_update_map[5]="hshq/barassistant-mcp:v1,ghcr.io/homeserverhq/barassistant-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearMeiliData true mfBarAssistantV11Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 11) newVer=v11 curImageList=mirror.gcr.io/barassistant/server:5.15.3,mirror.gcr.io/getmeili/meilisearch:v1.49.0,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/barassistant/salt-rim:4.15.0,mirror.gcr.io/nginx:1.31.2-alpine,ghcr.io/homeserverhq/barassistant-mcp:v2 image_update_map[0]="mirror.gcr.io/barassistant/server:5.15.3,mirror.gcr.io/barassistant/server:5.15.3" image_update_map[1]="mirror.gcr.io/getmeili/meilisearch:v1.49.0,mirror.gcr.io/getmeili/meilisearch:v1.49.0" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/barassistant/salt-rim:4.15.0,mirror.gcr.io/barassistant/salt-rim:4.15.0" image_update_map[4]="mirror.gcr.io/nginx:1.31.2-alpine,mirror.gcr.io/nginx:1.31.2-alpine" image_update_map[5]="ghcr.io/homeserverhq/barassistant-mcp:v2,ghcr.io/homeserverhq/barassistant-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfClearMeiliData false perform_update_report="${perform_update_report}$stack_upgrade_report" } function buildImageBarAssistantMCPV1() { set +e echo -e "\n========================================================================" echo -e " The BarAssistant MCP image is being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/bar-assistant-mcp-server cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 https://github.com/zhdenny/bar-assistant-mcp-server.git cd $HSHQ_BUILD_DIR/bar-assistant-mcp-server lineBegin="private getCocktailDirectLink(" lineEnd="private createResponseMetadata" replace_text=$(cat <<'EOFRT' private getCocktailDirectLink(slug: string): string { // Extract base URL from configured Bar Assistant URL const baseUrl = this.barClient['config'].baseUrl.replace(/\\\/bar$/, ''); const effectiveUrl = process.env.BAR_ASSISTANT_EXT_URL || baseUrl; return \`${effectiveUrl}/cocktails/${slug}\`; } /** * Create consistent metadata for responses */ private createResponseMetadata(source: string, resultCount: number, startTime?: number): ResponseSchemas.ResponseMetadata { EOFRT ) escaped_replace_text=$(printf '%s' "$replace_text" | sed ':a;N;$!ba;s/\n/\\n/g') sed -i "/$lineBegin/,/$lineEnd/c\\$escaped_replace_text" src/bar-assistant-mcp-server.ts docker image build -t hshq/barassistant-mcp:v1 . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/bar-assistant-mcp-server return $rtval } function performDBConfigBarAssistant() { dtnow=$(date '+%Y-%m-%d %H:%M:%S') api_hash=$(echo -n "$BARASSISTANT_API_KEY" | sha256sum | awk '{print $1}') sudo sqlite3 $HSHQ_STACKS_DIR/bar-assistant/app/database.ba3.sqlite "insert into personal_access_tokens(tokenable_type,tokenable_id,name,token,abilities,created_at,updated_at) values('Kami\Cocktail\Models\User',1,'MCP','$api_hash','[\"cocktails.read\",\"cocktails.write\",\"cocktails.import\",\"ingredients.write\",\"bars.read\",\"ingredients.read\",\"bars.write\"]','$dtnow','$dtnow');" } function outputComposeV9BarAssistant() { cat < $HOME/bar-assistant-compose.yml $STACK_VERSION_PREFIX bar-assistant v9 services: bar-assistant-app: image: mirror.gcr.io/barassistant/server:5.13.2 container_name: bar-assistant-app hostname: bar-assistant-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - bar-assistant-meilisearch - bar-assistant-redis healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-bar-assistant-app:/var/www/cocktails/storage/bar-assistant bar-assistant-meilisearch: image: mirror.gcr.io/getmeili/meilisearch:v1.41.0 container_name: bar-assistant-meilisearch hostname: bar-assistant-meilisearch restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7700"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-bar-assistant-meilisearch:/meili_data bar-assistant-saltrim: image: mirror.gcr.io/barassistant/salt-rim:4.14.1 container_name: bar-assistant-saltrim hostname: bar-assistant-saltrim restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - bar-assistant-app healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro bar-assistant-web: image: mirror.gcr.io/nginx:1.29.3-alpine container_name: bar-assistant-web hostname: bar-assistant-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-bar-assistant-net - dock-ext-net depends_on: - bar-assistant-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/web/nginx.conf:/etc/nginx/conf.d/default.conf bar-assistant-mcp: image: hshq/barassistant-mcp:v1 container_name: bar-assistant-mcp hostname: bar-assistant-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always command: npm start -- --sse networks: - int-bar-assistant-net - dock-aipriv-net depends_on: - bar-assistant-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro bar-assistant-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: bar-assistant-redis hostname: bar-assistant-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $BARASSISTANT_REDIS_PASSWORD --appendonly no networks: - int-bar-assistant-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-bar-assistant-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/app v-bar-assistant-meilisearch: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/meilisearch networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-bar-assistant-net: driver: bridge internal: true ipam: driver: default EOFBA } function clearMeiliData() { # This function assumes the BarAssistant stack/containers are NOT running. echo -e "\nClearing BarAssistant Meilisearch data...\n" sudo rm -fr ${HSHQ_STACKS_DIR}/bar-assistant/meilisearch/* sudo rm -fr $HSHQ_NONBACKUP_DIR/bar-assistant/redis/* sleep 3 } function mfClearMeiliData() { if [ "$stackStatus" = "1" ];then # Stack is running, stop it, then clear data startStopStack bar-assistant stop isStartStackFromStopped=true sleep 3 fi clearMeiliData } function mfUpdateBarAssistantV4() { mfClearMeiliData isStartStackFromStopped="" sed -i "s/localhost:3000/localhost:8080/g" $HOME/bar-assistant-compose.yml sed -i "s/bar-assistant-app:3000/bar-assistant-app:8080/g" $HSHQ_STACKS_DIR/bar-assistant/web/nginx.conf sudo chown -R 33:33 $HSHQ_STACKS_DIR/bar-assistant/app } function mfBarAssistantFixRedisCompose() { replaceRedisBlock bar-assistant bar-assistant-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/bar-assistant/redis/* } function mfBarAssistantAddMCP() { set +e buildImageBarAssistantMCPV1 if [ $? -ne 0 ]; then return 1 fi outputComposeV9BarAssistant grep -q MCP_API_KEY $HOME/bar-assistant.env if [ $? -ne 0 ]; then echo "BAR_ASSISTANT_URL=http://bar-assistant-app:8080" >> $HOME/bar-assistant.env echo "BAR_ASSISTANT_TOKEN=$BARASSISTANT_API_KEY" >> $HOME/bar-assistant.env echo "BAR_ASSISTANT_BAR_ID=1" >> $HOME/bar-assistant.env echo "MCP_API_KEY=$BARASSISTANT_MCP_API_KEY" >> $HOME/bar-assistant.env fi performDBConfigBarAssistant addMCPServerLiteLLM "barassistant" "bar" "http://bar-assistant-mcp:80/mcp" http none "" } function mfBarAssistantV11Update() { cat < $HOME/bar-assistant-compose.yml $STACK_VERSION_PREFIX bar-assistant v11 services: bar-assistant-app: image: mirror.gcr.io/barassistant/server:5.15.3 container_name: bar-assistant-app hostname: bar-assistant-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - bar-assistant-meilisearch - bar-assistant-redis healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-bar-assistant-app:/var/www/cocktails/storage/bar-assistant bar-assistant-meilisearch: image: mirror.gcr.io/getmeili/meilisearch:v1.49.0 container_name: bar-assistant-meilisearch hostname: bar-assistant-meilisearch restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7700"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-bar-assistant-meilisearch:/meili_data bar-assistant-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: bar-assistant-redis hostname: bar-assistant-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $BARASSISTANT_REDIS_PASSWORD --appendonly no networks: - int-bar-assistant-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro bar-assistant-saltrim: image: mirror.gcr.io/barassistant/salt-rim:4.15.0 container_name: bar-assistant-saltrim hostname: bar-assistant-saltrim restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - bar-assistant-app healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080"] interval: 10s timeout: 5s retries: 5 networks: - int-bar-assistant-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro bar-assistant-web: image: mirror.gcr.io/nginx:1.31.2-alpine container_name: bar-assistant-web hostname: bar-assistant-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-bar-assistant-net - dock-ext-net depends_on: - bar-assistant-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/web/nginx.conf:/etc/nginx/conf.d/default.conf bar-assistant-mcp: image: ghcr.io/homeserverhq/barassistant-mcp:v2 container_name: bar-assistant-mcp hostname: bar-assistant-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-bar-assistant-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-bar-assistant-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/app v-bar-assistant-meilisearch: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/bar-assistant/meilisearch networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-bar-assistant-net: driver: bridge internal: true ipam: driver: default EOFBA cat < $HOME/bar-assistant.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID BASE_URL=https://$SUB_BARASSISTANT.$HOMESERVER_DOMAIN APP_URL=\${BASE_URL} DEFAULT_LOCALE=en-US LOG_CHANNEL=stderr API_URL=\${BASE_URL}/bar MEILISEARCH_URL=\${BASE_URL}/search MEILI_MASTER_KEY=$BARASSISTANT_MEILISEARCH_KEY MEILI_ENV=production MEILISEARCH_KEY=$BARASSISTANT_MEILISEARCH_KEY MEILISEARCH_HOST=http://bar-assistant-meilisearch:7700 REDIS_HOST=bar-assistant-redis REDIS_PASSWORD=$BARASSISTANT_REDIS_PASSWORD ALLOW_REGISTRATION=true MAIL_MAILER=smtp MAILS_ENABLED=true MAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS MAIL_FROM_NAME=Bar Assistant $(getAdminEmailName) MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_ENCRYPTION=tls MAIL_REQUIRE_CONFIRMATION=true MAIL_CONFIRM_URL=\${BASE_URL}/confirmation/[id]/[hash] MAIL_RESET_URL=\${BASE_URL}/reset-password?token=[token] BARASSISTANT_BASE_URL=http://bar-assistant-app:8080 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false BARASSISTANT_PUBLIC_URL=https://$SUB_BARASSISTANT.$HOMESERVER_DOMAIN EOFBA } # FreshRSS function installFreshRSS() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory freshrss "FreshRSS" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName freshrss-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName freshrss-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/freshrss mkdir $HSHQ_STACKS_DIR/freshrss/db mkdir $HSHQ_STACKS_DIR/freshrss/dbexport mkdir $HSHQ_STACKS_DIR/freshrss/data mkdir $HSHQ_STACKS_DIR/freshrss/extensions chmod 777 $HSHQ_STACKS_DIR/freshrss/dbexport initServicesCredentials set +e addUserMailu alias $FRESHRSS_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$FRESHRSS_INIT_ENV" = "true" ]; then sendEmail -s "FreshRSS Admin Login Info" -b "FreshRSS Admin Username: $FRESHRSS_ADMIN_USERNAME\nFreshRSS Admin Password: $FRESHRSS_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" FRESHRSS_INIT_ENV=true updateConfigVar FRESHRSS_INIT_ENV $FRESHRSS_INIT_ENV fi FRESHRSS_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $FRESHRSS_OIDC_CLIENT_SECRET | tr -d ':\n') set +e outputConfigFreshRSS oidcBlock=$(cat $HOME/freshrss.oidc) rm -f $HOME/freshrss.oidc insertOIDCClientAuthelia freshrss "$oidcBlock" set +e installStack freshrss freshrss-app "apache2 -D FOREGROUND" $HOME/freshrss.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase FreshRSS postgres freshrss-db $FRESHRSS_DATABASE_NAME $FRESHRSS_DATABASE_USER $FRESHRSS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/freshrss/dbexport $FRESHRSS_DATABASE_READONLYUSER $FRESHRSS_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_FRESHRSS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://freshrss-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FRESHRSS $MANAGETLS_FRESHRSS "$is_integrate_hshq" $NETDEFAULT_FRESHRSS "$inner_block" insertSubAuthelia $SUB_FRESHRSS.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll freshrss "$FMLNAME_FRESHRSS" $USERTYPE_FRESHRSS "https://$SUB_FRESHRSS.$HOMESERVER_DOMAIN" "freshrss.png" "$(getHeimdallOrderFromSub $SUB_FRESHRSS $USERTYPE_FRESHRSS)" restartAllCaddyContainers checkAddDBConnection true freshrss "$FMLNAME_FRESHRSS" postgres freshrss-db $FRESHRSS_DATABASE_NAME $FRESHRSS_DATABASE_USER $FRESHRSS_DATABASE_USER_PASSWORD fi } function outputConfigFreshRSS() { cat < $HOME/freshrss-compose.yml $STACK_VERSION_PREFIX freshrss $(getScriptStackVersion freshrss) services: freshrss-db: image: $(getScriptImageByContainerName freshrss-db) container_name: freshrss-db hostname: freshrss-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-freshrss-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/freshrss/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/freshrss/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.freshrss-hourly-db.schedule=@every 1h" - "ofelia.job-exec.freshrss-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.freshrss-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.freshrss-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.freshrss-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.freshrss-hourly-db.email-from=FreshRSS Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.freshrss-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.freshrss-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.freshrss-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.freshrss-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.freshrss-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.freshrss-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.freshrss-monthly-db.email-from=FreshRSS Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.freshrss-monthly-db.mail-only-on-error=false" freshrss-app: image: $(getScriptImageByContainerName freshrss-app) container_name: freshrss-app hostname: freshrss-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true logging: driver: "json-file" options: max-size: "10m" depends_on: - freshrss-db networks: - int-freshrss-net - dock-ext-net volumes: - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-freshrss-data:/var/www/FreshRSS/data - v-freshrss-extensions:/var/www/FreshRSS/extensions environment: FRESHRSS_INSTALL: |- --auth_type http_auth --environment production --base_url \${BASE_URL} --language en --title FreshRSS --api_enabled --db-type pgsql --db-host \${DB_HOST} --db-user \${DB_USER} --db-password \${DB_PASSWORD} --db-base \${DB_BASE} --default_user \${ADMIN_USERNAME} FRESHRSS_USER: |- --api_password \${ADMIN_API_PASSWORD} --email \${ADMIN_EMAIL} --language en --password \${ADMIN_PASSWORD} --user \${ADMIN_USERNAME} volumes: v-freshrss-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/freshrss/data v-freshrss-extensions: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/freshrss/extensions networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-freshrss-net: driver: bridge internal: true ipam: driver: default EOFBA cat < $HOME/freshrss.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID BASE_URL=https://$SUB_FRESHRSS.$HOMESERVER_DOMAIN CRON_MIN=3,33 TRUSTED_PROXY=10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 POSTGRES_DB=$FRESHRSS_DATABASE_NAME POSTGRES_USER=$FRESHRSS_DATABASE_USER POSTGRES_PASSWORD=$FRESHRSS_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' ADMIN_USERNAME=$LDAP_ADMIN_USER_USERNAME ADMIN_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS ADMIN_PASSWORD=$LDAP_ADMIN_USER_PASSWORD ADMIN_API_PASSWORD=$LDAP_ADMIN_USER_PASSWORD DB_HOST=freshrss-db DB_BASE=freshrssdb DB_PASSWORD=$FRESHRSS_DATABASE_USER_PASSWORD DB_USER=freshrss-user OIDC_ENABLED=1 OIDC_PROVIDER_METADATA_URL=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/.well-known/openid-configuration OIDC_CLIENT_ID=freshrss OIDC_CLIENT_SECRET=$FRESHRSS_OIDC_CLIENT_SECRET OIDC_CLIENT_CRYPTO_KEY=$(pwgen -c -n 32 1) OIDC_REMOTE_USER_CLAIM=preferred_username OIDC_SCOPES=openid groups email profile OIDC_X_FORWARDED_HEADERS=X-Forwarded-Host X-Forwarded-Proto EOFBA cat < $HOME/freshrss.oidc # Authelia OIDC Client freshrss BEGIN - client_id: freshrss client_name: FreshRSS client_secret: $FRESHRSS_OIDC_CLIENT_SECRET_HASH public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth redirect_uris: - https://$SUB_FRESHRSS.$HOMESERVER_DOMAIN/i/oidc/ scopes: - openid - groups - email - profile userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client freshrss END EOFIM } function performUpdateFreshRSS() { perform_stack_name=freshrss prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v5 curImageList=postgres:15.0-bullseye,freshrss/freshrss:1.23.1 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="freshrss/freshrss:1.23.1,mirror.gcr.io/freshrss/freshrss:1.26.3" ;; 2) newVer=v5 curImageList=postgres:15.0-bullseye,freshrss/freshrss:1.24.1 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="freshrss/freshrss:1.24.1,mirror.gcr.io/freshrss/freshrss:1.26.3" ;; 3) newVer=v5 curImageList=postgres:15.0-bullseye,freshrss/freshrss:1.24.3 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="freshrss/freshrss:1.24.3,mirror.gcr.io/freshrss/freshrss:1.26.3" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,freshrss/freshrss:1.26.1 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="freshrss/freshrss:1.26.1,mirror.gcr.io/freshrss/freshrss:1.26.3" ;; 5) newVer=v6 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/freshrss/freshrss:1.26.3 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/freshrss/freshrss:1.26.3,mirror.gcr.io/freshrss/freshrss:1.27.1" ;; 6) newVer=v6 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/freshrss/freshrss:1.27.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/freshrss/freshrss:1.27.1,mirror.gcr.io/freshrss/freshrss:1.27.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Keila function installKeila() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory keila "Keila" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName keila-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName keila-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName keila-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/keila mkdir $HSHQ_STACKS_DIR/keila/db mkdir $HSHQ_STACKS_DIR/keila/dbexport mkdir $HSHQ_STACKS_DIR/keila/uploads chmod 777 $HSHQ_STACKS_DIR/keila/dbexport initServicesCredentials KEILA_SECRET_KEY_BASE=$(pwgen -c -n 64 1) set +e addUserMailu alias $KEILA_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$KEILA_INIT_ENV" = "true" ]; then sendEmail -s "Keila Admin Login Info" -b "Keila Admin Username: $KEILA_ADMIN_EMAIL_ADDRESS\nKeila Admin Password: $KEILA_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" KEILA_INIT_ENV=true updateConfigVar KEILA_INIT_ENV $KEILA_INIT_ENV fi outputConfigKeila installStack keila keila-app "Access KeilaWeb.Endpoint at" $HOME/keila.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 performIntegrationKeila addReadOnlyUserToDatabase Keila postgres keila-db $KEILA_DATABASE_NAME $KEILA_DATABASE_USER $KEILA_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/keila/dbexport $KEILA_DATABASE_READONLYUSER $KEILA_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_KEILA.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://keila-app:4000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KEILA $MANAGETLS_KEILA "$is_integrate_hshq" $NETDEFAULT_KEILA "$inner_block" insertSubAuthelia $SUB_KEILA.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll keila "$FMLNAME_KEILA" $USERTYPE_KEILA "https://$SUB_KEILA.$HOMESERVER_DOMAIN" "keila.png" "$(getHeimdallOrderFromSub $SUB_KEILA $USERTYPE_KEILA)" restartAllCaddyContainers checkAddDBConnection true keila "$FMLNAME_KEILA" postgres keila-db $KEILA_DATABASE_NAME $KEILA_DATABASE_USER $KEILA_DATABASE_USER_PASSWORD fi } function outputConfigKeila() { cat < $HOME/keila-compose.yml $STACK_VERSION_PREFIX keila $(getScriptStackVersion keila) services: keila-db: image: $(getScriptImageByContainerName keila-db) container_name: keila-db hostname: keila-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-keila-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/keila/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/keila/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.keila-hourly-db.schedule=@every 1h" - "ofelia.job-exec.keila-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.keila-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.keila-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.keila-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.keila-hourly-db.email-from=Keila Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.keila-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.keila-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.keila-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.keila-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.keila-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.keila-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.keila-monthly-db.email-from=Keila Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.keila-monthly-db.mail-only-on-error=false" keila-app: image: $(getScriptImageByContainerName keila-app) container_name: keila-app hostname: keila-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - keila-db networks: - int-keila-net - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-keila-uploads:/uploads keila-mcp: image: $(getScriptImageByContainerName keila-mcp) container_name: keila-mcp hostname: keila-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-keila-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-keila-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/keila/db v-keila-uploads: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/keila/uploads networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true int-keila-net: driver: bridge internal: true ipam: driver: default EOFBA cat < $HOME/keila.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_DB=$KEILA_DATABASE_NAME POSTGRES_USER=$KEILA_DATABASE_USER POSTGRES_PASSWORD=$KEILA_DATABASE_USER_PASSWORD SECRET_KEY_BASE=$KEILA_SECRET_KEY_BASE DB_URL=postgres://$KEILA_DATABASE_USER:$KEILA_DATABASE_USER_PASSWORD@keila-db/$KEILA_DATABASE_NAME URL_HOST=$SUB_KEILA.$HOMESERVER_DOMAIN URL_SCHEMA=https MAILER_SMTP_HOST=$SMTP_HOSTNAME MAILER_SMTP_PORT=587 MAILER_SMTP_USER=$EMAIL_SMTP_EMAIL_ADDRESS MAILER_SMTP_PASSWORD=$EMAIL_SMTP_PASSWORD MAILER_SMTP_FROM_EMAIL=$EMAIL_SMTP_EMAIL_ADDRESS MAILER_SMTP_FROM_NAME=$HOMESERVER_NAME MAILER_ENABLE_STARTTLS=true KEILA_USER=$KEILA_ADMIN_EMAIL_ADDRESS KEILA_PASSWORD=$KEILA_ADMIN_PASSWORD DISABLE_REGISTRATION=true USER_CONTENT_DIR=/uploads KEILA_BASE_URL=http://keila-app:4000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false KEILA_PUBLIC_URL=$SUB_KEILA.$HOMESERVER_DOMAIN EOFBA } function performIntegrationKeila() { set +e local usr project_esc usr=$(printf '%s' "$KEILA_ADMIN_EMAIL_ADDRESS" | sed 's/\\/\\\\/g; s/"/\\"/g') project_esc=$(printf '%s' "$HOMESERVER_NAME" | sed 's/\\/\\\\/g; s/"/\\"/g') local elixir="import Ecto.Query admin = Keila.Auth.find_user_by_email(\"$usr\") unless admin, do: throw(:admin_not_found) project = Keila.Repo.one(from p in Keila.Projects.Project, where: p.name == \"$project_esc\") project = case project do %Keila.Projects.Project{} -> project nil -> {:ok, project} = Keila.Projects.create_project(admin.id, %{name: \"$project_esc\"}); project end sender = Keila.Repo.one(from s in Keila.Mailings.Sender, where: s.project_id == ^project.id) unless sender do smtp_port = (System.get_env(\"MAILER_SMTP_PORT\") || \"587\") |> String.to_integer() smtp_username = System.get_env(\"MAILER_SMTP_USER\") smtp_password = System.get_env(\"MAILER_SMTP_PASSWORD\") tls_mode = cond do System.get_env(\"MAILER_ENABLE_STARTTLS\") in [\"1\", \"true\", \"TRUE\"] -> \"starttls\" System.get_env(\"MAILER_ENABLE_SSL\") in [\"1\", \"true\", \"TRUE\"] -> \"tls\" true -> \"\" end {:ok, _sender} = Keila.Mailings.create_sender(project.id, %{ name: project.name, from_email: System.fetch_env!(\"MAILER_SMTP_FROM_EMAIL\"), from_name: System.get_env(\"MAILER_SMTP_FROM_NAME\") || project.name, config: %{ type: \"smtp\", smtp_relay: System.fetch_env!(\"MAILER_SMTP_HOST\"), smtp_port: smtp_port, smtp_username: smtp_username, smtp_password: smtp_password, smtp_tls_mode: tls_mode, smtp_auth_method: (if smtp_username && smtp_password, do: \"password\", else: \"none\") } }) end Keila.Repo.delete_all( from t in Keila.Auth.Token, where: t.user_id == ^admin.id and t.scope == \"api\" and fragment(\"?->>?\", t.data, \"project_id\") == ^to_string(project.id) ) {:ok, key} = Keila.Auth.create_api_key(admin.id, project.id, \"$project_esc\") IO.puts(key.key)" KEILA_ADMIN_API_KEY=$(docker exec keila-app /opt/app/bin/keila rpc "$elixir" 2>/dev/null | grep -E '^[A-Za-z0-9_-]{43}$') updateConfigVar KEILA_ADMIN_API_KEY "$KEILA_ADMIN_API_KEY" } function performUpdateKeila() { perform_stack_name=keila prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=postgres:15.0-bullseye,pentacent/keila:0.13.1 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="pentacent/keila:0.13.1,pentacent/keila:0.14.11" ;; 2) newVer=v4 curImageList=postgres:15.0-bullseye,pentacent/keila:0.14.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="pentacent/keila:0.14.0,pentacent/keila:0.15.0" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,pentacent/keila:0.14.11 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="pentacent/keila:0.14.11,pentacent/keila:0.15.0" ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,pentacent/keila:0.15.0 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="pentacent/keila:0.15.0,pentacent/keila:0.17.1" ;; 5) newVer=v6 curImageList=postgres:15.0-bullseye,pentacent/keila:0.17.1 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="pentacent/keila:0.17.1,mirror.gcr.io/pentacent/keila:0.30.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfKeilaV6AddMCP perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 6) newVer=v6 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/pentacent/keila:0.30.2,ghcr.io/homeserverhq/keila-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/pentacent/keila:0.30.2,mirror.gcr.io/pentacent/keila:0.30.2" image_update_map[2]="ghcr.io/homeserverhq/keila-mcp:v2,ghcr.io/homeserverhq/keila-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfKeilaV6AddMCP() { cat < $HOME/keila-compose.yml $STACK_VERSION_PREFIX keila $(getScriptStackVersion keila) services: keila-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: keila-db hostname: keila-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-keila-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/keila/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/keila/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.keila-hourly-db.schedule=@every 1h" - "ofelia.job-exec.keila-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.keila-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.keila-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.keila-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.keila-hourly-db.email-from=Keila Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.keila-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.keila-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.keila-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.keila-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.keila-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.keila-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.keila-monthly-db.email-from=Keila Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.keila-monthly-db.mail-only-on-error=false" keila-app: image: mirror.gcr.io/pentacent/keila:0.30.2 container_name: keila-app hostname: keila-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - keila-db networks: - int-keila-net - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-keila-uploads:/uploads keila-mcp: image: ghcr.io/homeserverhq/keila-mcp:v2 container_name: keila-app hostname: keila-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-keila-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-keila-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/keila/db v-keila-uploads: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/keila/uploads networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true int-keila-net: driver: bridge internal: true ipam: driver: default EOFBA cat < $HOME/keila.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_DB=$KEILA_DATABASE_NAME POSTGRES_USER=$KEILA_DATABASE_USER POSTGRES_PASSWORD=$KEILA_DATABASE_USER_PASSWORD SECRET_KEY_BASE=$KEILA_SECRET_KEY_BASE DB_URL=postgres://$KEILA_DATABASE_USER:$KEILA_DATABASE_USER_PASSWORD@keila-db/$KEILA_DATABASE_NAME URL_HOST=$SUB_KEILA.$HOMESERVER_DOMAIN URL_SCHEMA=https MAILER_SMTP_HOST=$SMTP_HOSTNAME MAILER_SMTP_PORT=$SMTP_HOSTPORT MAILER_SMTP_USER=$EMAIL_SMTP_EMAIL_ADDRESS MAILER_SMTP_PASSWORD=$EMAIL_SMTP_PASSWORD MAILER_SMTP_FROM_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS MAILER_ENABLE_STARTTLS=true KEILA_USER=$KEILA_ADMIN_EMAIL_ADDRESS KEILA_PASSWORD=$KEILA_ADMIN_PASSWORD DISABLE_REGISTRATION=true USER_CONTENT_DIR=/uploads KEILA_BASE_URL=http://keila-app:4000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false KEILA_PUBLIC_URL=$SUB_KEILA.$HOMESERVER_DOMAIN EOFBA } # Wallabag function installWallabag() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory wallabag "Wallabag" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wallabag-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wallabag-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wallabag-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/wallabag mkdir $HSHQ_STACKS_DIR/wallabag/db mkdir $HSHQ_STACKS_DIR/wallabag/dbexport mkdir $HSHQ_STACKS_DIR/wallabag/images chmod 777 $HSHQ_STACKS_DIR/wallabag/dbexport initServicesCredentials if [ -z "$WALLABAG_ENV_SECRET" ]; then WALLABAG_ENV_SECRET=$(pwgen -c -n 30 1) updateConfigVar WALLABAG_ENV_SECRET $WALLABAG_ENV_SECRET fi if [ -z "$WALLABAG_REDIS_PASSWORD" ]; then WALLABAG_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar WALLABAG_REDIS_PASSWORD $WALLABAG_REDIS_PASSWORD fi set +e addUserMailu alias $WALLABAG_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$WALLABAG_INIT_ENV" = "true" ]; then sendEmail -s "Wallabag Admin Login Info" -b "Wallabag Admin Username: $WALLABAG_ADMIN_USERNAME\nWallabag Admin Password: $WALLABAG_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" WALLABAG_INIT_ENV=true updateConfigVar WALLABAG_INIT_ENV $WALLABAG_INIT_ENV fi outputConfigWallabag installStack wallabag wallabag-app "wallabag is ready" $HOME/wallabag.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 5 docker exec -t wallabag-app /var/www/wallabag/bin/console wallabag:install --env=prod --no-interaction > /dev/null 2>&1 docker exec -t wallabag-app /var/www/wallabag/bin/console doctrine:migrations:migrate --env=prod --no-interaction > /dev/null 2>&1 docker exec -t wallabag-app /var/www/wallabag/bin/console fos:user:create --env=prod --super-admin $WALLABAG_ADMIN_USERNAME $WALLABAG_ADMIN_EMAIL_ADDRESS $WALLABAG_ADMIN_PASSWORD > /dev/null 2>&1 docker exec -t wallabag-app /var/www/wallabag/bin/console fos:user:deactivate --env=prod wallabag > /dev/null 2>&1 docker exec wallabag-db /dbexport/setupRedis.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/wallabag/dbexport/setupRedis.sh addReadOnlyUserToDatabase Wallabag postgres wallabag-db $WALLABAG_DATABASE_NAME $WALLABAG_DATABASE_USER $WALLABAG_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/wallabag/dbexport $WALLABAG_DATABASE_READONLYUSER $WALLABAG_DATABASE_READONLYUSER_PASSWORD startStopStack wallabag stop sleep 3 startStopStack wallabag start inner_block="" inner_block=$inner_block">>https://$SUB_WALLABAG.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://wallabag-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_WALLABAG $MANAGETLS_WALLABAG "$is_integrate_hshq" $NETDEFAULT_WALLABAG "$inner_block" insertSubAuthelia $SUB_WALLABAG.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll wallabag "$FMLNAME_WALLABAG" $USERTYPE_WALLABAG "https://$SUB_WALLABAG.$HOMESERVER_DOMAIN" "wallabag.png" "$(getHeimdallOrderFromSub $SUB_WALLABAG $USERTYPE_WALLABAG)" restartAllCaddyContainers checkAddDBConnection true wallabag "$FMLNAME_WALLABAG" postgres wallabag-db $WALLABAG_DATABASE_NAME $WALLABAG_DATABASE_USER $WALLABAG_DATABASE_USER_PASSWORD fi } function outputConfigWallabag() { cat < $HOME/wallabag-compose.yml $STACK_VERSION_PREFIX wallabag $(getScriptStackVersion wallabag) services: wallabag-db: image: $(getScriptImageByContainerName wallabag-db) container_name: wallabag-db hostname: wallabag-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-wallabag-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wallabag/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wallabag/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.wallabag-hourly-db.schedule=@every 1h" - "ofelia.job-exec.wallabag-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.wallabag-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wallabag-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wallabag-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wallabag-hourly-db.email-from=Wallabag Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wallabag-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.wallabag-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.wallabag-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.wallabag-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.wallabag-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.wallabag-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.wallabag-monthly-db.email-from=Wallabag Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.wallabag-monthly-db.mail-only-on-error=false" wallabag-redis: image: $(getScriptImageByContainerName wallabag-redis) container_name: wallabag-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $WALLABAG_REDIS_PASSWORD --appendonly no networks: - int-wallabag-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro wallabag-app: image: $(getScriptImageByContainerName wallabag-app) container_name: wallabag-app hostname: wallabag-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - wallabag-db - wallabag-redis networks: - dock-ext-net - dock-internalmail-net - int-wallabag-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/wallabag/images:/var/www/wallabag/web/assets/images networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-wallabag-net: driver: bridge internal: true ipam: driver: default EOFBA cat < $HOME/wallabag.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_DB=$WALLABAG_DATABASE_NAME POSTGRES_USER=$WALLABAG_DATABASE_USER POSTGRES_PASSWORD=$WALLABAG_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' POPULATE_DATABASE=false SYMFONY__ENV__DATABASE_DRIVER=pdo_pgsql SYMFONY__ENV__DATABASE_HOST=wallabag-db SYMFONY__ENV__DATABASE_PORT=5432 SYMFONY__ENV__DATABASE_NAME=$WALLABAG_DATABASE_NAME SYMFONY__ENV__DATABASE_USER=$WALLABAG_DATABASE_USER SYMFONY__ENV__DATABASE_PASSWORD=$WALLABAG_DATABASE_USER_PASSWORD SYMFONY__ENV__MAILER_DSN=smtp://$SMTP_HOSTNAME:$SMTP_HOSTPORT SYMFONY__ENV__FROM_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS SYMFONY__ENV__DOMAIN_NAME=https://$SUB_WALLABAG.$HOMESERVER_DOMAIN SYMFONY__ENV__SERVER_NAME=$HOMESERVER_NAME SYMFONY__ENV__SECRET=$WALLABAG_ENV_SECRET SYMFONY__ENV__TWOFACTOR_SENDER=$EMAIL_ADMIN_EMAIL_ADDRESS SYMFONY__ENV__REDIS_HOST=wallabag-redis SYMFONY__ENV__REDIS_PASSWORD=$WALLABAG_REDIS_PASSWORD EOFBA cat < $HSHQ_STACKS_DIR/wallabag/dbexport/setupRedis.sh #!/bin/bash PGPASSWORD=$WALLABAG_DATABASE_USER_PASSWORD sqlcmd="update wallabag_internal_setting set value=1 where name='import_with_redis';" echo "\$sqlcmd" | psql -U $WALLABAG_DATABASE_USER $WALLABAG_DATABASE_NAME EOFPT chmod +x $HSHQ_STACKS_DIR/wallabag/dbexport/setupRedis.sh } function performUpdateWallabag() { perform_stack_name=wallabag prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,wallabag/wallabag:2.6.8 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[2]="wallabag/wallabag:2.6.8,wallabag/wallabag:2.6.10" ;; 2) newVer=v3 curImageList=postgres:15.0-bullseye,bitnami/redis:7.0.5,wallabag/wallabag:2.6.9 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" image_update_map[2]="wallabag/wallabag:2.6.9,wallabag/wallabag:2.6.10" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,bitnami/redis:7.4.2,wallabag/wallabag:2.6.10 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[2]="wallabag/wallabag:2.6.10,mirror.gcr.io/wallabag/wallabag:2.6.13" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfWallabagFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v5 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/wallabag/wallabag:2.6.13 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/wallabag/wallabag:2.6.13,mirror.gcr.io/wallabag/wallabag:2.6.14" ;; 5) newVer=v5 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/wallabag/wallabag:2.6.14 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/wallabag/wallabag:2.6.14,mirror.gcr.io/wallabag/wallabag:2.6.14" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfWallabagFixRedisCompose() { replaceRedisBlock wallabag wallabag-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/wallabag/redis/* } # Jupyter function installJupyter() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory jupyter "Jupyter" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName jupyter) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/jupyter mkdir $HSHQ_STACKS_DIR/jupyter/notebooks initServicesCredentials outputConfigJupyter set +e installStack jupyter jupyter "Jupyter .* is running at" $HOME/jupyter.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi set -e if ! [ "$JUPYTER_INIT_ENV" = "true" ]; then sendEmail -s "Jupyter Admin Login Info" -b "Jupyter Admin Password: $JUPYTER_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" JUPYTER_INIT_ENV=true updateConfigVar JUPYTER_INIT_ENV $JUPYTER_INIT_ENV fi sleep 3 inner_block="" inner_block=$inner_block">>https://$SUB_JUPYTER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://jupyter:8888 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_JUPYTER $MANAGETLS_JUPYTER "$is_integrate_hshq" $NETDEFAULT_JUPYTER "$inner_block" insertSubAuthelia $SUB_JUPYTER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll jupyter "$FMLNAME_JUPYTER" $USERTYPE_JUPYTER "https://$SUB_JUPYTER.$HOMESERVER_DOMAIN" "jupyter.png" "$(getHeimdallOrderFromSub $SUB_JUPYTER $USERTYPE_JUPYTER)" restartAllCaddyContainers fi } function outputConfigJupyter() { cat < $HOME/jupyter-compose.yml $STACK_VERSION_PREFIX jupyter $(getScriptStackVersion jupyter) services: jupyter: image: $(getScriptImageByContainerName jupyter) container_name: jupyter hostname: jupyter restart: unless-stopped env_file: stack.env command: '/bin/bash -c "conda install jupyter -y --quiet && mkdir -p /opt/notebooks && jupyter notebook --notebook-dir=/opt/notebooks --ip="*" --port=8888 --no-browser --allow-root --NotebookApp.token=$JUPYTER_ADMIN_PASSWORD"' security_opt: - no-new-privileges:true tty: true stdin_open: true networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/jupyter/notebooks:/opt/notebooks networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFDZ cat < $HOME/jupyter.env TZ=\${PORTAINER_TZ} EOFDZ } function performUpdateJupyter() { perform_stack_name=jupyter prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=continuumio/anaconda3:2023.09-0 image_update_map[0]="continuumio/anaconda3:2023.09-0,continuumio/anaconda3:2024.10-1" ;; 2) newVer=v4 curImageList=continuumio/anaconda3:2024.02-1 image_update_map[0]="continuumio/anaconda3:2024.02-1,continuumio/anaconda3:2024.10-1" ;; 3) newVer=v4 curImageList=continuumio/anaconda3:2024.06-1 image_update_map[0]="continuumio/anaconda3:2024.06-1,continuumio/anaconda3:2024.10-1" ;; 4) newVer=v4 curImageList=continuumio/anaconda3:2024.10-1 image_update_map[0]="continuumio/anaconda3:2024.10-1,continuumio/anaconda3:2024.10-1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Paperless function installPaperless() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory paperless "Paperless" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-tika) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-gotenberg) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-ai) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-gpt) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName paperless-mcp) if [ $? -ne 0 ]; then return 1 fi if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/paperless mkdir $HSHQ_STACKS_DIR/paperless/db mkdir $HSHQ_STACKS_DIR/paperless/dbexport mkdir $HSHQ_STACKS_DIR/paperless/redis mkdir $HSHQ_STACKS_DIR/paperless/data mkdir $HSHQ_STACKS_DIR/paperless/media mkdir $HSHQ_STACKS_DIR/paperless/export mkdir $HSHQ_STACKS_DIR/paperless/consume mkdir $HSHQ_STACKS_DIR/paperless/ai mkdir $HSHQ_STACKS_DIR/paperless/gpt mkdir $HSHQ_STACKS_DIR/paperless/gpt/prompts mkdir $HSHQ_STACKS_DIR/paperless/gpt/hocr mkdir $HSHQ_STACKS_DIR/paperless/gpt/pdf mkdir $HSHQ_STACKS_DIR/paperless/public_objects chmod 777 $HSHQ_STACKS_DIR/paperless/dbexport initServicesCredentials if [ -z "$PAPERLESS_SECRET_KEY" ]; then PAPERLESS_SECRET_KEY=$(pwgen -c -n 64 1) updateConfigVar PAPERLESS_SECRET_KEY $PAPERLESS_SECRET_KEY fi if [ -z "$PAPERLESS_REDIS_PASSWORD" ]; then PAPERLESS_REDIS_PASSWORD=$(pwgen -c -n 32 1) updateConfigVar PAPERLESS_REDIS_PASSWORD $PAPERLESS_REDIS_PASSWORD fi PAPERLESS_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $PAPERLESS_OIDC_CLIENT_SECRET | tr -d ':\n') set +e addUserMailu alias $PAPERLESS_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$PAPERLESS_INIT_ENV" = "true" ]; then sendEmail -s "Paperless Admin Login Info" -b "Paperless Admin Username: $PAPERLESS_ADMIN_USERNAME\nPaperless Admin Password: $PAPERLESS_ADMIN_PASSWORD\nZettelrobbe Admin Username: $PAPERLESS_AI_ADMIN_USERNAME\nZettelrobbe Admin Password: $PAPERLESS_AI_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" PAPERLESS_INIT_ENV=true updateConfigVar PAPERLESS_INIT_ENV $PAPERLESS_INIT_ENV fi outputConfigPaperless oidcBlock=$(cat $HOME/paperless.oidc) rm -f $HOME/paperless.oidc insertOIDCClientAuthelia paperless "$oidcBlock" set +e installStack paperless paperless-app "celery@paperless-app ready" $HOME/paperless.env 10 600 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addMCPServerLiteLLM "paperless" "pap" "http://paperless-mcp:80/mcp" http none "" addReadOnlyUserToDatabase Paperless postgres paperless-db $PAPERLESS_DATABASE_NAME $PAPERLESS_DATABASE_USER $PAPERLESS_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/paperless/dbexport $PAPERLESS_DATABASE_READONLYUSER $PAPERLESS_DATABASE_READONLYUSER_PASSWORD docker exec paperless-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/paperless/dbexport/setupDBSettings.sh if [ $? -ne 0 ]; then echo "ERROR: There was a problem with the paperless-ai container." return 1 fi inner_block="" inner_block=$inner_block">>https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://paperless-app:8000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PAPERLESS_APP $MANAGETLS_PAPERLESS_APP "$is_integrate_hshq" $NETDEFAULT_PAPERLESS_APP "$inner_block" insertSubAuthelia $SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_BASEHEADER\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com cdn.tailwindcss.com unpkg.com cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com unpkg.com; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com cdn.tailwindcss.com unpkg.com; img-src 'self' *.${HOMESERVER_DOMAIN} data:; font-src 'self' cdnjs.cloudflare.com data:; frame-src 'self' *.${HOMESERVER_DOMAIN} data: blob:; connect-src 'self' *.${HOMESERVER_DOMAIN} data: api.github.com; object-src 'none'; upgrade-insecure-requests; frame-ancestors 'self' *.${HOMESERVER_DOMAIN};\"\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://paperless-ai:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PAPERLESS_AI_WEB $MANAGETLS_PAPERLESS_AI_WEB "$is_integrate_hshq" $NETDEFAULT_PAPERLESS_AI_WEB "$inner_block" insertSubAuthelia $SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>forward_auth https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>uri /api/verify?rd=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN\n" inner_block=$inner_block">>>>>>>>copy_headers Remote-User Remote-Groups Remote-Name Remote-Email\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://paperless-gpt:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PAPERLESS_GPT_WEB $MANAGETLS_PAPERLESS_GPT_WEB "$is_integrate_hshq" $NETDEFAULT_PAPERLESS_GPT_WEB "$inner_block" insertSubAuthelia $SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll paperless "$FMLNAME_PAPERLESS_AI_WEB" $USERTYPE_PAPERLESS_AI_WEB "https://$SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN" "zettelrobbe.png" "$(getHeimdallOrderFromSub $SUB_PAPERLESS_AI_WEB $USERTYPE_PAPERLESS_AI_WEB)" insertEnableSvcAll paperless "$FMLNAME_PAPERLESS_APP" $USERTYPE_PAPERLESS_APP "https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN" "paperless.png" "$(getHeimdallOrderFromSub $SUB_PAPERLESS_APP $USERTYPE_PAPERLESS_APP)" insertEnableSvcHeimdall paperless "$FMLNAME_PAPERLESS_GPT_WEB" $USERTYPE_PAPERLESS_GPT_WEB "https://$SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN" "paperless-gpt.png" true "$(getHeimdallOrderFromSub $SUB_PAPERLESS_GPT_WEB $USERTYPE_PAPERLESS_GPT_WEB)" restartAllCaddyContainers checkAddDBConnection true paperless "$FMLNAME_PAPERLESS_APP" postgres paperless-db $PAPERLESS_DATABASE_NAME $PAPERLESS_DATABASE_USER $PAPERLESS_DATABASE_USER_PASSWORD fi sleep 5 performIntegrationPaperlessAI performWorkflowsIntegrationPaperless } function outputConfigPaperless() { outputComposePaperless rm -f $HOME/paperless.env cat < $HOME/paperless.env POSTGRES_DB=$PAPERLESS_DATABASE_NAME POSTGRES_USER=$PAPERLESS_DATABASE_USER POSTGRES_PASSWORD=$PAPERLESS_DATABASE_USER_PASSWORD PAPERLESS_REDIS=redis://:$PAPERLESS_REDIS_PASSWORD@paperless-redis:6379 PAPERLESS_DBHOST=paperless-db PAPERLESS_DBNAME=$PAPERLESS_DATABASE_NAME PAPERLESS_DBUSER=$PAPERLESS_DATABASE_USER PAPERLESS_DBPASS=$PAPERLESS_DATABASE_USER_PASSWORD PAPERLESS_SECRET_KEY=$PAPERLESS_SECRET_KEY PAPERLESS_URL=https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN PAPERLESS_ADMIN_USER=$PAPERLESS_ADMIN_USERNAME PAPERLESS_ADMIN_MAIL=$PAPERLESS_ADMIN_EMAIL_ADDRESS PAPERLESS_ADMIN_PASSWORD=$PAPERLESS_ADMIN_PASSWORD PAPERLESS_TIKA_ENABLED=true PAPERLESS_TIKA_ENDPOINT=http://paperless-tika:9998 PAPERLESS_TIKA_GOTENBERG_ENDPOINT=http://paperless-gotenberg:3000 PAPERLESS_EMAIL_HOST=$SMTP_HOSTNAME PAPERLESS_EMAIL_PORT=$SMTP_HOSTPORT PAPERLESS_EMAIL_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS PAPERLESS_EMAIL_USE_TLS=true PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS=true PAPERLESS_SOCIAL_AUTO_SIGNUP=true PAPERLESS_SOCIALACCOUNT_PROVIDERS={"openid_connect":{"SCOPE":["openid","profile","email","groups"],"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"authelia","name":"Authelia","client_id":"paperless","secret":"$PAPERLESS_OIDC_CLIENT_SECRET","settings":{"server_url":"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN","token_auth_method":"client_secret_basic"}}]}} PAPERLESS_CONSUMER_RECURSIVE=true PYTHON_VER=python3.12 ANONYMIZED_TELEMETRY=False PAPERLESS_BASE_URL=http://paperless-app:8000 PAPERLESS_API_TOKEN=$PAPERLESS_API_TOKEN PAPERLESS_PUBLIC_URL=https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN PAPERLESS_DEFAULT_OBJECT_ACCESS_GROUP=primaryusers MANUAL_TAG=paperless-gpt AUTO_TAG=paperless-gpt-auto LLM_PROVIDER=openai LLM_MODEL=Vision OPENAI_API_KEY=$LITELLM_AUTH_KEY OPENAI_BASE_URL=http://litellm-proxy:4000/v1 OCR_PROVIDER=docling DOCLING_URL=http://docling-app:5001 DOCLING_IMAGE_EXPORT_MODE=placeholder DOCLING_OCR_PIPELINE=standard DOCLING_OCR_ENGINE=easyocr VISION_LLM_PROVIDER=openai VISION_LLM_MODEL=Vision OCR_PROCESS_MODE=image PDF_SKIP_EXISTING_OCR=false CREATE_LOCAL_HOCR=false LOCAL_HOCR_PATH=/app/hocr CREATE_LOCAL_PDF=false LOCAL_PDF_PATH=/app/pdf PDF_UPLOAD=false PDF_REPLACE=false PDF_COPY_METADATA=true PDF_OCR_TAGGING=true PDF_OCR_COMPLETE_TAG=paperless-gpt-ocr-complete AUTO_OCR_TAG=paperless-gpt-ocr-auto PAPERLESS_OCR_PAGES=0 PAPERLESS_OCR_MODE=auto OCR_LIMIT_PAGES=0 LOG_LEVEL=info PAPERLESS_API_KEY=$PAPERLESS_API_TOKEN USERMAP_UID=82 USERMAP_GID=82 PUID=82 PGID=82 PAPERLESS_AI_PORT=3000 RAG_SERVICE_URL=http://localhost:8000 RAG_SERVICE_ENABLED=true PAPERLESS_API_URL=http://paperless-app:8000 PAPERLESS_EXTRA_TEXT_MIMETYPES={"text/markdown": ".md", "text/x-markdown": ".md"} MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false PAPERLESS_AI_ENABLED=true PAPERLESS_AI_LLM_EMBEDDING_BACKEND=openai-like PAPERLESS_AI_LLM_EMBEDDING_MODEL=Embed PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT=http://litellm-proxy:4000/v1 PAPERLESS_AI_LLM_BACKEND=openai-like PAPERLESS_AI_LLM_MODEL=LongContext PAPERLESS_AI_LLM_API_KEY=$LITELLM_AUTH_KEY PAPERLESS_AI_LLM_ENDPOINT=http://litellm-proxy:4000/v1 PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS=true PAPERLESS_OCR_USER_ARGS='{"invalidate_digital_signatures": true}' EOFJT rm -f $HOME/paperless.oidc cat < $HOME/paperless.oidc # Authelia OIDC Client paperless BEGIN - client_id: paperless client_name: Paperless client_secret: '$PAPERLESS_OIDC_CLIENT_SECRET_HASH' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth require_pkce: true pkce_challenge_method: S256 redirect_uris: - https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN/accounts/oidc/authelia/login/callback/ scopes: - openid - profile - email - groups userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client paperless END EOFIM outputEnvPaperlessAI outputPublicObjectsPaperless dtnow=$(date '+%Y-%m-%d %H:%M:%S.%3N') cat < $HSHQ_STACKS_DIR/paperless/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$PAPERLESS_DATABASE_USER_PASSWORD echo "insert into authtoken_token(key,created,user_id) values('$PAPERLESS_API_TOKEN','$dtnow',$PAPERLESS_ADMIN_ID);" | psql -U $PAPERLESS_DATABASE_USER $PAPERLESS_DATABASE_NAME echo "update auth_user set first_name='HSHQ Admin', last_name='Paperless' where id=$PAPERLESS_ADMIN_ID;" | psql -U $PAPERLESS_DATABASE_USER $PAPERLESS_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/paperless/dbexport/setupDBSettings.sh } function outputComposePaperless() { rm -f $HOME/paperless-compose.yml cat < $HOME/paperless-compose.yml $STACK_VERSION_PREFIX paperless $(getScriptStackVersion paperless) services: paperless-db: image: $(getScriptImageByContainerName paperless-db) container_name: paperless-db hostname: paperless-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.paperless-hourly-db.schedule=@every 1h" - "ofelia.job-exec.paperless-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-hourly-db.email-from=Paperless Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.paperless-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.paperless-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-monthly-db.email-from=Paperless Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-monthly-db.mail-only-on-error=false" paperless-gotenberg: image: $(getScriptImageByContainerName paperless-gotenberg) container_name: paperless-gotenberg hostname: paperless-gotenberg restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: - "gotenberg" - "--chromium-disable-javascript=true" - "--chromium-allow-list=file:///tmp/.*" networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-tika: image: $(getScriptImageByContainerName paperless-tika) container_name: paperless-tika hostname: paperless-tika restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-app: image: $(getScriptImageByContainerName paperless-app) container_name: paperless-app hostname: paperless-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - paperless-db - paperless-redis - paperless-gotenberg - paperless-tika networks: - dock-proxy-net - dock-internalmail-net - dock-privateip-net - dock-aipriv-net - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-paperless-data:/usr/src/paperless/data - v-paperless-media:/usr/src/paperless/media - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/export:/usr/src/paperless/export - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/consume:/usr/src/paperless/consume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedConsume:/usr/src/paperless/consume/SharedConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalConsume:/usr/src/paperless/consume/PersonalConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalTranscribeOutput:/usr/src/paperless/consume/PersonalTranscribeOutput - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedProcessed:/usr/src/paperless/media/documents/originals/SharedProcessed - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalProcessed:/usr/src/paperless/media/documents/originals/PersonalProcessed paperless-ai: image: $(getScriptImageByContainerName paperless-ai) container_name: paperless-ai hostname: paperless-ai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/huggingface:/root/.cache/huggingface - v-paperless-ai:/app/data paperless-gpt: image: $(getScriptImageByContainerName paperless-gpt) container_name: paperless-gpt hostname: paperless-gpt restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/prompts:/app/prompts - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/hocr:/app/hocr - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/pdf:/app/pdf paperless-mcp: image: $(getScriptImageByContainerName paperless-mcp) container_name: paperless-mcp hostname: paperless-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-paperless-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro paperless-redis: image: $(getScriptImageByContainerName paperless-redis) container_name: paperless-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $PAPERLESS_REDIS_PASSWORD --appendonly no networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-paperless-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/data v-paperless-media: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/media v-paperless-ai: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/ai networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true dock-aipriv-net: name: dock-aipriv external: true int-paperless-net: driver: bridge internal: true ipam: driver: default EOFJT } function outputEnvPaperlessAI() { cat < $HSHQ_STACKS_DIR/paperless/ai.env PAPERLESS_API_URL=http://paperless-app:8000/api PAPERLESS_API_TOKEN=$PAPERLESS_API_TOKEN PAPERLESS_USERNAME=$PAPERLESS_ADMIN_USERNAME AI_PROVIDER=custom SCAN_INTERVAL=*/5 * * * * SYSTEM_PROMPT=\`You are a personalized document analyzer. Your task is to analyze documents and extract relevant information.\n\nAnalyze the document content and extract the following information into a structured JSON object:\n\n1. title: Create a concise, meaningful title for the document\n2. correspondent: Identify the sender/institution but do not include addresses\n3. tags: Select up to 4 relevant thematic tags\n4. document_date: Extract the document date (format: YYYY-MM-DD)\n5. document_type: Select the document type from the following list: {{ALL_DOCUMENT_TYPES}}. **CRITICAL** DO NOT fabricate a document type, ONLY select from the available document types.\n6. language: Determine the document language (e.g. "de" or "en")\n \nImportant rules for the analysis:\n\nFor tags:\n- Use only relevant categories\n- Maximum 4 tags per document, less if sufficient (at least 1)\n- Avoid generic or too specific tags\n- Use only the most important information for tag creation\n\nFor the title:\n- Short and concise, NO ADDRESSES\n- Contains the most important identification features\n- For invoices/orders, mention invoice/order number if available\n\nFor the correspondent:\n- Identify the sender or institution\n- When generating the correspondent, always create the shortest possible form of the company name (e.g. "Amazon" instead of "Amazon EU SARL, German branch")\n\nFor the document date:\n- Extract the date of the document\n- Use the format YYYY-MM-DD\n- If multiple dates are present, use the most relevant one\n\nFor the language:\n- Determine the document language\n- Use language codes like "de" for German or "en" for English\n- If the language is not clear, use "und" as a placeholder \` PROCESS_PREDEFINED_DOCUMENTS=no TOKEN_LIMIT=32768 RESPONSE_TOKENS=8192 TAGS= ADD_AI_PROCESSED_TAG=yes AI_PROCESSED_TAG_NAME=AI-Processed USE_PROMPT_TAGS=no PROMPT_TAGS= USE_EXISTING_DATA=yes PRE_EXISTING_DATA_PROMPT=\`Available document types: {{ALL_DOCUMENT_TYPES}}\` API_KEY=$PAPERLESS_AI_API_KEY JWT_SECRET=$PAPERLESS_AI_JWT_SECRET CUSTOM_API_KEY=$LITELLM_AUTH_KEY CUSTOM_BASE_URL=http://litellm-proxy:4000/v1 CUSTOM_MODEL=Vision PAPERLESS_AI_INITIAL_SETUP=yes ACTIVATE_TAGGING=yes ACTIVATE_CORRESPONDENTS=yes ACTIVATE_DOCUMENT_TYPE=yes ACTIVATE_TITLE=no ACTIVATE_CUSTOM_FIELDS=no CUSTOM_FIELDS={"custom_fields":[]} DISABLE_AUTOMATIC_PROCESSING=no RESTRICT_TO_EXISTING_DOCUMENT_TYPES=yes AZURE_ENDPOINT= AZURE_API_KEY= AZURE_DEPLOYMENT_NAME= AZURE_API_VERSION= MISTRAL_OCR_ENABLED=yes OCR_PROVIDER=custom OCR_API_URL=http://litellm-proxy:4000/v1 OCR_API_KEY=$LITELLM_AUTH_KEY MISTRAL_OCR_MODEL=Vision EOFPA } function performIntegrationPaperlessAI() { echo "Integrating paperless-ai, please wait..." sleep 5 waitForContainerLogString paperless-app 3 60 "celery@paperless-app ready" if [ -z "$PAPERLESS_API_TOKEN" ]; then PAPERLESS_API_TOKEN=$(docker exec paperless-app curl -s -X POST http://localhost:8000/api/token/ -d "username=${PAPERLESS_ADMIN_USERNAME}&password=${PAPERLESS_ADMIN_PASSWORD}" | jq -r .token) fi sed -i "s/PAPERLESS_API_TOKEN=.*/PAPERLESS_API_TOKEN=$PAPERLESS_API_TOKEN/" $HSHQ_STACKS_DIR/paperless/ai.env dtNow=$(date '+%Y-%m-%d %H:%M:%S') pwHash=$(htpasswd -bnBC 15 "" $PAPERLESS_AI_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2b/') sleep 1 startStopStack paperless stop sleep 1 sudo rm -fr $HSHQ_STACKS_DIR/paperless/ai mkdir $HSHQ_STACKS_DIR/paperless/ai mv $HSHQ_STACKS_DIR/paperless/ai.env $HSHQ_STACKS_DIR/paperless/ai/.env startStopStack paperless start waitForContainerLogString paperless-ai 3 60 "Server running on port 3000" sudo sqlite3 $HSHQ_STACKS_DIR/paperless/ai/documents.db "insert into users(id,username,password,created_at) values(1,'$PAPERLESS_AI_ADMIN_USERNAME','$pwHash','$dtNow');" docker container restart paperless-ai > /dev/null 2>&1 } function performWorkflowsIntegrationPaperless() { set +e numTries=0 totalTries=10 isAPISuccess=false while [ $numTries -lt $totalTries ] do apiRes=$(docker exec paperless-app curl -s -X GET "http://localhost:8000/api/groups/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN") if ! [ -z "$apiRes" ]; then isAPISuccess=true break fi echo "Paperless API not ready, sleep 5 seconds..." sleep 5 ((numTries++)) done if ! [ "$isAPISuccess" = "true" ]; then echo "ERROR: Could not connect to Paperless API, returning..." return fi jsonbody="{\"name\": \"$LDAP_PRIMARY_USER_GROUP_NAME\", \"permissions\": [\"view_logentry\",\"view_group\",\"view_user\",\"add_correspondent\",\"change_correspondent\",\"delete_correspondent\",\"view_correspondent\",\"add_document\",\"change_document\",\"delete_document\",\"view_document\",\"view_documenttype\",\"add_note\",\"change_note\",\"delete_note\",\"view_note\",\"add_savedview\",\"change_savedview\",\"delete_savedview\",\"view_savedview\",\"add_sharelink\",\"change_sharelink\",\"delete_sharelink\",\"view_sharelink\",\"add_tag\",\"change_tag\",\"delete_tag\",\"view_tag\",\"add_uisettings\",\"change_uisettings\",\"delete_uisettings\",\"view_uisettings\",\"view_workflow\",\"add_mailaccount\",\"change_mailaccount\",\"delete_mailaccount\",\"view_mailaccount\",\"add_mailrule\",\"change_mailrule\",\"delete_mailrule\",\"view_mailrule\",\"add_processedmail\",\"change_processedmail\",\"delete_processedmail\",\"view_processedmail\"]}" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/groups/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 pap_doc_types=( "Invoice" "Receipt" "Contract" "Bank Statement" "Taxes" "Policy" "Identification" "Medical Record" "Pay Stub" "Certificate" "Quote" "Form" "Report" "Other" ) for cur_doc_type in "${pap_doc_types[@]}"; do RESPONSE=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/document_types/" \ -H "Authorization: Token $PAPERLESS_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$cur_doc_type\", \"matching_algorithm\": 6, \"match\": \"\"}") done manual_doc_type=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/document_types/" \ -H "Authorization: Token $PAPERLESS_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"Manual\", \"matching_algorithm\": 6, \"match\": \"\"}" | jq -r '.id') research_doc_type=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/document_types/" \ -H "Authorization: Token $PAPERLESS_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"Research Paper\", \"matching_algorithm\": 6, \"match\": \"\"}" | jq -r '.id') PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME="Transcription" PAPERLESS_TRANSCRIPTION_DOCTYPE_ID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/document_types/" \ -H "Authorization: Token $PAPERLESS_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME\", \"matching_algorithm\": 6, \"match\": \"\"}" | jq -r '.id') updateConfigVar PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME "$PAPERLESS_TRANSCRIPTION_DOCTYPE_NAME" updateConfigVar PAPERLESS_TRANSCRIPTION_DOCTYPE_ID "$PAPERLESS_TRANSCRIPTION_DOCTYPE_ID" PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME="Personal Email" jsonbody="{ \"name\": \"$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME\", \"color\": \"#299aa5\" }" PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/tags/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') updateConfigVar PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME "$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_NAME" updateConfigVar PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID "$PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID" PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME="Shared Email" jsonbody="{ \"name\": \"$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME\", \"color\": \"#ff7f7f\" }" PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/tags/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') updateConfigVar PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME "$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_NAME" updateConfigVar PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID "$PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID" PAPERLESS_KNOWLEDGEBASE_TAG_NAME="Knowledge Base" jsonbody="{ \"name\": \"$PAPERLESS_KNOWLEDGEBASE_TAG_NAME\", \"color\": \"#615dff\", \"matching_algorithm\": 6, \"is_insensitive\": true }" PAPERLESS_KNOWLEDGEBASE_TAG_ID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/tags/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') updateConfigVar PAPERLESS_KNOWLEDGEBASE_TAG_NAME "$PAPERLESS_KNOWLEDGEBASE_TAG_NAME" updateConfigVar PAPERLESS_KNOWLEDGEBASE_TAG_ID "$PAPERLESS_KNOWLEDGEBASE_TAG_ID" jsonbody="{\"name\": \"PersonalProcessed\",\"path\": \"PersonalProcessed/{{owner_username}}/PersonalProcessed/{{title}}\",\"match\": \"\",\"matching_algorithm\": 6,\"is_insensitive\": true,\"owner\": $PAPERLESS_ADMIN_ID}" personalPathID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/storage_paths/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r .id) if [ -z "$personalPathID" ] || [ $personalPathID -ne 1 ]; then echo "ERROR: The assigned ID($personalPathID) for this storage path is unexpected. It should be assigned an id of 1." fi jsonbody="{\"name\": \"SharedProcessed\",\"path\": \"SharedProcessed/{{title}}\",\"match\": \"\",\"matching_algorithm\": 6,\"is_insensitive\": true,\"owner\": $PAPERLESS_ADMIN_ID}" sharedPathID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/storage_paths/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r .id) if [ -z "$sharedPathID" ] || [ $sharedPathID -ne 2 ]; then echo "ERROR: The assigned ID($sharedPathID) for this storage path is unexpected. It should be assigned an id of 2." fi jsonbody="{\"name\": \"AdminProcessed\",\"path\": \"PersonalProcessed/$NEXTCLOUD_ADMIN_USERNAME/PersonalProcessed/{{title}}\",\"match\": \"\",\"matching_algorithm\": 6,\"is_insensitive\": true,\"owner\": $PAPERLESS_ADMIN_ID}" adminPathID=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/storage_paths/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r .id) if [ -z "$adminPathID" ] || [ $adminPathID -ne 3 ]; then echo "ERROR: The assigned ID($adminPathID) for this storage path is unexpected. It should be assigned an id of 3." fi jsonbody="{ \"name\": \"default_personalprocessed\", \"order\": 1, \"enabled\": true, \"triggers\": [ { \"sources\": [ 1, 2, 3, 4 ], \"type\": 1, \"filter_path\": null, \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true } ], \"actions\": [ { \"type\": 1, \"assign_title\": null, \"assign_tags\": [], \"assign_correspondent\": null, \"assign_document_type\": null, \"assign_storage_path\": 1, \"assign_owner\": null, \"assign_view_users\": [], \"assign_view_groups\": [], \"assign_change_users\": [], \"assign_change_groups\": [], \"assign_custom_fields\": [], \"assign_custom_fields_values\": {}, \"remove_all_tags\": false, \"remove_tags\": [], \"remove_all_correspondents\": false, \"remove_correspondents\": [], \"remove_all_document_types\": false, \"remove_document_types\": [], \"remove_all_storage_paths\": false, \"remove_storage_paths\": [], \"remove_custom_fields\": [], \"remove_all_custom_fields\": false, \"remove_all_owners\": false, \"remove_owners\": [], \"remove_all_permissions\": false, \"remove_view_users\": [], \"remove_view_groups\": [], \"remove_change_users\": [], \"remove_change_groups\": [], \"email\": null, \"webhook\": null } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"sharedconsume\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"sources\": [ 1, 2, 3, 4 ], \"type\": 1, \"filter_path\": \"*/SharedConsume/*\", \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true } ], \"actions\": [ { \"type\": 1, \"assign_title\": null, \"assign_tags\": [], \"assign_correspondent\": null, \"assign_document_type\": null, \"assign_storage_path\": 2, \"assign_owner\": $PAPERLESS_ADMIN_ID, \"assign_view_users\": [], \"assign_view_groups\": [ 1 ], \"assign_change_users\": [], \"assign_change_groups\": [ 1 ], \"assign_custom_fields\": [], \"assign_custom_fields_values\": {}, \"remove_all_tags\": false, \"remove_tags\": [], \"remove_all_correspondents\": false, \"remove_correspondents\": [], \"remove_all_document_types\": false, \"remove_document_types\": [], \"remove_all_storage_paths\": false, \"remove_storage_paths\": [], \"remove_custom_fields\": [], \"remove_all_custom_fields\": false, \"remove_all_owners\": false, \"remove_owners\": [], \"remove_all_permissions\": false, \"remove_view_users\": [], \"remove_view_groups\": [], \"remove_change_users\": [], \"remove_change_groups\": [], \"email\": null, \"webhook\": null } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"sharedemail\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"sources\": [], \"type\": 2, \"filter_path\": null, \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true, \"filter_has_tags\": [ $PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID ] }, { \"sources\": [], \"type\": 3, \"filter_path\": null, \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true, \"filter_has_tags\": [ $PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID ] } ], \"actions\": [ { \"type\": 1, \"assign_title\": null, \"assign_tags\": [], \"assign_correspondent\": null, \"assign_document_type\": null, \"assign_storage_path\": 2, \"assign_owner\": null, \"assign_view_users\": [], \"assign_view_groups\": [ 1 ], \"assign_change_users\": [], \"assign_change_groups\": [ 1 ], \"assign_custom_fields\": [], \"assign_custom_fields_values\": {}, \"remove_all_tags\": false, \"remove_tags\": [], \"remove_all_correspondents\": false, \"remove_correspondents\": [], \"remove_all_document_types\": false, \"remove_document_types\": [], \"remove_all_storage_paths\": false, \"remove_storage_paths\": [], \"remove_custom_fields\": [], \"remove_all_custom_fields\": false, \"remove_all_owners\": false, \"remove_owners\": [], \"remove_all_permissions\": false, \"remove_view_users\": [], \"remove_view_groups\": [], \"remove_change_users\": [], \"remove_change_groups\": [], \"email\": null, \"webhook\": null } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"admin_personalconsume\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"sources\": [ 1, 2, 3, 4 ], \"type\": 1, \"filter_path\": \"*/PersonalConsume/$NEXTCLOUD_ADMIN_USERNAME/PersonalConsume/*\", \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true }, { \"sources\": [], \"type\": 2, \"filter_path\": null, \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true, \"filter_has_tags\": [ $PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID ] }, { \"sources\": [], \"type\": 3, \"filter_path\": null, \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true, \"filter_has_tags\": [ $PAPERLESS_EMAIL_PROCESSED_PERSONAL_TAG_ID ] } ], \"actions\": [ { \"type\": 1, \"assign_owner\": $PAPERLESS_ADMIN_ID }, { \"type\": 1, \"assign_storage_path\": 3 } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"admin_transcribeconsume\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"sources\": [ 1, 2, 3, 4 ], \"type\": 1, \"filter_path\": \"*/PersonalTranscribeOutput/$SPEAKR_ADMIN_USERNAME/*\", \"filter_filename\": null, \"filter_mailrule\": null, \"matching_algorithm\": 0, \"match\": \"\", \"is_insensitive\": true } ], \"actions\": [ { \"type\": 1, \"assign_owner\": $PAPERLESS_ADMIN_ID, \"assign_document_type\": $PAPERLESS_TRANSCRIPTION_DOCTYPE_ID }, { \"type\": 1, \"assign_storage_path\": 3 } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"assign_kb\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"type\": 2, \"filter_has_any_document_types\": [$manual_doc_type, $research_doc_type, $PAPERLESS_TRANSCRIPTION_DOCTYPE_ID] }, { \"type\": 3, \"filter_has_any_document_types\": [$manual_doc_type, $research_doc_type, $PAPERLESS_TRANSCRIPTION_DOCTYPE_ID] } ], \"actions\": [ { \"type\": 1, \"assign_tags\": [ $PAPERLESS_KNOWLEDGEBASE_TAG_ID ] } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"trigger_ai\", \"order\": 2, \"enabled\": true, \"triggers\": [ { \"type\": 2, \"filter_filename\": null, \"matching_algorithm\": 0 } ], \"actions\": [ { \"type\": 4,\"order\": 0, \"webhook\": { \"url\": \"http://paperless-ai:3000/api/webhook/document\", \"as_json\": true, \"params\": { \"url\": \"{{doc_url}}\" }, \"headers\": { \"x-api-key\": \"$PAPERLESS_AI_API_KEY\" }, \"include_document\": false } } ] }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/workflows/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"${EMAIL_JOINT_USERNAME} Email\", \"imap_server\": \"$SMTP_HOSTNAME\", \"imap_port\": 143, \"imap_security\": 3, \"username\": \"$EMAIL_JOINT_EMAIL_ADDRESS\", \"password\": \"$EMAIL_JOINT_PASSWORD\", \"account_type\": 1, \"owner\": $PAPERLESS_ADMIN_ID, \"user_can_change\": true }" mail_account_id=$(docker exec paperless-app curl -s -X POST "http://localhost:8000/api/mail_accounts/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" | jq -r '.id') if [ -n "$mail_account_id" ]; then jsonbody="{ \"name\": \"${EMAIL_JOINT_USERNAME} Email Personal\", \"account\": $mail_account_id, \"enabled\": true, \"folder\": \"Processed.Personal\", \"maximum_age\": 0, \"action\": 5, \"action_parameter\": \"paperless\", \"assign_title_from\": 2, \"assign_correspondent_from\": 1, \"assign_tags\": [ $PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID ], \"assign_owner_from_rule\": true, \"order\": 2, \"attachment_type\": 1, \"consumption_scope\": 1, \"pdf_layout\": 0, \"owner\": $PAPERLESS_ADMIN_ID, \"user_can_change\": true, \"stop_processing\": false }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/mail_rules/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 jsonbody="{ \"name\": \"${EMAIL_JOINT_USERNAME} Email Work\", \"account\": $mail_account_id, \"enabled\": true, \"folder\": \"Processed.Work\", \"maximum_age\": 0, \"action\": 5, \"action_parameter\": \"paperless\", \"assign_title_from\": 2, \"assign_correspondent_from\": 1, \"assign_tags\": [ $PAPERLESS_EMAIL_PROCESSED_SHARED_TAG_ID ], \"assign_owner_from_rule\": true, \"order\": 2, \"attachment_type\": 1, \"consumption_scope\": 1, \"pdf_layout\": 0, \"owner\": $PAPERLESS_ADMIN_ID, \"user_can_change\": true, \"stop_processing\": false }" docker exec paperless-app curl -s -X POST "http://localhost:8000/api/mail_rules/" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" -d "$jsonbody" > /dev/null 2>&1 fi } function getPaperlessIDFromUsername() { pap_username="$1" pap_id=$(docker exec paperless-app curl -s -X GET "http://localhost:8000/api/users/?username__iexact=$pap_username" -H "Content-Type: application/json" -H "Authorization: Token $PAPERLESS_API_TOKEN" | jq -r '.results[0].id') echo $pap_id } function performIntegrationPaperlessGPT() { echo "Integrating paperless-gpt, please wait..." waitForContainerLogString paperless-app 3 60 "celery@paperless-app ready" PAPERLESS_API_TOKEN=$(docker exec paperless-app curl -s -X POST http://localhost:8000/api/token/ -d "username=${PAPERLESS_ADMIN_USERNAME}&password=${PAPERLESS_ADMIN_PASSWORD}" | jq -r .token) updateConfigVar PAPERLESS_API_TOKEN $PAPERLESS_API_TOKEN updateStackEnv paperless mfUpdateAPITokenPaperless } function performUpdatePaperless() { perform_stack_name=paperless prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=postgres:15.0-bullseye,gotenberg/gotenberg:7.10,ghcr.io/paperless-ngx/tika:2.9.1-minimal,ghcr.io/paperless-ngx/paperless-ngx:2.4.3,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gotenberg/gotenberg:7.10,gotenberg/gotenberg:8.12.0" image_update_map[2]="ghcr.io/paperless-ngx/tika:2.9.1-minimal,ghcr.io/paperless-ngx/tika:2.9.1-minimal" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.4.3,ghcr.io/paperless-ngx/paperless-ngx:2.13.2" image_update_map[4]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 2) newVer=v3 curImageList=postgres:15.0-bullseye,gotenberg/gotenberg:8.2.2,ghcr.io/paperless-ngx/tika:2.9.1-minimal,ghcr.io/paperless-ngx/paperless-ngx:2.6.2,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gotenberg/gotenberg:8.2.2,gotenberg/gotenberg:8.12.0" image_update_map[2]="ghcr.io/paperless-ngx/tika:2.9.1-minimal,apache/tika:3.0.0.0" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.6.2,ghcr.io/paperless-ngx/paperless-ngx:2.13.2" image_update_map[4]="bitnami/redis:7.0.5,bitnami/redis:7.0.5" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,gotenberg/gotenberg:8.12.0,apache/tika:3.0.0.0,ghcr.io/paperless-ngx/paperless-ngx:2.13.2,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="gotenberg/gotenberg:8.12.0,gotenberg/gotenberg:8.19.1" image_update_map[2]="apache/tika:3.0.0.0,apache/tika:3.1.0.0" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.13.2,ghcr.io/paperless-ngx/paperless-ngx:2.14.7" image_update_map[4]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessUpdatePythonCertPath perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,gotenberg/gotenberg:8.19.1,apache/tika:3.1.0.0,ghcr.io/paperless-ngx/paperless-ngx:2.14.7,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="gotenberg/gotenberg:8.19.1,mirror.gcr.io/gotenberg/gotenberg:8.21" image_update_map[2]="apache/tika:3.1.0.0,mirror.gcr.io/apache/tika:3.2.2.0-full" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.14.7,ghcr.io/paperless-ngx/paperless-ngx:2.17.1" image_update_map[4]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v6 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.21,mirror.gcr.io/apache/tika:3.2.2.0-full,ghcr.io/paperless-ngx/paperless-ngx:2.17.1,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.21,mirror.gcr.io/gotenberg/gotenberg:8.25.1" image_update_map[2]="mirror.gcr.io/apache/tika:3.2.2.0-full,mirror.gcr.io/apache/tika:3.2.3.0-full" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.17.1,ghcr.io/paperless-ngx/paperless-ngx:2.20.1" image_update_map[4]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 6) newVer=v7 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.25.1,mirror.gcr.io/apache/tika:3.2.3.0-full,ghcr.io/paperless-ngx/paperless-ngx:2.20.1,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.25.1,mirror.gcr.io/gotenberg/gotenberg:8.25.1" image_update_map[2]="mirror.gcr.io/apache/tika:3.2.3.0-full,mirror.gcr.io/apache/tika:3.2.3.0-full" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.20.1,ghcr.io/paperless-ngx/paperless-ngx:2.20.1" image_update_map[4]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessV7AddAI if [ $? -ne 0 ]; then is_upgrade_error=true perform_update_report="ERROR: There was a problem with the paperless-ai container." return fi perform_update_report="${perform_update_report}$stack_upgrade_report" performIntegrationPaperlessAI performWorkflowsIntegrationPaperless return ;; 7) newVer=v8 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.25.1,mirror.gcr.io/apache/tika:3.2.3.0-full,ghcr.io/paperless-ngx/paperless-ngx:2.20.1,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/clusterzx/paperless-ai:3.0.9 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.25.1,mirror.gcr.io/gotenberg/gotenberg:8.25.1" image_update_map[2]="mirror.gcr.io/apache/tika:3.2.3.0-full,mirror.gcr.io/apache/tika:3.2.3.0-full" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.20.1,ghcr.io/paperless-ngx/paperless-ngx:2.20.1" image_update_map[4]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[5]="mirror.gcr.io/clusterzx/paperless-ai:3.0.9,mirror.gcr.io/clusterzx/paperless-ai:3.0.9" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessV8AddGPT if [ $? -ne 0 ]; then is_upgrade_error=true perform_update_report="ERROR: There was a problem with the paperless-gpt container." return fi perform_update_report="${perform_update_report}$stack_upgrade_report" performIntegrationPaperlessGPT return ;; 8) newVer=v9 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.25.1,mirror.gcr.io/apache/tika:3.2.3.0-full,ghcr.io/paperless-ngx/paperless-ngx:2.20.1,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/clusterzx/paperless-ai:3.0.9,mirror.gcr.io/icereed/paperless-gpt:v0.25.1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.25.1,mirror.gcr.io/gotenberg/gotenberg:8.30.1" image_update_map[2]="mirror.gcr.io/apache/tika:3.2.3.0-full,mirror.gcr.io/apache/tika:3.3.0.0-full" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.20.1,ghcr.io/paperless-ngx/paperless-ngx:2.20.13" image_update_map[4]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[5]="mirror.gcr.io/clusterzx/paperless-ai:3.0.9,mirror.gcr.io/clusterzx/paperless-ai:3.0.9" image_update_map[6]="mirror.gcr.io/icereed/paperless-gpt:v0.25.1,mirror.gcr.io/icereed/paperless-gpt:v0.25.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessV9AddMCP perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 9) newVer=v10 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.30.1,mirror.gcr.io/apache/tika:3.3.0.0-full,ghcr.io/paperless-ngx/paperless-ngx:2.20.13,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/clusterzx/paperless-ai:3.0.9,mirror.gcr.io/icereed/paperless-gpt:v0.25.1,hshq/paperless-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.30.1,mirror.gcr.io/gotenberg/gotenberg:8.34.0" image_update_map[2]="mirror.gcr.io/apache/tika:3.3.0.0-full,mirror.gcr.io/apache/tika:3.3.1.0-full" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:2.20.13,ghcr.io/paperless-ngx/paperless-ngx:3.0.5" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[5]="mirror.gcr.io/clusterzx/paperless-ai:3.0.9,hshq/paperless-ai-next:v1" image_update_map[6]="mirror.gcr.io/icereed/paperless-gpt:v0.25.1,ghcr.io/icereed/paperless-gpt:v0.26.1" image_update_map[7]="hshq/paperless-mcp:v1,ghcr.io/homeserverhq/paperless-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessV10Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 10) newVer=v11 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.34.0,mirror.gcr.io/apache/tika:3.3.1.0-full,ghcr.io/paperless-ngx/paperless-ngx:3.0.5,mirror.gcr.io/valkey/valkey:alpine3.23,hshq/paperless-ai-next:v1,ghcr.io/icereed/paperless-gpt:v0.26.1,ghcr.io/homeserverhq/paperless-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.34.0,mirror.gcr.io/gotenberg/gotenberg:8.34.0" image_update_map[2]="mirror.gcr.io/apache/tika:3.3.1.0-full,mirror.gcr.io/apache/tika:3.3.1.0-full" image_update_map[3]="ghcr.io/paperless-ngx/paperless-ngx:3.0.5,ghcr.io/homeserverhq/paperless-ngx:v3.0.5" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[5]="hshq/paperless-ai-next:v1,hshq/paperless-ai-next:v1" image_update_map[6]="ghcr.io/icereed/paperless-gpt:v0.26.1,ghcr.io/icereed/paperless-gpt:v0.27.0" image_update_map[7]="ghcr.io/homeserverhq/paperless-mcp:v2,ghcr.io/homeserverhq/paperless-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessV11Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 11) newVer=v12 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.34.0,mirror.gcr.io/apache/tika:3.3.1.0-full,ghcr.io/homeserverhq/paperless-ngx:v3.0.5,mirror.gcr.io/valkey/valkey:alpine3.23,hshq/paperless-ai-next:v1,ghcr.io/icereed/paperless-gpt:v0.27.0,ghcr.io/homeserverhq/paperless-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.34.0,mirror.gcr.io/gotenberg/gotenberg:8.34.0" image_update_map[2]="mirror.gcr.io/apache/tika:3.3.1.0-full,mirror.gcr.io/apache/tika:3.3.1.0-full" image_update_map[3]="ghcr.io/homeserverhq/paperless-ngx:v3.0.5,ghcr.io/homeserverhq/paperless-ngx:v3.0.5" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[5]="hshq/paperless-ai-next:v1,ghcr.io/homeserverhq/zettelrobbe:v2026.08.04" image_update_map[6]="ghcr.io/icereed/paperless-gpt:v0.27.0,ghcr.io/icereed/paperless-gpt:v0.27.0" image_update_map[7]="ghcr.io/homeserverhq/paperless-mcp:v2,ghcr.io/homeserverhq/paperless-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPaperlessV12Update perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 12) newVer=v12 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/gotenberg/gotenberg:8.34.0,mirror.gcr.io/apache/tika:3.3.1.0-full,ghcr.io/homeserverhq/paperless-ngx:v3.0.5,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/zettelrobbe:v2026.08.04,ghcr.io/icereed/paperless-gpt:v0.27.0,ghcr.io/homeserverhq/paperless-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/gotenberg/gotenberg:8.34.0,mirror.gcr.io/gotenberg/gotenberg:8.34.0" image_update_map[2]="mirror.gcr.io/apache/tika:3.3.1.0-full,mirror.gcr.io/apache/tika:3.3.1.0-full" image_update_map[3]="ghcr.io/homeserverhq/paperless-ngx:v3.0.5,ghcr.io/homeserverhq/paperless-ngx:v3.0.5" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[5]="ghcr.io/homeserverhq/zettelrobbe:v2026.08.04,ghcr.io/homeserverhq/zettelrobbe:v2026.08.04" image_update_map[6]="ghcr.io/icereed/paperless-gpt:v0.27.0,ghcr.io/icereed/paperless-gpt:v0.27.0" image_update_map[7]="ghcr.io/homeserverhq/paperless-mcp:v2,ghcr.io/homeserverhq/paperless-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function buildImagePaperlessMCPV1() { set +e echo -e "\n========================================================================" echo -e " The Paperless MCP image is being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/PaperlessMCP-Multitenant cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 https://github.com/homeserverhq/PaperlessMCP-Multitenant.git cd $HSHQ_BUILD_DIR/PaperlessMCP-Multitenant/PaperlessMCP docker image build -t hshq/paperless-mcp:v1 -f ./Dockerfile . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/PaperlessMCP-Multitenant return $rtval } function buildImagePaperlessAINextV1() { set +e echo -e "\n========================================================================" echo -e " The Zettelrobbe image is being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/paperless-ai-next cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 --branch v2026.07.02 https://github.com/admonstrator/paperless-ai-next.git cd $HSHQ_BUILD_DIR/paperless-ai-next docker image build -t hshq/paperless-ai-next:v1 -f ./Dockerfile . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/paperless-ai-next return $rtval } function mvFixRedisDir() { outputConfigPaperless docker volume rm paperless_v-paperless-redis } function mfPaperlessUpdatePythonCertPath() { # This will address the python version cert path correctly # by moving the version to an environment variable. # Much easier to update in the future. sed -i "s|\/etc\/ssl\/certs\/ca-certificates.crt:\/usr\/local\/lib\/python.*|\/etc\/ssl\/certs\/ca-certificates.crt:\/usr\/local\/lib\/\${PYTHON_VER}\/site-packages\/certifi\/cacert.pem:ro|g" $HOME/paperless-compose.yml grep "PYTHON_VER=" $HOME/paperless.env > /dev/null 2>&1 if [ $? -ne 0 ]; then echo "PYTHON_VER=python3.12" >> $HOME/paperless.env fi } function mfPaperlessFixRedisCompose() { replaceRedisBlock paperless paperless-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/paperless/redis/* } function mfPaperlessV7AddAI() { initServicesCredentials set +e pullImage mirror.gcr.io/clusterzx/paperless-ai:3.0.9 if [ $? -ne 0 ]; then return 1 fi mkdir $HSHQ_STACKS_DIR/paperless/ai outputComposeV7Paperless outputEnvPaperlessAI set +e grep -q PAPERLESS_AI_PORT $HOME/paperless.env if [ $? -ne 0 ]; then echo "PUID=\$PORTAINER_UID" >> $HOME/paperless.env echo "PGID=\$PORTAINER_GID" >> $HOME/paperless.env echo "PAPERLESS_CONSUMER_RECURSIVE=true" >> $HOME/paperless.env echo "PAPERLESS_AI_PORT=3000" >> $HOME/paperless.env echo "RAG_SERVICE_URL=http://localhost:8000" >> $HOME/paperless.env echo "RAG_SERVICE_ENABLED=true" >> $HOME/paperless.env echo "ANONYMIZED_TELEMETRY=False" >> $HOME/paperless.env echo "PAPERLESS_API_URL=http://paperless-app:8000" >> $HOME/paperless.env PAPERLESS_API_TOKEN=$(docker exec paperless-app curl -s -X POST http://localhost:8000/api/token/ -d "username=${PAPERLESS_ADMIN_USERNAME}&password=${PAPERLESS_ADMIN_PASSWORD}" | jq -r .token) updateConfigVar PAPERLESS_API_TOKEN $PAPERLESS_API_TOKEN echo "PAPERLESS_API_TOKEN=$PAPERLESS_API_TOKEN" >> $HOME/paperless.env fi inner_block="" inner_block=$inner_block">>https://$SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_BASEHEADER\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com cdn.tailwindcss.com unpkg.com cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com unpkg.com; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com cdn.tailwindcss.com unpkg.com; img-src 'self' *.${HOMESERVER_DOMAIN} data:; font-src 'self' cdnjs.cloudflare.com data:; frame-src 'self' *.${HOMESERVER_DOMAIN} data: blob:; connect-src 'self' *.${HOMESERVER_DOMAIN} data: api.github.com; object-src 'none'; upgrade-insecure-requests; frame-ancestors 'self' *.${HOMESERVER_DOMAIN};\"" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://paperless-ai:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PAPERLESS_AI_WEB $MANAGETLS_PAPERLESS_AI_WEB "$is_integrate_hshq" $NETDEFAULT_PAPERLESS_AI_WEB "$inner_block" insertSubAuthelia $SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} insertEnableSvcAll paperless "$FMLNAME_PAPERLESS_AI_WEB" $USERTYPE_PAPERLESS_AI_WEB "https://$SUB_PAPERLESS_AI_WEB.$HOMESERVER_DOMAIN" "paperless-ai.png" "$(getHeimdallOrderFromSub $SUB_PAPERLESS_AI_WEB $USERTYPE_PAPERLESS_AI_WEB)" restartAllCaddyContainers sendEmail -s "Zettelrobbe Admin Login Info" -b "Zettelrobbe Admin Username: $PAPERLESS_AI_ADMIN_USERNAME\nZettelrobbe Admin Password: $PAPERLESS_AI_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" } function outputComposeV7Paperless() { rm -f $HOME/paperless-compose.yml cat < $HOME/paperless-compose.yml $STACK_VERSION_PREFIX paperless v7 services: paperless-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: paperless-db hostname: paperless-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.paperless-hourly-db.schedule=@every 1h" - "ofelia.job-exec.paperless-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-hourly-db.email-from=Paperless Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.paperless-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.paperless-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-monthly-db.email-from=Paperless Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-monthly-db.mail-only-on-error=false" paperless-gotenberg: image: mirror.gcr.io/gotenberg/gotenberg:8.25.1 container_name: paperless-gotenberg hostname: paperless-gotenberg restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: - "gotenberg" - "--chromium-disable-javascript=true" - "--chromium-allow-list=file:///tmp/.*" networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-tika: image: mirror.gcr.io/apache/tika:3.2.3.0-full container_name: paperless-tika hostname: paperless-tika restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-app: image: ghcr.io/paperless-ngx/paperless-ngx:2.20.1 container_name: paperless-app hostname: paperless-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - paperless-db - paperless-redis - paperless-gotenberg - paperless-tika networks: - dock-proxy-net - dock-internalmail-net - dock-privateip-net - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-paperless-data:/usr/src/paperless/data - v-paperless-media:/usr/src/paperless/media - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/export:/usr/src/paperless/export - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/consume:/usr/src/paperless/consume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedConsume:/usr/src/paperless/consume/SharedConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalConsume:/usr/src/paperless/consume/PersonalConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedProcessed:/usr/src/paperless/media/documents/originals/SharedProcessed - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalProcessed:/usr/src/paperless/media/documents/originals/PersonalProcessed paperless-ai: image: mirror.gcr.io/clusterzx/paperless-ai:3.0.9 container_name: paperless-ai hostname: paperless-ai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/huggingface:/root/.cache/huggingface - v-paperless-ai:/app/data paperless-redis: image: mirror.gcr.io/redis:8.4.0-bookworm container_name: paperless-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $PAPERLESS_REDIS_PASSWORD --appendonly no networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-paperless-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/data v-paperless-media: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/media v-paperless-ai: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/ai networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true dock-aipriv-net: name: dock-aipriv external: true int-paperless-net: driver: bridge internal: true ipam: driver: default EOFJT } function outputComposeV8Paperless() { rm -f $HOME/paperless-compose.yml cat < $HOME/paperless-compose.yml $STACK_VERSION_PREFIX paperless v8 services: paperless-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: paperless-db hostname: paperless-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.paperless-hourly-db.schedule=@every 1h" - "ofelia.job-exec.paperless-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-hourly-db.email-from=Paperless Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.paperless-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.paperless-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-monthly-db.email-from=Paperless Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-monthly-db.mail-only-on-error=false" paperless-gotenberg: image: mirror.gcr.io/gotenberg/gotenberg:8.25.1 container_name: paperless-gotenberg hostname: paperless-gotenberg restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: - "gotenberg" - "--chromium-disable-javascript=true" - "--chromium-allow-list=file:///tmp/.*" networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-tika: image: mirror.gcr.io/apache/tika:3.2.3.0-full container_name: paperless-tika hostname: paperless-tika restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-app: image: ghcr.io/paperless-ngx/paperless-ngx:2.20.1 container_name: paperless-app hostname: paperless-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - paperless-db - paperless-redis - paperless-gotenberg - paperless-tika networks: - dock-proxy-net - dock-internalmail-net - dock-privateip-net - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-paperless-data:/usr/src/paperless/data - v-paperless-media:/usr/src/paperless/media - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/export:/usr/src/paperless/export - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/consume:/usr/src/paperless/consume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedConsume:/usr/src/paperless/consume/SharedConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalConsume:/usr/src/paperless/consume/PersonalConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedProcessed:/usr/src/paperless/media/documents/originals/SharedProcessed - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalProcessed:/usr/src/paperless/media/documents/originals/PersonalProcessed paperless-ai: image: mirror.gcr.io/clusterzx/paperless-ai:3.0.9 container_name: paperless-ai hostname: paperless-ai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/huggingface:/root/.cache/huggingface - v-paperless-ai:/app/data paperless-gpt: image: mirror.gcr.io/icereed/paperless-gpt:v0.25.1 container_name: paperless-gpt hostname: paperless-gpt restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/prompts:/app/prompts - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/hocr:/app/hocr - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/pdf:/app/pdf paperless-redis: image: mirror.gcr.io/redis:8.4.0-bookworm container_name: paperless-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $PAPERLESS_REDIS_PASSWORD --appendonly no networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-paperless-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/data v-paperless-media: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/media v-paperless-ai: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/ai networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true dock-aipriv-net: name: dock-aipriv external: true int-paperless-net: driver: bridge internal: true ipam: driver: default EOFJT } function outputComposeV9Paperless() { rm -f $HOME/paperless-compose.yml cat < $HOME/paperless-compose.yml $STACK_VERSION_PREFIX paperless v9 services: paperless-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: paperless-db hostname: paperless-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.paperless-hourly-db.schedule=@every 1h" - "ofelia.job-exec.paperless-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-hourly-db.email-from=Paperless Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.paperless-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.paperless-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-monthly-db.email-from=Paperless Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-monthly-db.mail-only-on-error=false" paperless-gotenberg: image: mirror.gcr.io/gotenberg/gotenberg:8.30.1 container_name: paperless-gotenberg hostname: paperless-gotenberg restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: - "gotenberg" - "--chromium-disable-javascript=true" - "--chromium-allow-list=file:///tmp/.*" networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-tika: image: mirror.gcr.io/apache/tika:3.3.0.0-full container_name: paperless-tika hostname: paperless-tika restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-app: image: ghcr.io/paperless-ngx/paperless-ngx:2.20.13 container_name: paperless-app hostname: paperless-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - paperless-db - paperless-redis - paperless-gotenberg - paperless-tika networks: - dock-proxy-net - dock-internalmail-net - dock-privateip-net - dock-aipriv-net - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-paperless-data:/usr/src/paperless/data - v-paperless-media:/usr/src/paperless/media - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/export:/usr/src/paperless/export - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/consume:/usr/src/paperless/consume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedConsume:/usr/src/paperless/consume/SharedConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalConsume:/usr/src/paperless/consume/PersonalConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedProcessed:/usr/src/paperless/media/documents/originals/SharedProcessed - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalProcessed:/usr/src/paperless/media/documents/originals/PersonalProcessed paperless-ai: image: mirror.gcr.io/clusterzx/paperless-ai:3.0.9 container_name: paperless-ai hostname: paperless-ai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/huggingface:/root/.cache/huggingface - v-paperless-ai:/app/data paperless-gpt: image: mirror.gcr.io/icereed/paperless-gpt:v0.25.1 container_name: paperless-gpt hostname: paperless-gpt restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/prompts:/app/prompts - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/hocr:/app/hocr - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/pdf:/app/pdf paperless-mcp: image: hshq/paperless-mcp:v1 container_name: paperless-mcp hostname: paperless-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-paperless-net - dock-aipriv-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro paperless-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: paperless-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $PAPERLESS_REDIS_PASSWORD --appendonly no networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-paperless-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/data v-paperless-media: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/media v-paperless-ai: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/ai networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true dock-aipriv-net: name: dock-aipriv external: true int-paperless-net: driver: bridge internal: true ipam: driver: default EOFJT } function mfPaperlessV8AddGPT() { initServicesCredentials set +e pullImage mirror.gcr.io/icereed/paperless-gpt:v0.25.1 if [ $? -ne 0 ]; then return 1 fi mkdir $HSHQ_STACKS_DIR/paperless/gpt mkdir $HSHQ_STACKS_DIR/paperless/gpt/prompts mkdir $HSHQ_STACKS_DIR/paperless/gpt/hocr mkdir $HSHQ_STACKS_DIR/paperless/gpt/pdf outputComposeV8Paperless set +e grep -q CREATE_LOCAL_HOCR $HOME/paperless.env if [ $? -ne 0 ]; then echo "PAPERLESS_BASE_URL=http://paperless-app:8000" >> $HOME/paperless.env echo "PAPERLESS_PUBLIC_URL=https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN" >> $HOME/paperless.env echo "PAPERLESS_CONSUMER_RECURSIVE=true" >> $HOME/paperless.env echo "MANUAL_TAG=paperless-gpt" >> $HOME/paperless.env echo "AUTO_TAG=paperless-gpt-auto" >> $HOME/paperless.env echo "LLM_PROVIDER=openai" >> $HOME/paperless.env echo "LLM_MODEL=Vision" >> $HOME/paperless.env echo "OPENAI_API_KEY=$LITELLM_AUTH_KEY" >> $HOME/paperless.env echo "OPENAI_BASE_URL=http://litellm-proxy:4000/v1" >> $HOME/paperless.env echo "OCR_PROVIDER=docling" >> $HOME/paperless.env echo "DOCLING_URL=http://docling-app:5001" >> $HOME/paperless.env echo "DOCLING_IMAGE_EXPORT_MODE=placeholder" >> $HOME/paperless.env echo "DOCLING_OCR_PIPELINE=standard" >> $HOME/paperless.env echo "DOCLING_OCR_ENGINE=easyocr" >> $HOME/paperless.env echo "VISION_LLM_PROVIDER=openai" >> $HOME/paperless.env echo "VISION_LLM_MODEL=Vision" >> $HOME/paperless.env echo "OCR_PROCESS_MODE=image" >> $HOME/paperless.env echo "PDF_SKIP_EXISTING_OCR=false" >> $HOME/paperless.env echo "CREATE_LOCAL_HOCR=false" >> $HOME/paperless.env echo "LOCAL_HOCR_PATH=/app/hocr" >> $HOME/paperless.env echo "CREATE_LOCAL_PDF=false" >> $HOME/paperless.env echo "LOCAL_PDF_PATH=/app/pdf" >> $HOME/paperless.env echo "PDF_UPLOAD=false" >> $HOME/paperless.env echo "PDF_REPLACE=false" >> $HOME/paperless.env echo "PDF_COPY_METADATA=true" >> $HOME/paperless.env echo "PDF_OCR_TAGGING=true" >> $HOME/paperless.env echo "PDF_OCR_COMPLETE_TAG=paperless-gpt-ocr-complete" >> $HOME/paperless.env echo "AUTO_OCR_TAG=paperless-gpt-ocr-auto" >> $HOME/paperless.env echo "PAPERLESS_OCR_PAGES=0" >> $HOME/paperless.env echo "PAPERLESS_OCR_MODE=auto" >> $HOME/paperless.env echo "OCR_LIMIT_PAGES=0" >> $HOME/paperless.env echo "LOG_LEVEL=info" >> $HOME/paperless.env fi inner_block="" inner_block=$inner_block">>https://$SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>forward_auth https://authelia:9091 {\n" inner_block=$inner_block">>>>>>>>uri /api/verify?rd=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN\n" inner_block=$inner_block">>>>>>>>copy_headers Remote-User Remote-Groups Remote-Name Remote-Email\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://paperless-gpt:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PAPERLESS_GPT_WEB $MANAGETLS_PAPERLESS_GPT_WEB "$is_integrate_hshq" $NETDEFAULT_PAPERLESS_GPT_WEB "$inner_block" insertSubAuthelia $SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} insertEnableSvcAll paperless "$FMLNAME_PAPERLESS_GPT_WEB" $USERTYPE_PAPERLESS_GPT_WEB "https://$SUB_PAPERLESS_GPT_WEB.$HOMESERVER_DOMAIN" "paperless-gpt.png" "$(getHeimdallOrderFromSub $SUB_PAPERLESS_GPT_WEB $USERTYPE_PAPERLESS_GPT_WEB)" restartAllCaddyContainers } function mfUpdateAPITokenPaperless() { sed -i "s/PAPERLESS_API_TOKEN=.*/PAPERLESS_API_TOKEN=$PAPERLESS_API_TOKEN/" $HOME/paperless.env } function mfPaperlessV9AddMCP() { set +e buildImagePaperlessMCPV1 if [ $? -ne 0 ]; then return 1 fi if [ "$stackStatus" = "1" ];then # Stack is running, stop it, then clear data startStopStack paperless stop isStartStackFromStopped=true sleep 1 fi sudo rm -fr $HSHQ_NONBACKUP_DIR/paperless/redis/* grep -q "PAPERLESS_API_KEY" $HOME/paperless.env if [ $? -ne 0 ]; then echo "PAPERLESS_API_KEY=$PAPERLESS_API_TOKEN" >> $HOME/paperless.env fi outputComposeV9Paperless addMCPServerLiteLLM "paperless" "pap" "http://paperless-mcp:80/mcp" http none "" } function outputPublicObjectsPaperless() { mkdir -p $HSHQ_STACKS_DIR/paperless/public_objects touch $HSHQ_STACKS_DIR/paperless/public_objects/__init__.py cat < $HSHQ_STACKS_DIR/paperless/public_objects/apps.py from django.apps import AppConfig class PublicObjectsConfig(AppConfig): default_auto_field = "django.db.models.AutoField" name = "public_objects" def ready(self): import public_objects.signals EOFJT cat < $HSHQ_STACKS_DIR/paperless/public_objects/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from documents.models import Tag, Correspondent, DocumentType @receiver(post_save, sender=Tag) @receiver(post_save, sender=Correspondent) @receiver(post_save, sender=DocumentType) def make_public(sender, instance, **kwargs): if instance.owner is not None: sender.objects.filter(pk=instance.pk).update(owner=None) EOFJT } function mfPaperlessV10Update() { buildImagePaperlessAINextV1 rm -f $HOME/paperless.env cat < $HOME/paperless.env POSTGRES_DB=$PAPERLESS_DATABASE_NAME POSTGRES_USER=$PAPERLESS_DATABASE_USER POSTGRES_PASSWORD=$PAPERLESS_DATABASE_USER_PASSWORD PAPERLESS_REDIS=redis://:$PAPERLESS_REDIS_PASSWORD@paperless-redis:6379 PAPERLESS_DBHOST=paperless-db PAPERLESS_DBNAME=$PAPERLESS_DATABASE_NAME PAPERLESS_DBUSER=$PAPERLESS_DATABASE_USER PAPERLESS_DBPASS=$PAPERLESS_DATABASE_USER_PASSWORD PAPERLESS_SECRET_KEY=$PAPERLESS_SECRET_KEY PAPERLESS_URL=https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN PAPERLESS_ADMIN_USER=$PAPERLESS_ADMIN_USERNAME PAPERLESS_ADMIN_MAIL=$PAPERLESS_ADMIN_EMAIL_ADDRESS PAPERLESS_ADMIN_PASSWORD=$PAPERLESS_ADMIN_PASSWORD PAPERLESS_TIKA_ENABLED=true PAPERLESS_TIKA_ENDPOINT=http://paperless-tika:9998 PAPERLESS_TIKA_GOTENBERG_ENDPOINT=http://paperless-gotenberg:3000 PAPERLESS_EMAIL_HOST=$SMTP_HOSTNAME PAPERLESS_EMAIL_PORT=$SMTP_HOSTPORT PAPERLESS_EMAIL_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS PAPERLESS_EMAIL_USE_TLS=true PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS=true PAPERLESS_SOCIAL_AUTO_SIGNUP=true PAPERLESS_SOCIALACCOUNT_PROVIDERS={"openid_connect":{"SCOPE":["openid","profile","email","groups"],"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"authelia","name":"Authelia","client_id":"paperless","secret":"$PAPERLESS_OIDC_CLIENT_SECRET","settings":{"server_url":"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN","token_auth_method":"client_secret_basic"}}]}} PAPERLESS_CONSUMER_RECURSIVE=true PYTHON_VER=python3.12 ANONYMIZED_TELEMETRY=False PAPERLESS_BASE_URL=http://paperless-app:8000 PAPERLESS_API_TOKEN=$PAPERLESS_API_TOKEN PAPERLESS_PUBLIC_URL=https://$SUB_PAPERLESS_APP.$HOMESERVER_DOMAIN PAPERLESS_DEFAULT_OBJECT_ACCESS_GROUP=primaryusers MANUAL_TAG=paperless-gpt AUTO_TAG=paperless-gpt-auto LLM_PROVIDER=openai LLM_MODEL=Vision OPENAI_API_KEY=$LITELLM_AUTH_KEY OPENAI_BASE_URL=http://litellm-proxy:4000/v1 OCR_PROVIDER=docling DOCLING_URL=http://docling-app:5001 DOCLING_IMAGE_EXPORT_MODE=placeholder DOCLING_OCR_PIPELINE=standard DOCLING_OCR_ENGINE=easyocr VISION_LLM_PROVIDER=openai VISION_LLM_MODEL=Vision OCR_PROCESS_MODE=image PDF_SKIP_EXISTING_OCR=false CREATE_LOCAL_HOCR=false LOCAL_HOCR_PATH=/app/hocr CREATE_LOCAL_PDF=false LOCAL_PDF_PATH=/app/pdf PDF_UPLOAD=false PDF_REPLACE=false PDF_COPY_METADATA=true PDF_OCR_TAGGING=true PDF_OCR_COMPLETE_TAG=paperless-gpt-ocr-complete AUTO_OCR_TAG=paperless-gpt-ocr-auto PAPERLESS_OCR_PAGES=0 PAPERLESS_OCR_MODE=auto OCR_LIMIT_PAGES=0 LOG_LEVEL=info PAPERLESS_API_KEY=$PAPERLESS_API_TOKEN USERMAP_UID=82 USERMAP_GID=82 PUID=82 PGID=82 PAPERLESS_AI_PORT=3000 RAG_SERVICE_URL=http://localhost:8000 RAG_SERVICE_ENABLED=true PAPERLESS_API_URL=http://paperless-app:8000 PAPERLESS_EXTRA_TEXT_MIMETYPES={"text/markdown": ".md", "text/x-markdown": ".md"} MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFJT cat < $HSHQ_STACKS_DIR/paperless/ai.env PAPERLESS_API_URL=http://paperless-app:8000/api PAPERLESS_API_TOKEN=$PAPERLESS_API_TOKEN PAPERLESS_USERNAME=$PAPERLESS_ADMIN_USERNAME AI_PROVIDER=custom SCAN_INTERVAL=*/5 * * * * SYSTEM_PROMPT=\`You are a personalized document analyzer. Your task is to analyze documents and extract relevant information.\n\nAnalyze the document content and extract the following information into a structured JSON object:\n\n1. title: Create a concise, meaningful title for the document\n2. correspondent: Identify the sender/institution but do not include addresses\n3. tags: Select up to 4 relevant thematic tags\n4. document_date: Extract the document date (format: YYYY-MM-DD)\n5. document_type: Select the document type from the following list: {{ALL_DOCUMENT_TYPES}}. **CRITICAL** DO NOT fabricate a document type, ONLY select from the available document types.\n6. language: Determine the document language (e.g. "de" or "en")\n \nImportant rules for the analysis:\n\nFor tags:\n- Use only relevant categories\n- Maximum 4 tags per document, less if sufficient (at least 1)\n- Avoid generic or too specific tags\n- Use only the most important information for tag creation\n\nFor the title:\n- Short and concise, NO ADDRESSES\n- Contains the most important identification features\n- For invoices/orders, mention invoice/order number if available\n\nFor the correspondent:\n- Identify the sender or institution\n- When generating the correspondent, always create the shortest possible form of the company name (e.g. "Amazon" instead of "Amazon EU SARL, German branch")\n\nFor the document date:\n- Extract the date of the document\n- Use the format YYYY-MM-DD\n- If multiple dates are present, use the most relevant one\n\nFor the language:\n- Determine the document language\n- Use language codes like "de" for German or "en" for English\n- If the language is not clear, use "und" as a placeholder \` PROCESS_PREDEFINED_DOCUMENTS=no TOKEN_LIMIT=32768 RESPONSE_TOKENS=8192 TAGS= ADD_AI_PROCESSED_TAG=yes AI_PROCESSED_TAG_NAME=AI-Processed USE_PROMPT_TAGS=no PROMPT_TAGS= USE_EXISTING_DATA=yes PRE_EXISTING_DATA_PROMPT=\`Available document types: {{ALL_DOCUMENT_TYPES}}\` API_KEY=$PAPERLESS_AI_API_KEY JWT_SECRET=$PAPERLESS_AI_JWT_SECRET CUSTOM_API_KEY=$LITELLM_AUTH_KEY CUSTOM_BASE_URL=http://litellm-proxy:4000/v1 CUSTOM_MODEL=Vision PAPERLESS_AI_INITIAL_SETUP=yes ACTIVATE_TAGGING=yes ACTIVATE_CORRESPONDENTS=yes ACTIVATE_DOCUMENT_TYPE=yes ACTIVATE_TITLE=no ACTIVATE_CUSTOM_FIELDS=no CUSTOM_FIELDS={"custom_fields":[]} DISABLE_AUTOMATIC_PROCESSING=no RESTRICT_TO_EXISTING_DOCUMENT_TYPES=yes AZURE_ENDPOINT= AZURE_API_KEY= AZURE_DEPLOYMENT_NAME= AZURE_API_VERSION= MISTRAL_OCR_ENABLED=yes OCR_PROVIDER=custom OCR_API_URL=http://litellm-proxy:4000/v1 OCR_API_KEY=$LITELLM_AUTH_KEY MISTRAL_OCR_MODEL=Vision EOFPA mv $HSHQ_STACKS_DIR/paperless/ai.env $HSHQ_STACKS_DIR/paperless/ai/.env } function mfPaperlessV11Update() { set +e rm -f $HOME/paperless-compose.yml cat < $HOME/paperless-compose.yml $STACK_VERSION_PREFIX paperless v11 services: paperless-db: image: mirror.gcr.io/postgres:15.0-bullseye container_name: paperless-db hostname: paperless-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.paperless-hourly-db.schedule=@every 1h" - "ofelia.job-exec.paperless-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-hourly-db.email-from=Paperless Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.paperless-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.paperless-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.paperless-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.paperless-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.paperless-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.paperless-monthly-db.email-from=Paperless Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.paperless-monthly-db.mail-only-on-error=false" paperless-gotenberg: image: mirror.gcr.io/gotenberg/gotenberg:8.34.0 container_name: paperless-gotenberg hostname: paperless-gotenberg restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: - "gotenberg" - "--chromium-disable-javascript=true" - "--chromium-allow-list=file:///tmp/.*" networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-tika: image: mirror.gcr.io/apache/tika:3.3.1.0-full container_name: paperless-tika hostname: paperless-tika restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro paperless-app: image: ghcr.io/homeserverhq/paperless-ngx:v3.0.5 container_name: paperless-app hostname: paperless-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - paperless-db - paperless-redis - paperless-gotenberg - paperless-tika networks: - dock-proxy-net - dock-internalmail-net - dock-privateip-net - dock-aipriv-net - int-paperless-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - v-paperless-data:/usr/src/paperless/data - v-paperless-media:/usr/src/paperless/media - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/export:/usr/src/paperless/export - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/consume:/usr/src/paperless/consume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedConsume:/usr/src/paperless/consume/SharedConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalConsume:/usr/src/paperless/consume/PersonalConsume - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalTranscribeOutput:/usr/src/paperless/consume/PersonalTranscribeOutput - \${PORTAINER_HSHQ_STACKS_DIR}/shared/SharedProcessed:/usr/src/paperless/media/documents/originals/SharedProcessed - \${PORTAINER_HSHQ_STACKS_DIR}/shared/PersonalProcessed:/usr/src/paperless/media/documents/originals/PersonalProcessed paperless-ai: image: hshq/paperless-ai-next:v1 container_name: paperless-ai hostname: paperless-ai restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/huggingface:/root/.cache/huggingface - v-paperless-ai:/app/data paperless-gpt: image: ghcr.io/icereed/paperless-gpt:v0.27.0 container_name: paperless-gpt hostname: paperless-gpt restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net - int-paperless-net depends_on: - paperless-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/prompts:/app/prompts - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/hocr:/app/hocr - \${PORTAINER_HSHQ_STACKS_DIR}/paperless/gpt/pdf:/app/pdf paperless-mcp: image: ghcr.io/homeserverhq/paperless-mcp:v2 container_name: paperless-mcp hostname: paperless-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-paperless-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro paperless-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: paperless-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $PAPERLESS_REDIS_PASSWORD --appendonly no networks: - int-paperless-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-paperless-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/data v-paperless-media: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/media v-paperless-ai: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/paperless/ai networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true dock-dbs-net: name: dock-dbs external: true dock-privateip-net: name: dock-privateip external: true dock-aipriv-net: name: dock-aipriv external: true int-paperless-net: driver: bridge internal: true ipam: driver: default EOFJT } function mfPaperlessV12Update() { set +e grep -q "PAPERLESS_AI_ENABLED" $HOME/paperless.env if [ $? -ne 0 ]; then echo "PAPERLESS_AI_ENABLED=true" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_EMBEDDING_BACKEND=openai-like" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_EMBEDDING_MODEL=Embed" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT=http://litellm-proxy:4000/v1" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_BACKEND=openai-like" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_MODEL=LongContext" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_API_KEY=$LITELLM_AUTH_KEY" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_ENDPOINT=http://litellm-proxy:4000/v1" >> $HOME/paperless.env echo "PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS=true" >> $HOME/paperless.env echo "PAPERLESS_OCR_USER_ARGS='{\"invalidate_digital_signatures\": true}'" >> $HOME/paperless.env fi grep -q "PRE_EXISTING_DATA_PROMPT" $HSHQ_STACKS_DIR/paperless/ai/.env.migrated if [ $? -ne 0 ]; then echo "PRE_EXISTING_DATA_PROMPT=\`Pre-existing document types: {{ALL_DOCUMENT_TYPES}}\`" >> $HSHQ_STACKS_DIR/paperless/ai/.env.migrated fi grep -q "PAPERLESS_DEFAULT_OBJECT_ACCESS_GROUP" $HOME/paperless.env if [ $? -ne 0 ]; then echo "PAPERLESS_DEFAULT_OBJECT_ACCESS_GROUP=primaryusers" >> $HOME/paperless.env fi } # Speedtest Tracker Local function installSpeedtestTrackerLocal() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory speedtest-tracker-local "SpeedtestTrackerLocal" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName speedtest-tracker-local-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName speedtest-tracker-local-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/speedtest-tracker-local mkdir $HSHQ_STACKS_DIR/speedtest-tracker-local/db mkdir $HSHQ_STACKS_DIR/speedtest-tracker-local/dbexport mkdir $HSHQ_STACKS_DIR/speedtest-tracker-local/config chmod 777 $HSHQ_STACKS_DIR/speedtest-tracker-local/dbexport initServicesCredentials set +e addUserMailu alias $SPEEDTEST_TRACKER_LOCAL_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$SPEEDTEST_TRACKER_LOCAL_INIT_ENV" = "true" ]; then sendEmail -s "SpeedtestTrackerLocal Admin Login Info" -b "SpeedtestTrackerLocal Admin Username: $SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS\nSpeedtestTrackerLocal Admin Password: $SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" SPEEDTEST_TRACKER_LOCAL_INIT_ENV=true updateConfigVar SPEEDTEST_TRACKER_LOCAL_INIT_ENV $SPEEDTEST_TRACKER_LOCAL_INIT_ENV fi outputConfigSpeedtestTrackerLocal installStack speedtest-tracker-local speedtest-tracker-local-app "\[ls.io-init\] done" $HOME/speedtest-tracker-local.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 5 docker exec speedtest-tracker-local-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/speedtest-tracker-local/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase SpeedtestTrackerLocal postgres speedtest-tracker-local-db $SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/speedtest-tracker-local/dbexport $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD sleep 4 startStopStack speedtest-tracker-local stop sleep 1 startStopStack speedtest-tracker-local start inner_block="" inner_block=$inner_block">>https://$SUB_SPEEDTEST_TRACKER_LOCAL.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://speedtest-tracker-local-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SPEEDTEST_TRACKER_LOCAL $MANAGETLS_SPEEDTEST_TRACKER_LOCAL "$is_integrate_hshq" $NETDEFAULT_SPEEDTEST_TRACKER_LOCAL "$inner_block" insertSubAuthelia $SUB_SPEEDTEST_TRACKER_LOCAL.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll speedtest-tracker-local "$FMLNAME_SPEEDTEST_TRACKER_LOCAL" $USERTYPE_SPEEDTEST_TRACKER_LOCAL "https://$SUB_SPEEDTEST_TRACKER_LOCAL.$HOMESERVER_DOMAIN" "speedtest-tracker.png" "$(getHeimdallOrderFromSub $SUB_SPEEDTEST_TRACKER_LOCAL $USERTYPE_SPEEDTEST_TRACKER_LOCAL)" restartAllCaddyContainers checkAddDBConnection true speedtest-tracker-local "$FMLNAME_SPEEDTEST_TRACKER_LOCAL" postgres speedtest-tracker-local-db $SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD fi } function outputConfigSpeedtestTrackerLocal() { pw_hash=$(htpasswd -bnBC 10 "" $SPEEDTEST_TRACKER_LOCAL_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$/\\$/g') rand_minute=$((3 + RANDOM % 24)) cat < $HOME/speedtest-tracker-local-compose.yml $STACK_VERSION_PREFIX speedtest-tracker-local $(getScriptStackVersion speedtest-tracker-local) services: speedtest-tracker-local-db: image: $(getScriptImageByContainerName speedtest-tracker-local-db) container_name: speedtest-tracker-local-db hostname: speedtest-tracker-local-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-speedtest-tracker-local-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/speedtest-tracker-local/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/speedtest-tracker-local/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.speedtest-tracker-local-hourly-db.schedule=@every 1h" - "ofelia.job-exec.speedtest-tracker-local-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.speedtest-tracker-local-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.speedtest-tracker-local-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.speedtest-tracker-local-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.speedtest-tracker-local-hourly-db.email-from=SpeedtestTrackerLocal Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.speedtest-tracker-local-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.speedtest-tracker-local-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.speedtest-tracker-local-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.speedtest-tracker-local-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.speedtest-tracker-local-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.speedtest-tracker-local-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.speedtest-tracker-local-monthly-db.email-from=SpeedtestTrackerLocal Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.speedtest-tracker-local-monthly-db.mail-only-on-error=false" speedtest-tracker-local-app: image: $(getScriptImageByContainerName speedtest-tracker-local-app) container_name: speedtest-tracker-local-app hostname: speedtest-tracker-local-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - speedtest-tracker-local-db networks: - dock-ext-net - dock-internalmail-net - int-speedtest-tracker-local-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${HSHQ_STACKS_DIR}/speedtest-tracker-local/config:/config networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-speedtest-tracker-local-net: driver: bridge internal: true ipam: driver: default EOFBA set -f cat < $HOME/speedtest-tracker-local.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID APP_NAME=Speedtest Tracker Local APP_URL=https://$SUB_SPEEDTEST_TRACKER_LOCAL.$HOMESERVER_DOMAIN APP_KEY=base64:$(openssl rand -base64 32) APP_TIMEZONE=${TZ} DISPLAY_TIMEZONE=${TZ} PUBLIC_DASHBOARD=false SPEEDTEST_SCHEDULE='$rand_minute */3 * * *' POSTGRES_DB=$SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME POSTGRES_USER=$SPEEDTEST_TRACKER_LOCAL_DATABASE_USER POSTGRES_PASSWORD=$SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' DB_CONNECTION=pgsql DB_HOST=speedtest-tracker-local-db DB_PORT=5432 DB_DATABASE=$SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME DB_USERNAME=$SPEEDTEST_TRACKER_LOCAL_DATABASE_USER DB_PASSWORD=$SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD MAIL_MAILER=smtp MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS MAIL_FROM_NAME=SpeedtestTrackerLocal $(getAdminEmailName) EOFBA set +f cat < $HSHQ_STACKS_DIR/speedtest-tracker-local/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$SPEEDTEST_TRACKER_LOCAL_DATABASE_USER_PASSWORD echo "update settings set payload='[ { \"email_address\": \"$EMAIL_ADMIN_EMAIL_ADDRESS\" } ]' where name='mail_recipients';" | psql -U $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER $SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME echo "update users set name='${HOMESERVER_ABBREV^^} SpeedtestTrackerLocal Admin', email='$SPEEDTEST_TRACKER_LOCAL_ADMIN_EMAIL_ADDRESS', password='$pw_hash' where id=1;" | psql -U $SPEEDTEST_TRACKER_LOCAL_DATABASE_USER $SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/speedtest-tracker-local/dbexport/setupDBSettings.sh } function performUpdateSpeedtestTrackerLocal() { perform_stack_name=speedtest-tracker-local prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.15.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.15.2,linuxserver/speedtest-tracker:0.15.4" ;; 2) newVer=v2 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.15.4 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.15.4,linuxserver/speedtest-tracker:0.15.4" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Speedtest Tracker cannot be upgraded to the next version. You must uninstall your current version and reinstall the new version." return ;; 3) newVer=v3 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.18.3 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.18.3,linuxserver/speedtest-tracker:0.18.3" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Speedtest Tracker cannot be upgraded to the next version. You must uninstall your current version and reinstall the new version." return ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.20.8 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.20.8,linuxserver/speedtest-tracker:1.3.0" ;; 5) newVer=v7 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:1.3.0 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:1.3.0,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0" ;; 6) newVer=v7 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/linuxserver/speedtest-tracker:1.6.6 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/linuxserver/speedtest-tracker:1.6.6,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0" ;; 7) newVer=v7 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Speedtest Tracker VPN function installSpeedtestTrackerVPN() { set +e if ! [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then return fi is_integrate_hshq=$1 checkDeleteStackAndDirectory speedtest-tracker-vpn "SpeedtestTrackerVPN" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName speedtest-tracker-vpn-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName speedtest-tracker-vpn-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/speedtest-tracker-vpn mkdir $HSHQ_STACKS_DIR/speedtest-tracker-vpn/db mkdir $HSHQ_STACKS_DIR/speedtest-tracker-vpn/dbexport mkdir $HSHQ_STACKS_DIR/speedtest-tracker-vpn/config chmod 777 $HSHQ_STACKS_DIR/speedtest-tracker-vpn/dbexport initServicesCredentials set +e addUserMailu alias $SPEEDTEST_TRACKER_VPN_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$SPEEDTEST_TRACKER_VPN_INIT_ENV" = "true" ]; then sendEmail -s "SpeedtestTrackerVPN Admin Login Info" -b "SpeedtestTrackerVPN Admin Username: $SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS\nSpeedtestTrackerVPN Admin Password: $SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" SPEEDTEST_TRACKER_VPN_INIT_ENV=true updateConfigVar SPEEDTEST_TRACKER_VPN_INIT_ENV $SPEEDTEST_TRACKER_VPN_INIT_ENV fi outputConfigSpeedtestTrackerVPN installStack speedtest-tracker-vpn speedtest-tracker-vpn-app "\[ls.io-init\] done" $HOME/speedtest-tracker-vpn.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 5 docker exec speedtest-tracker-vpn-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/speedtest-tracker-vpn/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase SpeedtestTrackerVPN postgres speedtest-tracker-vpn-db $SPEEDTEST_TRACKER_VPN_DATABASE_NAME $SPEEDTEST_TRACKER_VPN_DATABASE_USER $SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/speedtest-tracker-vpn/dbexport $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD sleep 4 startStopStack speedtest-tracker-vpn stop sleep 1 startStopStack speedtest-tracker-vpn start inner_block="" inner_block=$inner_block">>https://$SUB_SPEEDTEST_TRACKER_VPN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://speedtest-tracker-vpn-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SPEEDTEST_TRACKER_VPN $MANAGETLS_SPEEDTEST_TRACKER_VPN "$is_integrate_hshq" $NETDEFAULT_SPEEDTEST_TRACKER_VPN "$inner_block" insertSubAuthelia $SUB_SPEEDTEST_TRACKER_VPN.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll speedtest-tracker-vpn "$FMLNAME_SPEEDTEST_TRACKER_VPN" $USERTYPE_SPEEDTEST_TRACKER_VPN "https://$SUB_SPEEDTEST_TRACKER_VPN.$HOMESERVER_DOMAIN" "speedtest-tracker.png" "$(getHeimdallOrderFromSub $SUB_SPEEDTEST_TRACKER_VPN $USERTYPE_SPEEDTEST_TRACKER_VPN)" restartAllCaddyContainers checkAddDBConnection true speedtest-tracker-vpn "$FMLNAME_SPEEDTEST_TRACKER_VPN" postgres speedtest-tracker-vpn-db $SPEEDTEST_TRACKER_VPN_DATABASE_NAME $SPEEDTEST_TRACKER_VPN_DATABASE_USER $SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD fi } function outputConfigSpeedtestTrackerVPN() { pw_hash=$(htpasswd -bnBC 10 "" $SPEEDTEST_TRACKER_VPN_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$/\\$/g') rand_minute=$((33 + RANDOM % 24)) cat < $HOME/speedtest-tracker-vpn-compose.yml $STACK_VERSION_PREFIX speedtest-tracker-vpn $(getScriptStackVersion speedtest-tracker-vpn) services: speedtest-tracker-vpn-db: image: $(getScriptImageByContainerName speedtest-tracker-vpn-db) container_name: speedtest-tracker-vpn-db hostname: speedtest-tracker-vpn-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-speedtest-tracker-vpn-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/speedtest-tracker-vpn/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/speedtest-tracker-vpn/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.speedtest-tracker-vpn-hourly-db.schedule=@every 1h" - "ofelia.job-exec.speedtest-tracker-vpn-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.speedtest-tracker-vpn-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.speedtest-tracker-vpn-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.speedtest-tracker-vpn-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.speedtest-tracker-vpn-hourly-db.email-from=SpeedtestTrackerVPN Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.speedtest-tracker-vpn-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.speedtest-tracker-vpn-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.speedtest-tracker-vpn-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.speedtest-tracker-vpn-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.speedtest-tracker-vpn-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.speedtest-tracker-vpn-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.speedtest-tracker-vpn-monthly-db.email-from=SpeedtestTrackerVPN Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.speedtest-tracker-vpn-monthly-db.mail-only-on-error=false" speedtest-tracker-vpn-app: image: $(getScriptImageByContainerName speedtest-tracker-vpn-app) container_name: speedtest-tracker-vpn-app hostname: speedtest-tracker-vpn-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - speedtest-tracker-vpn-db networks: - dock-proxy-net - dwg-${RELAYSERVER_WG_INTERNET_NETNAME}-net - dock-internalmail-net - int-speedtest-tracker-vpn-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${HSHQ_STACKS_DIR}/speedtest-tracker-vpn/config:/config networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dwg-${RELAYSERVER_WG_INTERNET_NETNAME}-net: name: dwg-${RELAYSERVER_WG_INTERNET_NETNAME} external: true dock-dbs-net: name: dock-dbs external: true int-speedtest-tracker-vpn-net: driver: bridge internal: true ipam: driver: default EOFBA set -f cat < $HOME/speedtest-tracker-vpn.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID APP_NAME=Speedtest Tracker VPN APP_URL=https://$SUB_SPEEDTEST_TRACKER_VPN.$HOMESERVER_DOMAIN APP_KEY=base64:$(openssl rand -base64 32) APP_TIMEZONE=${TZ} DISPLAY_TIMEZONE=${TZ} PUBLIC_DASHBOARD=false SPEEDTEST_SCHEDULE='$rand_minute */3 * * *' POSTGRES_DB=$SPEEDTEST_TRACKER_VPN_DATABASE_NAME POSTGRES_USER=$SPEEDTEST_TRACKER_VPN_DATABASE_USER POSTGRES_PASSWORD=$SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' DB_CONNECTION=pgsql DB_HOST=speedtest-tracker-vpn-db DB_PORT=5432 DB_DATABASE=$SPEEDTEST_TRACKER_VPN_DATABASE_NAME DB_USERNAME=$SPEEDTEST_TRACKER_VPN_DATABASE_USER DB_PASSWORD=$SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD MAIL_MAILER=smtp MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS MAIL_FROM_NAME=SpeedtestTrackerVPN $(getAdminEmailName) EOFBA set +f cat < $HSHQ_STACKS_DIR/speedtest-tracker-vpn/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$SPEEDTEST_TRACKER_VPN_DATABASE_USER_PASSWORD echo "update settings set payload='[ { \"email_address\": \"$EMAIL_ADMIN_EMAIL_ADDRESS\" } ]' where name='mail_recipients';" | psql -U $SPEEDTEST_TRACKER_VPN_DATABASE_USER $SPEEDTEST_TRACKER_VPN_DATABASE_NAME echo "update users set name='${HOMESERVER_ABBREV^^} SpeedtestTrackerVPN Admin', email='$SPEEDTEST_TRACKER_VPN_ADMIN_EMAIL_ADDRESS', password='$pw_hash' where id=1;" | psql -U $SPEEDTEST_TRACKER_VPN_DATABASE_USER $SPEEDTEST_TRACKER_VPN_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/speedtest-tracker-vpn/dbexport/setupDBSettings.sh } function performUpdateSpeedtestTrackerVPN() { perform_stack_name=speedtest-tracker-vpn prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.15.2 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.15.2,linuxserver/speedtest-tracker:0.15.4" ;; 2) newVer=v2 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.15.4 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.15.4,linuxserver/speedtest-tracker:0.15.4" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Speedtest Tracker cannot be upgraded to the next version. You must uninstall your current version and reinstall the new version." return ;; 3) newVer=v3 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.18.3 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.18.3,linuxserver/speedtest-tracker:0.18.3" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Speedtest Tracker cannot be upgraded to the next version. You must uninstall your current version and reinstall the new version." return ;; 4) newVer=v5 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:0.20.8 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:0.20.8,linuxserver/speedtest-tracker:1.3.0" ;; 5) newVer=v7 curImageList=postgres:15.0-bullseye,linuxserver/speedtest-tracker:1.3.0 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="linuxserver/speedtest-tracker:1.3.0,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0" ;; 6) newVer=v7 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/linuxserver/speedtest-tracker:1.6.6 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/linuxserver/speedtest-tracker:1.6.6,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0" ;; 7) newVer=v7 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0,mirror.gcr.io/linuxserver/speedtest-tracker:1.12.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Change Detection function installChangeDetection() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory changedetection "$FMLNAME_CHANGEDETECTION" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName changedetection-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName changedetection-playwright-chrome) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/changedetection mkdir $HSHQ_STACKS_DIR/changedetection/data initServicesCredentials if ! [ "$CHANGEDETECTION_INIT_ENV" = "true" ]; then sendEmail -s "Change Detection Admin Login Info" -b "Change Detection Admin Password: $CHANGEDETECTION_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" CHANGEDETECTION_INIT_ENV=true updateConfigVar CHANGEDETECTION_INIT_ENV $CHANGEDETECTION_INIT_ENV fi outputConfigChangeDetection installStack changedetection changedetection-app "Running on http" $HOME/changedetection.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 pass_hash=$(docker exec -it changedetection-app python3 /datastore/genpass.py) startStopStack changedetection stop rem_item="notfound" rem_tag="notfound" tmp_item=$(cat $HSHQ_STACKS_DIR/changedetection/data/url-watches.json | jq -r '.watching[] | select(.url | contains("ycombinator.com")) | .uuid') if ! [ -z "$tmp_item" ]; then rem_item="$tmp_item" fi if ! [ -z "$rem_item" ] && ! [ "$rem_item" = "notfound" ]; then rem_tag=$(cat $HSHQ_STACKS_DIR/changedetection/data/url-watches.json | jq -r --arg remitem "$rem_item" '.watching[$remitem].tags[0]') fi cat $HSHQ_STACKS_DIR/changedetection/data/url-watches.json | jq --arg mylist "[ \"mailto://$SMTP_HOSTNAME:$SMTP_HOSTPORT?from=$EMAIL_ADMIN_EMAIL_ADDRESS&to=$EMAIL_ADMIN_EMAIL_ADDRESS\" ]" '.settings.application.notification_urls |= ($mylist | fromjson)' | jq --arg remtag "$rem_tag" 'del(.settings.application.tags[$remtag])' | jq --arg remitem $rem_item 'del(.watching[$remitem])' | jq --arg passhash $pass_hash '.settings.application.password |= $passhash' > $HSHQ_STACKS_DIR/changedetection/url-watches.json sudo rm -fr $HSHQ_STACKS_DIR/changedetection/data/$rem_item sudo chmod 644 $HSHQ_STACKS_DIR/changedetection/url-watches.json sudo chown root:root $HSHQ_STACKS_DIR/changedetection/url-watches.json sudo mv $HSHQ_STACKS_DIR/changedetection/url-watches.json $HSHQ_STACKS_DIR/changedetection/data/url-watches.json sudo rm -f $HSHQ_STACKS_DIR/changedetection/data/genpass.py startStopStack changedetection start inner_block="" inner_block=$inner_block">>https://$SUB_CHANGEDETECTION.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://changedetection-app:5000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CHANGEDETECTION $MANAGETLS_CHANGEDETECTION "$is_integrate_hshq" $NETDEFAULT_CHANGEDETECTION "$inner_block" insertSubAuthelia $SUB_CHANGEDETECTION.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll changedetection "$FMLNAME_CHANGEDETECTION" $USERTYPE_CHANGEDETECTION "https://$SUB_CHANGEDETECTION.$HOMESERVER_DOMAIN" "changedetection.png" "$(getHeimdallOrderFromSub $SUB_CHANGEDETECTION $USERTYPE_CHANGEDETECTION)" restartAllCaddyContainers fi } function outputConfigChangeDetection() { cat < $HOME/changedetection-compose.yml $STACK_VERSION_PREFIX changedetection $(getScriptStackVersion changedetection) services: changedetection-app: image: $(getScriptImageByContainerName changedetection-app) container_name: changedetection-app hostname: changedetection-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-changedetection-net - dock-ext-net - dock-internalmail-net depends_on: - changedetection-playwright-chrome volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-changedetection-data:/datastore changedetection-playwright-chrome: image: $(getScriptImageByContainerName changedetection-playwright-chrome) container_name: changedetection-playwright-chrome hostname: changedetection-playwright-chrome restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-changedetection-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-changedetection-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/changedetection/data networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true int-changedetection-net: driver: bridge internal: true ipam: driver: default EOFCD cat < $HOME/changedetection.env TZ=${TZ} PUID=$USERID PGID=$GROUPID PORT=5000 PLAYWRIGHT_DRIVER_URL=ws://changedetection-playwright-chrome:3000 BASE_URL=https://$SUB_CHANGEDETECTION.$HOMESERVER_DOMAIN HIDE_REFERER=true SCREEN_WIDTH=1920 SCREEN_HEIGHT=1024 SCREEN_DEPTH=16 MAX_CONCURRENT_CHROME_PROCESSES=10 STATS_REFRESH_SECONDS=30 EOFCD cat < $HSHQ_STACKS_DIR/changedetection/data/genpass.py import base64 import hashlib import secrets password="$CHANGEDETECTION_ADMIN_PASSWORD" salt = secrets.token_bytes(32) key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) store = base64.b64encode(salt + key).decode('ascii') print(store) EOFCD } function performUpdateChangeDetection() { perform_stack_name=changedetection prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=ghcr.io/dgtlmoon/changedetection.io:0.45.14,dgtlmoon/sockpuppetbrowser image_update_map[0]="ghcr.io/dgtlmoon/changedetection.io:0.45.14,ghcr.io/dgtlmoon/changedetection.io:0.47.05" image_update_map[1]="dgtlmoon/sockpuppetbrowser,dgtlmoon/sockpuppetbrowser:latest" ;; 2) newVer=v4 curImageList=ghcr.io/dgtlmoon/changedetection.io:0.45.16,dgtlmoon/sockpuppetbrowser:latest image_update_map[0]="ghcr.io/dgtlmoon/changedetection.io:0.45.16,ghcr.io/dgtlmoon/changedetection.io:0.47.05" image_update_map[1]="dgtlmoon/sockpuppetbrowser:latest,dgtlmoon/sockpuppetbrowser:latest" ;; 3) newVer=v4 curImageList=ghcr.io/dgtlmoon/changedetection.io:0.46.02,dgtlmoon/sockpuppetbrowser:latest image_update_map[0]="ghcr.io/dgtlmoon/changedetection.io:0.46.02,ghcr.io/dgtlmoon/changedetection.io:0.47.05" image_update_map[1]="dgtlmoon/sockpuppetbrowser:latest,dgtlmoon/sockpuppetbrowser:latest" ;; 4) newVer=v6 curImageList=ghcr.io/dgtlmoon/changedetection.io:0.47.05,dgtlmoon/sockpuppetbrowser:latest image_update_map[0]="ghcr.io/dgtlmoon/changedetection.io:0.47.05,ghcr.io/dgtlmoon/changedetection.io:0.50.8" image_update_map[1]="dgtlmoon/sockpuppetbrowser:latest,dgtlmoon/sockpuppetbrowser:latest" ;; 5) newVer=v6 curImageList=ghcr.io/dgtlmoon/changedetection.io:0.49.11,dgtlmoon/sockpuppetbrowser:latest image_update_map[0]="ghcr.io/dgtlmoon/changedetection.io:0.49.11,ghcr.io/dgtlmoon/changedetection.io:0.50.8" image_update_map[1]="dgtlmoon/sockpuppetbrowser:latest,dgtlmoon/sockpuppetbrowser:latest" ;; 6) newVer=v7 curImageList=ghcr.io/dgtlmoon/changedetection.io:0.50.8,dgtlmoon/sockpuppetbrowser:latest image_update_map[0]="ghcr.io/dgtlmoon/changedetection.io:0.50.8,ghcr.io/dgtlmoon/changedetection.io:0.51.4" image_update_map[1]="dgtlmoon/sockpuppetbrowser:latest,mirror.gcr.io/dgtlmoon/sockpuppetbrowser:latest" ;; 7) newVer=v7 curImageList=ghcr.io/dgtlmoon/changedetection.io:0.50.8,dgtlmoon/sockpuppetbrowser:latest image_update_map[0]="ghcr.io/dgtlmoon/changedetection.io:0.51.4,ghcr.io/dgtlmoon/changedetection.io:0.51.4" image_update_map[1]="mirror.gcr.io/dgtlmoon/sockpuppetbrowser:latest,mirror.gcr.io/dgtlmoon/sockpuppetbrowser:latest" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Huginn function installHuginn() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory huginn "$FMLNAME_HUGINN" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName huginn-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName huginn-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/huginn mkdir $HSHQ_STACKS_DIR/huginn/db mkdir $HSHQ_STACKS_DIR/huginn/dbexport chmod 777 $HSHQ_STACKS_DIR/huginn/dbexport initServicesCredentials if [ -z "$HUGINN_APP_SECRET_TOKEN" ]; then HUGINN_APP_SECRET_TOKEN=$(openssl rand -hex 64) updateConfigVar HUGINN_APP_SECRET_TOKEN $HUGINN_APP_SECRET_TOKEN fi set +e addUserMailu alias $HUGINN_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$HUGINN_INIT_ENV" = "true" ]; then sendEmail -s "Huginn Admin Login Info" -b "Huginn Admin Username: $HUGINN_ADMIN_USERNAME\nHuginn Admin Password: $HUGINN_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" HUGINN_INIT_ENV=true updateConfigVar HUGINN_INIT_ENV $HUGINN_INIT_ENV fi outputConfigHuginn installStack huginn huginn-app "stdout entered RUNNING state" $HOME/huginn.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase Huginn postgres huginn-db $HUGINN_DATABASE_NAME $HUGINN_DATABASE_USER $HUGINN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/huginn/dbexport $HUGINN_DATABASE_READONLYUSER $HUGINN_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_HUGINN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://huginn-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HUGINN $MANAGETLS_HUGINN "$is_integrate_hshq" $NETDEFAULT_HUGINN "$inner_block" insertSubAuthelia $SUB_HUGINN.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll huginn "$FMLNAME_HUGINN" $USERTYPE_HUGINN "https://$SUB_HUGINN.$HOMESERVER_DOMAIN" "huginn.png" "$(getHeimdallOrderFromSub $SUB_HUGINN $USERTYPE_HUGINN)" restartAllCaddyContainers checkAddDBConnection true huginn "$FMLNAME_HUGINN" postgres huginn-db $HUGINN_DATABASE_NAME $HUGINN_DATABASE_USER $HUGINN_DATABASE_USER_PASSWORD fi } function outputConfigHuginn() { cat < $HOME/huginn-compose.yml $STACK_VERSION_PREFIX huginn $(getScriptStackVersion huginn) services: huginn-db: image: $(getScriptImageByContainerName huginn-db) container_name: huginn-db hostname: huginn-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-huginn-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/huginn/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/huginn/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.huginn-hourly-db.schedule=@every 1h" - "ofelia.job-exec.huginn-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.huginn-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.huginn-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.huginn-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.huginn-hourly-db.email-from=Huginn Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.huginn-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.huginn-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.huginn-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.huginn-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.huginn-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.huginn-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.huginn-monthly-db.email-from=Huginn Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.huginn-monthly-db.mail-only-on-error=false" huginn-app: image: $(getScriptImageByContainerName huginn-app) container_name: huginn-app hostname: huginn-app restart: unless-stopped env_file: stack.env depends_on: - huginn-db networks: - int-huginn-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true int-huginn-net: driver: bridge internal: true ipam: driver: default EOFDZ cat < $HOME/huginn.env UID=$USERID GID=$GROUPID TZ=${TZ} POSTGRES_DB=$HUGINN_DATABASE_NAME POSTGRES_USER=$HUGINN_DATABASE_USER POSTGRES_PASSWORD=$HUGINN_DATABASE_USER_PASSWORD APP_SECRET_TOKEN=$HUGINN_APP_SECRET_TOKEN DOMAIN=$SUB_HUGINN.$HOMESERVER_DOMAIN PORT=3000 DATABASE_ADAPTER=postgresql DATABASE_ENCODING=utf8 DATABASE_RECONNECT=true DATABASE_NAME=$HUGINN_DATABASE_NAME DATABASE_POOL=20 DATABASE_USERNAME=$HUGINN_DATABASE_USER DATABASE_PASSWORD=$HUGINN_DATABASE_USER_PASSWORD DATABASE_HOST=huginn-db DATABASE_PORT=5432 FORCE_SSL=false SKIP_INVITATION_CODE=true REQUIRE_CONFIRMED_EMAIL=true ALLOW_UNCONFIRMED_ACCESS_FOR=0 MIN_PASSWORD_LENGTH=16 USE_JQ=jq USE_GRAPHVIZ_DOT=dot SMTP_DOMAIN=$SMTP_HOSTNAME SMTP_SERVER=$SMTP_HOSTNAME SMTP_PORT=$SMTP_HOSTPORT SMTP_USER_NAME=none SMTP_ENABLE_STARTTLS_AUTO=true SMTP_SSL=false EMAIL_FROM_ADDRESS=Huginn $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> SEED_USERNAME=$HUGINN_ADMIN_USERNAME SEED_PASSWORD=$HUGINN_ADMIN_PASSWORD SEED_EMAIL=$HUGINN_ADMIN_EMAIL_ADDRESS TWITTER_OAUTH_KEY= TWITTER_OAUTH_SECRET= THIRTY_SEVEN_SIGNALS_OAUTH_KEY= THIRTY_SEVEN_SIGNALS_OAUTH_SECRET= GITHUB_OAUTH_KEY= GITHUB_OAUTH_SECRET= TUMBLR_OAUTH_KEY= TUMBLR_OAUTH_SECRET= EVERNOTE_OAUTH_KEY= EVERNOTE_OAUTH_SECRET= USE_EVERNOTE_SANDBOX=true EOFDZ } function performUpdateHuginn() { perform_stack_name=huginn prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=postgres:15.0-bullseye,ghcr.io/huginn/huginn:c2839b8a78335a1cb7052d6ee1c4fbdc11ee6bb5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/huginn/huginn:c2839b8a78335a1cb7052d6ee1c4fbdc11ee6bb5,ghcr.io/huginn/huginn:1e0c359a46b1e84eb8c658404212eaf693b30e61" ;; 2) newVer=v3 curImageList=postgres:15.0-bullseye,ghcr.io/huginn/huginn:087bcdd8f21ead2dd4e587b554e8248e5ffd4b99 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/huginn/huginn:087bcdd8f21ead2dd4e587b554e8248e5ffd4b99,ghcr.io/huginn/huginn:1e0c359a46b1e84eb8c658404212eaf693b30e61" ;; 3) newVer=v4 curImageList=postgres:15.0-bullseye,ghcr.io/huginn/huginn:1e0c359a46b1e84eb8c658404212eaf693b30e61 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/huginn/huginn:1e0c359a46b1e84eb8c658404212eaf693b30e61,ghcr.io/huginn/huginn:e95438fe02e9fc5be85101029b2e4441d0822328" ;; 4) newVer=v4 curImageList=postgres:15.0-bullseye,ghcr.io/huginn/huginn:e95438fe02e9fc5be85101029b2e4441d0822328 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="ghcr.io/huginn/huginn:e95438fe02e9fc5be85101029b2e4441d0822328,ghcr.io/huginn/huginn:e95438fe02e9fc5be85101029b2e4441d0822328" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Coturn function installCoturn() { # Since we allow other stack install functions to install Coturn, # we need to just return if already installed. if [ -d $HSHQ_STACKS_DIR/coturn ]; then return fi set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory coturn "$FMLNAME_COTURN" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName coturn) retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi set -e mkdir $HSHQ_STACKS_DIR/coturn initServicesCredentials outputConfigCoturn generateCert coturn "coturn,$SUB_COTURN.$HOMESERVER_DOMAIN" set +e installStack coturn coturn "Total auth threads" $HOME/coturn.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 } function outputConfigCoturn() { cat < $HOME/coturn-compose.yml $STACK_VERSION_PREFIX coturn $(getScriptStackVersion coturn) services: coturn: image: $(getScriptImageByContainerName coturn) container_name: coturn hostname: coturn restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net ports: - "$COTURN_PRIMARY_PORT:$COTURN_PRIMARY_PORT" - "$COTURN_PRIMARY_PORT:$COTURN_PRIMARY_PORT/udp" - "$COTURN_SECONDARY_PORT:$COTURN_SECONDARY_PORT" - "$COTURN_SECONDARY_PORT:$COTURN_SECONDARY_PORT/udp" - "${COTURN_COMMS_MIN_PORT}-${COTURN_COMMS_MAX_PORT}:${COTURN_COMMS_MIN_PORT}-${COTURN_COMMS_MAX_PORT}/udp" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/coturn/turnserver.conf:/etc/coturn/turnserver.conf - \${PORTAINER_HSHQ_SSL_DIR}/coturn.crt:/usr/local/etc/cert.pem - \${PORTAINER_HSHQ_SSL_DIR}/coturn.key:/usr/local/etc/key.pem networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFOF cat < $HOME/coturn.env TZ=\${PORTAINER_TZ} EOFRM outputCoturnConf checkAddIPsCoturn } function outputCoturnConf() { sudo rm -f $HSHQ_STACKS_DIR/coturn/turnserver.conf cat < $HSHQ_STACKS_DIR/coturn/turnserver.conf listening-port=$COTURN_PRIMARY_PORT tls-listening-port=$COTURN_SECONDARY_PORT min-port=$COTURN_COMMS_MIN_PORT max-port=$COTURN_COMMS_MAX_PORT fingerprint use-auth-secret static-auth-secret=$COTURN_STATIC_SECRET realm=$SUB_COTURN.$HOMESERVER_DOMAIN total-quota=0 bps-capacity=0 stale-nonce=600 cert=/usr/local/etc/cert.pem pkey=/usr/local/etc/key.pem no-rfc5780 no-stun-backward-compatibility response-origin-only-with-rfc5780 no-cli no-multicast-peers denied-peer-ip=0.0.0.0-0.255.255.255 denied-peer-ip=10.0.0.0-10.255.255.255 denied-peer-ip=100.64.0.0-100.127.255.255 denied-peer-ip=127.0.0.0-127.255.255.255 denied-peer-ip=169.254.0.0-169.254.255.255 denied-peer-ip=172.16.0.0-172.31.255.255 denied-peer-ip=192.0.0.0-192.0.0.255 denied-peer-ip=192.0.2.0-192.0.2.255 denied-peer-ip=192.88.99.0-192.88.99.255 denied-peer-ip=192.168.0.0-192.168.255.255 denied-peer-ip=198.18.0.0-198.19.255.255 denied-peer-ip=198.51.100.0-198.51.100.255 denied-peer-ip=203.0.113.0-203.0.113.255 denied-peer-ip=240.0.0.0-255.255.255.255 denied-peer-ip=::1 denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 denied-peer-ip=100::-100::ffff:ffff:ffff:ffff denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff EOFRM } function checkAddIPsCoturn() { set +e if ! [ -f $HSHQ_STACKS_DIR/coturn/turnserver.conf ]; then return fi is_any_changed=false ifaceIPsArr=($(echo "$(getAllInterfaceIPs)" | tr "," "\n")) for curIP in "${ifaceIPsArr[@]}" do if [ -z "$curIP" ]; then continue fi grep "allowed-peer-ip=$curIP" $HSHQ_STACKS_DIR/coturn/turnserver.conf > /dev/null 2>&1 if [ $? -ne 0 ]; then is_any_changed=true fi done sed -i "/allowed-peer-ip=.*/d" $HSHQ_STACKS_DIR/coturn/turnserver.conf for curIP in "${ifaceIPsArr[@]}" do if [ -z "$curIP" ]; then continue fi echo "allowed-peer-ip=$curIP" >> $HSHQ_STACKS_DIR/coturn/turnserver.conf done ext_subnet_startIP=$(sipcalc $NET_EXTERNAL_SUBNET | grep "^Network range" | rev | cut -d"-" -f2- | xargs | cut -d" " -f1 | rev) ext_subnet_endIP=$(sipcalc $NET_EXTERNAL_SUBNET | grep "^Network range" | rev | cut -d" " -f1 | rev) echo "allowed-peer-ip=${ext_subnet_startIP}-${ext_subnet_endIP}" >> $HSHQ_STACKS_DIR/coturn/turnserver.conf if [ "$is_any_changed" = "true" ]; then return 1 fi } function updateCoturnAllowedIPs() { set +e if [ -f $HSHQ_STACKS_DIR/coturn/turnserver.conf ]; then checkAddIPsCoturn if [ $? -eq 1 ]; then docker ps | grep coturn > /dev/null 2>&1 if [ $? -eq 0 ]; then startStopStack coturn stop > /dev/null 2>&1 sleep 1 startStopStack coturn start > /dev/null 2>&1 fi fi fi } function performUpdateCoturn() { perform_stack_name=coturn prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v4 curImageList=coturn/coturn:4.6 image_update_map[0]="coturn/coturn:4.6,mirror.gcr.io/coturn/coturn:4.7.0" ;; 2) newVer=v4 curImageList=coturn/coturn:4.6.2 image_update_map[0]="coturn/coturn:4.6.2,mirror.gcr.io/coturn/coturn:4.7.0" ;; 3) newVer=v4 curImageList=coturn/coturn:4.6.3 image_update_map[0]="coturn/coturn:4.6.3,mirror.gcr.io/coturn/coturn:4.7.0" ;; 4) newVer=v4 curImageList=mirror.gcr.io/coturn/coturn:4.7.0 image_update_map[0]="mirror.gcr.io/coturn/coturn:4.7.0,mirror.gcr.io/coturn/coturn:4.7.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # FileDrop function installFileDrop() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory filedrop "FileDrop" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName filedrop) if [ $? -ne 0 ]; then buildOrPullImage filedrop/filedrop:1 fi if [ $? -ne 0 ]; then return 1 fi if ! [ -d $HSHQ_STACKS_DIR/coturn ]; then echo "Missing coturn, installing..." installCoturn retval=$? if [ $retval -ne 0 ]; then notifyStackInstallFailure coturn fi fi set -e mkdir $HSHQ_STACKS_DIR/filedrop outputConfigFileDrop installStack filedrop filedrop "Server running on" $HOME/filedrop.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 inner_block="" inner_block=$inner_block">>https://$SUB_FILEDROP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://filedrop:5000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FILEDROP $MANAGETLS_FILEDROP "$is_integrate_hshq" $NETDEFAULT_FILEDROP "$inner_block" insertSubAuthelia $SUB_FILEDROP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll filedrop "$FMLNAME_FILEDROP" $USERTYPE_FILEDROP "https://$SUB_FILEDROP.$HOMESERVER_DOMAIN" "filedrop.png" "$(getHeimdallOrderFromSub $SUB_FILEDROP $USERTYPE_FILEDROP)" restartAllCaddyContainers fi } function outputConfigFileDrop() { cat < $HOME/filedrop-compose.yml $STACK_VERSION_PREFIX filedrop $(getScriptStackVersion filedrop) services: filedrop: image: $(getScriptImageByContainerName filedrop) container_name: filedrop hostname: filedrop restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFDZ cat < $HOME/filedrop.env TZ=\${PORTAINER_TZ} WS_HOST=0.0.0.0 WS_APP_NAME=$HOMESERVER_NAME FileDrop WS_ABUSE_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS WS_REQUIRE_CRYPTO=1 TURN_MODE=hmac TURN_SERVER=turns:$SUB_COTURN.$HOMESERVER_DOMAIN:$COTURN_SECONDARY_PORT TURN_USERNAME=filedrop TURN_SECRET=$COTURN_STATIC_SECRET STUN_SERVER= EOFDZ } function performUpdateFileDrop() { perform_stack_name=filedrop prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=filedrop/filedrop:1 image_update_map[0]="filedrop/filedrop:1,filedrop/filedrop:1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function buildImageFileDropV1() { set +e sudo rm -fr $HSHQ_BUILD_DIR/filedrop git clone https://github.com/mat-sz/filedrop.git $HSHQ_BUILD_DIR/filedrop retVal=$? if [ $retVal -ne 0 ]; then sudo rm -fr $HSHQ_BUILD_DIR/filedrop return $retVal fi docker build --build-arg VITE_APP_NAME=FileDrop -t filedrop/filedrop:1 $HSHQ_BUILD_DIR/filedrop retVal=$? sudo rm -fr $HSHQ_BUILD_DIR/filedrop return $retVal } # Piped function installPiped() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory piped "Piped" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName piped-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName piped-frontend) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName piped-proxy) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName piped-api) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName piped-cron) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName piped-web) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/piped mkdir $HSHQ_STACKS_DIR/piped/db mkdir $HSHQ_STACKS_DIR/piped/dbexport mkdir $HSHQ_STACKS_DIR/piped/config mkdir $HSHQ_STACKS_DIR/piped/cron mkdir $HSHQ_STACKS_DIR/piped/web chmod 777 $HSHQ_STACKS_DIR/piped/dbexport mkdir $HSHQ_NONBACKUP_DIR/piped mkdir $HSHQ_NONBACKUP_DIR/piped/proxy initServicesCredentials outputConfigPiped installStack piped piped-api "Database connection is ready" $HOME/piped.env retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase Piped postgres piped-db $PIPED_DATABASE_NAME $PIPED_DATABASE_USER $PIPED_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/piped/dbexport $PIPED_DATABASE_READONLYUSER $PIPED_DATABASE_READONLYUSER_PASSWORD inner_block="" inner_block=$inner_block">>https://$SUB_PIPED_FRONTEND.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://piped-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PIPED_FRONTEND $MANAGETLS_PIPED_FRONTEND "$is_integrate_hshq" $NETDEFAULT_PIPED_FRONTEND "$inner_block" insertSubAuthelia $SUB_PIPED_FRONTEND.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_PIPED_PROXY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>uri replace \"&ump=1\" \"\"\n" inner_block=$inner_block">>>>>>reverse_proxy http://piped-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PIPED_PROXY $MANAGETLS_PIPED_PROXY "$is_integrate_hshq" $NETDEFAULT_PIPED_PROXY "$inner_block" insertSubAuthelia $SUB_PIPED_PROXY.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_PIPED_API.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://piped-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PIPED_API $MANAGETLS_PIPED_API "$is_integrate_hshq" $NETDEFAULT_PIPED_API "$inner_block" insertSubAuthelia $SUB_PIPED_API.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll piped "$FMLNAME_PIPED_FRONTEND" $USERTYPE_PIPED_FRONTEND "https://$SUB_PIPED_FRONTEND.$HOMESERVER_DOMAIN" "piped.png" "$(getHeimdallOrderFromSub $SUB_PIPED_FRONTEND $USERTYPE_PIPED_FRONTEND)" restartAllCaddyContainers checkAddDBConnection true piped "$FMLNAME_PIPED_FRONTEND" postgres piped-db $PIPED_DATABASE_NAME $PIPED_DATABASE_USER $PIPED_DATABASE_USER_PASSWORD fi } function outputConfigPiped() { cat < $HOME/piped-compose.yml $STACK_VERSION_PREFIX piped $(getScriptStackVersion piped) services: piped-db: image: $(getScriptImageByContainerName piped-db) container_name: piped-db hostname: piped-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-piped-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.piped-hourly-db.schedule=@every 1h" - "ofelia.job-exec.piped-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.piped-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.piped-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.piped-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.piped-hourly-db.email-from=Piped Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.piped-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.piped-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.piped-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.piped-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.piped-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.piped-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.piped-monthly-db.email-from=Piped Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.piped-monthly-db.mail-only-on-error=false" piped-frontend: image: $(getScriptImageByContainerName piped-frontend) container_name: piped-frontend hostname: piped-frontend restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true entrypoint: ash -c 'sed -i s/pipedapi.kavin.rocks/$SUB_PIPED_API.$HOMESERVER_DOMAIN/g /usr/share/nginx/html/assets/* && /docker-entrypoint.sh && nginx -g "daemon off;"' depends_on: - piped-api networks: - int-piped-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro piped-proxy: image: $(getScriptImageByContainerName piped-proxy) container_name: piped-proxy hostname: piped-proxy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-piped-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-piped-proxy:/app/socket piped-api: image: $(getScriptImageByContainerName piped-api) container_name: piped-api hostname: piped-api restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-piped-net - dock-ext-net depends_on: - piped-db volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/config/config.properties:/app/config.properties:ro piped-cron: image: $(getScriptImageByContainerName piped-cron) container_name: piped-cron hostname: piped-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true tty: true networks: - int-piped-net - dock-ext-net depends_on: - piped-web volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/cron/updateSubs.sh:/updateSubs.sh labels: - "ofelia.enabled=true" - "ofelia.job-exec.piped-subs.schedule=@every 15m" - "ofelia.job-exec.piped-subs.command=/updateSubs.sh" piped-web: image: $(getScriptImageByContainerName piped-web) container_name: piped-web hostname: piped-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-piped-net - dock-proxy-net depends_on: - piped-frontend - piped-proxy - piped-api volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/web/nginx.conf:/etc/nginx/nginx.conf:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/web/pipedapi.conf:/etc/nginx/conf.d/pipedapi.conf:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/web/pipedproxy.conf:/etc/nginx/conf.d/pipedproxy.conf:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/web/pipedfrontend.conf:/etc/nginx/conf.d/pipedfrontend.conf:ro - \${PORTAINER_HSHQ_STACKS_DIR}/piped/web/ytproxy.conf:/etc/nginx/snippets/ytproxy.conf:ro - v-piped-proxy:/var/run/ytproxy volumes: v-piped-proxy: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/piped/proxy networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-piped-net: driver: bridge internal: true ipam: driver: default EOFPI cat < $HOME/piped.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID POSTGRES_DB=$PIPED_DATABASE_NAME POSTGRES_USER=$PIPED_DATABASE_USER POSTGRES_PASSWORD=$PIPED_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C' UDS=1 EOFPI cat < $HSHQ_STACKS_DIR/piped/config/config.properties PORT: 8080 HTTP_WORKERS: 2 PROXY_PART: https://$SUB_PIPED_PROXY.$HOMESERVER_DOMAIN CAPTCHA_BASE_URL: CAPTCHA_API_KEY: API_URL: https://$SUB_PIPED_API.$HOMESERVER_DOMAIN FRONTEND_URL: https://$SUB_PIPED_FRONTEND.$HOMESERVER_DOMAIN COMPROMISED_PASSWORD_CHECK: false DISABLE_REGISTRATION: false FEED_RETENTION: 30 hibernate.connection.url: jdbc:postgresql://piped-db:5432/$PIPED_DATABASE_NAME hibernate.connection.driver_class: org.postgresql.Driver hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect hibernate.connection.username: $PIPED_DATABASE_USER hibernate.connection.password: $PIPED_DATABASE_USER_PASSWORD EOFPI cat < $HSHQ_STACKS_DIR/piped/web/nginx.conf user root; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; server_names_hash_bucket_size 128; log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' '\$status \$body_bytes_sent "\$http_referer" ' '"\$http_user_agent" "\$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nodelay on; keepalive_timeout 65; resolver 127.0.0.11 ipv6=off valid=10s; include /etc/nginx/conf.d/*.conf; } EOFPI cat < $HSHQ_STACKS_DIR/piped/web/pipedapi.conf proxy_cache_path /tmp/pipedapi_cache levels=1:2 keys_zone=pipedapi:4m max_size=2g inactive=60m use_temp_path=off; server { listen 80; server_name $SUB_PIPED_API.$HOMESERVER_DOMAIN; set \$backend "http://piped-api:8080"; location / { proxy_cache pipedapi; proxy_pass \$backend; proxy_http_version 1.1; proxy_set_header Connection "keep-alive"; } } EOFPI cat < $HSHQ_STACKS_DIR/piped/web/pipedfrontend.conf server { listen 80; server_name $SUB_PIPED_FRONTEND.$HOMESERVER_DOMAIN; set \$backend "http://piped-frontend:80"; location / { proxy_pass \$backend; proxy_http_version 1.1; proxy_set_header Connection "keep-alive"; } } EOFPI cat < $HSHQ_STACKS_DIR/piped/web/pipedproxy.conf server { listen 80; server_name $SUB_PIPED_PROXY.$HOMESERVER_DOMAIN; location ~ (/videoplayback|/api/v4/|/api/manifest/) { include snippets/ytproxy.conf; add_header Cache-Control private always; } location / { include snippets/ytproxy.conf; add_header Cache-Control "public, max-age=604800"; } } EOFPI cat < $HSHQ_STACKS_DIR/piped/web/ytproxy.conf proxy_buffering on; proxy_buffers 1024 16k; proxy_set_header X-Forwarded-For ""; proxy_set_header CF-Connecting-IP ""; proxy_hide_header "alt-svc"; sendfile on; sendfile_max_chunk 512k; tcp_nopush on; aio threads=default; aio_write on; directio 16m; proxy_hide_header Cache-Control; proxy_hide_header etag; proxy_http_version 1.1; proxy_set_header Connection keep-alive; proxy_max_temp_file_size 32m; access_log off; proxy_pass http://unix:/var/run/ytproxy/actix.sock; EOFPI cat < $HSHQ_STACKS_DIR/piped/cron/updateSubs.sh #!/bin/bash sqlcmd="SELECT DISTINCT(channel) FROM users_subscribed;" echo "\$sqlcmd" | PGPASSWORD=\$POSTGRES_PASSWORD psql -qtA -h piped-db -U \$POSTGRES_USER \$POSTGRES_DB | xargs -I\\{} curl -sk -o /dev/null "https://$SUB_PIPED_API.$HOMESERVER_DOMAIN/channel/{}" EOFPI chmod 751 $HSHQ_STACKS_DIR/piped/cron/updateSubs.sh } function performUpdatePiped() { perform_stack_name=piped prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=postgres:15.0-bullseye,1337kavin/piped-frontend:latest,1337kavin/piped-proxy:latest,1337kavin/piped:latest,barrypiccinni/psql-curl,nginx:1.25.3-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="1337kavin/piped-frontend:latest,1337kavin/piped-frontend:latest" image_update_map[2]="1337kavin/piped-proxy:latest,1337kavin/piped-proxy:latest" image_update_map[3]="1337kavin/piped:latest,1337kavin/piped:latest" image_update_map[4]="barrypiccinni/psql-curl,barrypiccinni/psql-curl" image_update_map[5]="nginx:1.25.3-alpine,nginx:1.27.4-alpine" ;; 2) newVer=v2 curImageList=postgres:15.0-bullseye,1337kavin/piped-frontend:latest,1337kavin/piped-proxy:latest,1337kavin/piped:latest,barrypiccinni/psql-curl,nginx:1.27.4-alpine image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="1337kavin/piped-frontend:latest,1337kavin/piped-frontend:latest" image_update_map[2]="1337kavin/piped-proxy:latest,1337kavin/piped-proxy:latest" image_update_map[3]="1337kavin/piped:latest,1337kavin/piped:latest" image_update_map[4]="barrypiccinni/psql-curl,barrypiccinni/psql-curl" image_update_map[5]="nginx:1.27.4-alpine,nginx:1.27.4-alpine" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # GrampsWeb function installGrampsWeb() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory grampsweb "GrampsWeb" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName grampsweb-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName grampsweb-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/grampsweb mkdir $HSHQ_STACKS_DIR/grampsweb/redis mkdir $HSHQ_STACKS_DIR/grampsweb/users mkdir $HSHQ_STACKS_DIR/grampsweb/index mkdir $HSHQ_STACKS_DIR/grampsweb/thumbcache mkdir $HSHQ_STACKS_DIR/grampsweb/cache mkdir $HSHQ_STACKS_DIR/grampsweb/secret mkdir $HSHQ_STACKS_DIR/grampsweb/db mkdir $HSHQ_STACKS_DIR/grampsweb/media mkdir $HSHQ_STACKS_DIR/grampsweb/tmp initServicesCredentials set +e addUserMailu alias $GRAMPSWEB_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS if ! [ "$GRAMPSWEB_INIT_ENV" = "true" ]; then sendEmail -s "GrampsWeb Admin Login Info" -b "GrampsWeb Admin Username: $GRAMPSWEB_ADMIN_USERNAME\nGrampsWeb Admin Password: $GRAMPSWEB_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" GRAMPSWEB_INIT_ENV=true updateConfigVar GRAMPSWEB_INIT_ENV $GRAMPSWEB_INIT_ENV fi outputConfigGrampsWeb installStack grampsweb grampsweb-app "Listening at: http://0.0.0.0:5000" $HOME/grampsweb.env retval=$? if [ $retval -ne 0 ]; then return $retval fi docker exec -it grampsweb-app python3 -m gramps_webapi user add --fullname "$HOMESERVER_NAME Admin" --email "$GRAMPSWEB_ADMIN_EMAIL_ADDRESS" --role 5 "$GRAMPSWEB_ADMIN_USERNAME" "$GRAMPSWEB_ADMIN_PASSWORD" > /dev/null 2>&1 sudo sqlite3 $HSHQ_STACKS_DIR/grampsweb/users/users.sqlite "insert into configuration values(1,'EMAIL_HOST','$SMTP_HOSTNAME');" sudo sqlite3 $HSHQ_STACKS_DIR/grampsweb/users/users.sqlite "insert into configuration values(2,'EMAIL_PORT','465');" sudo sqlite3 $HSHQ_STACKS_DIR/grampsweb/users/users.sqlite "insert into configuration values(3,'EMAIL_HOST_USER','$EMAIL_SMTP_EMAIL_ADDRESS');" sudo sqlite3 $HSHQ_STACKS_DIR/grampsweb/users/users.sqlite "insert into configuration values(4,'EMAIL_HOST_PASSWORD','$EMAIL_SMTP_PASSWORD');" sudo sqlite3 $HSHQ_STACKS_DIR/grampsweb/users/users.sqlite "insert into configuration values(5,'DEFAULT_FROM_EMAIL','GrampsWeb $(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>');" sudo sqlite3 $HSHQ_STACKS_DIR/grampsweb/users/users.sqlite "insert into configuration values(6,'BASE_URL','https://$SUB_GRAMPSWEB.$HOMESERVER_DOMAIN');" sudo sqlite3 $HSHQ_STACKS_DIR/grampsweb/users/users.sqlite "insert into configuration values(7,'EMAIL_USE_TLS','0');" startStopStack grampsweb stop startStopStack grampsweb start inner_block="" inner_block=$inner_block">>https://$SUB_GRAMPSWEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://grampsweb-app:5000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_GRAMPSWEB $MANAGETLS_GRAMPSWEB "$is_integrate_hshq" $NETDEFAULT_GRAMPSWEB "$inner_block" insertSubAuthelia $SUB_GRAMPSWEB.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll grampsweb "$FMLNAME_GRAMPSWEB" $USERTYPE_GRAMPSWEB "https://$SUB_GRAMPSWEB.$HOMESERVER_DOMAIN" "grampsweb.png" "$(getHeimdallOrderFromSub $SUB_GRAMPSWEB $USERTYPE_GRAMPSWEB)" restartAllCaddyContainers fi } function outputConfigGrampsWeb() { cat < $HOME/grampsweb-compose.yml $STACK_VERSION_PREFIX grampsweb $(getScriptStackVersion grampsweb) services: grampsweb-app: image: $(getScriptImageByContainerName grampsweb-app) container_name: grampsweb-app hostname: grampsweb-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - grampsweb-redis networks: - int-grampsweb-net - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-grampsweb-users:/app/users - v-grampsweb-index:/app/indexdir - v-grampsweb-thumbcache:/app/thumbnail_cache - v-grampsweb-cache:/app/cache - v-grampsweb-secret:/app/secret - v-grampsweb-db:/root/.gramps/grampsdb - v-grampsweb-media:/app/media - v-grampsweb-tmp:/tmp grampsweb-celery: image: $(getScriptImageByContainerName grampsweb-app) container_name: grampsweb-celery hostname: grampsweb-celery restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: celery -A gramps_webapi.celery worker --loglevel=INFO depends_on: - grampsweb-redis networks: - int-grampsweb-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-grampsweb-users:/app/users - v-grampsweb-index:/app/indexdir - v-grampsweb-thumbcache:/app/thumbnail_cache - v-grampsweb-cache:/app/cache - v-grampsweb-secret:/app/secret - v-grampsweb-db:/root/.gramps/grampsdb - v-grampsweb-media:/app/media - v-grampsweb-tmp:/tmp grampsweb-redis: image: $(getScriptImageByContainerName grampsweb-redis) container_name: grampsweb-redis hostname: grampsweb-redis restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: redis-server --requirepass $GRAMPSWEB_REDIS_PASSWORD --appendonly yes networks: - int-grampsweb-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-grampsweb-redis:/data volumes: v-grampsweb-redis: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/redis v-grampsweb-users: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/users v-grampsweb-index: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/index v-grampsweb-thumbcache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/thumbcache v-grampsweb-cache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/cache v-grampsweb-secret: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/secret v-grampsweb-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/db v-grampsweb-media: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/media v-grampsweb-tmp: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/grampsweb/tmp networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true int-grampsweb-net: driver: bridge internal: true ipam: driver: default EOFPI cat < $HOME/grampsweb.env TZ=\${PORTAINER_TZ} UID=$USERID GID=$GROUPID GRAMPSWEB_SECRET_KEY=$GRAMPSWEB_SECRET_KEY GRAMPSWEB_TREE=GrampsWeb Tree GRAMPSWEB_CELERY_CONFIG__broker_url=redis://:$GRAMPSWEB_REDIS_PASSWORD@grampsweb-redis:6379/0 GRAMPSWEB_CELERY_CONFIG__result_backend=redis://:$GRAMPSWEB_REDIS_PASSWORD@grampsweb-redis:6379/0 GRAMPSWEB_RATELIMIT_STORAGE_URI=redis://:$GRAMPSWEB_REDIS_PASSWORD@grampsweb-redis:6379/1 REDIS_PASSWORD=$GRAMPSWEB_REDIS_PASSWORD EOFPI } function performUpdateGrampsWeb() { perform_stack_name=grampsweb prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=ghcr.io/gramps-project/grampsweb:v24.8.0,bitnami/redis:7.0.5 image_update_map[0]="ghcr.io/gramps-project/grampsweb:v24.8.0,ghcr.io/gramps-project/grampsweb:v25.4.0" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 2) newVer=v3 curImageList=ghcr.io/gramps-project/grampsweb:v24.10.0,bitnami/redis:7.0.5 image_update_map[0]="ghcr.io/gramps-project/grampsweb:v24.10.0,ghcr.io/gramps-project/grampsweb:v25.4.0" image_update_map[1]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 3) newVer=v4 curImageList=ghcr.io/gramps-project/grampsweb:v25.4.0,bitnami/redis:7.4.2 image_update_map[0]="ghcr.io/gramps-project/grampsweb:v25.4.0,ghcr.io/gramps-project/grampsweb:25.7.3" image_update_map[1]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfGrampsWebFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v4 curImageList=ghcr.io/gramps-project/grampsweb:25.7.3,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="ghcr.io/gramps-project/grampsweb:25.7.3,ghcr.io/gramps-project/grampsweb:25.11.2" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 5) # This version is broken, come back to it later... newVer=v5 curImageList=ghcr.io/gramps-project/grampsweb:25.11.2,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="ghcr.io/gramps-project/grampsweb:25.11.2,ghcr.io/gramps-project/grampsweb:25.11.2" image_update_map[1]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfGrampsWebFixRedisCompose() { lineStart=" grampsweb-redis:" lineEnd=" - v-grampsweb-redis:/bitnami/redis/data" replaceText="" replaceText=$replaceText">>grampsweb-redis:\n" replaceText=$replaceText">>>>image: mirror.gcr.io/redis:8.2.0-bookworm\n" replaceText=$replaceText">>>>container_name: grampsweb-redis\n" replaceText=$replaceText">>>>restart: unless-stopped\n" replaceText=$replaceText">>>>env_file: stack.env\n" replaceText=$replaceText">>>>security_opt:\n" replaceText=$replaceText">>>>>>- no-new-privileges:true\n" replaceText=$replaceText">>>>command: redis-server\n" replaceText=$replaceText">>>>>>--requirepass $GRAMPSWEB_REDIS_PASSWORD\n" replaceText=$replaceText">>>>networks:\n" replaceText=$replaceText">>>>>>- int-grampsweb-net\n" replaceText=$replaceText">>>>volumes:\n" replaceText=$replaceText">>>>>>- /etc/localtime:/etc/localtime:ro\n" replaceText=$replaceText">>>>>>- /etc/timezone:/etc/timezone:ro\n" replaceText=$replaceText">>>>>>- v-grampsweb-redis:/data\n" replaceTextBlockInFile "$lineStart" "$lineEnd" "$replaceText" $HOME/grampsweb-compose.yml false ">" cat -s $HOME/grampsweb-compose.yml > $HOME/grampsweb.tmp mv $HOME/grampsweb.tmp $HOME/grampsweb-compose.yml sudo rm -fr $HSHQ_NONBACKUP_DIR/grampsweb/redis/* sed -i "s/dock-grampsweb-net/int-grampsweb-net/g" $HOME/grampsweb-compose.yml } # Penpot function installPenpot() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory penpot "$FMLNAME_PENPOT" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName penpot-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName penpot-backend) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName penpot-frontend) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName penpot-exporter) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName penpot-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $IMG_TOMCAT if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/penpot mkdir $HSHQ_STACKS_DIR/penpot/db mkdir $HSHQ_STACKS_DIR/penpot/dbexport mkdir $HSHQ_STACKS_DIR/penpot/ssl mkdir $HSHQ_STACKS_DIR/penpot/assets chmod 777 $HSHQ_STACKS_DIR/penpot/dbexport initServicesCredentials set +e docker run --rm --name=tomcat -v $HSHQ_STACKS_DIR/penpot/ssl:/certsexport -v /usr/local/share/ca-certificates:/usr/local/share/ca-certificates $IMG_TOMCAT bash -c "keytool -importcert -file /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt -alias ${CERTS_ROOT_CA_NAME}.crt -cacerts -storepass changeit -noprompt; cp /opt/java/openjdk/lib/security/cacerts /certsexport/" if ! [ -f $HSHQ_STACKS_DIR/penpot/ssl/cacerts ]; then echo "There was an error generating the java keystore with the added CA certificate, returning..." return 2 fi outputConfigPenpot installStack penpot penpot-backend "welcome to penpot" $HOME/penpot.env 10 600 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase Penpot postgres penpot-db $PENPOT_DATABASE_NAME $PENPOT_DATABASE_USER $PENPOT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/penpot/dbexport $PENPOT_DATABASE_READONLYUSER $PENPOT_DATABASE_READONLYUSER_PASSWORD if ! [ "$PENPOT_INIT_ENV" = "true" ]; then sendEmail -s "Penpot Login Info" -b "Penpot has been installed. It has been configured and integrated with LDAP. However, you must use the email address of the LDAP user rather than the username. The password is the LDAP user's password. Here are the credentials for the LDAP Admin user: \n\nEmail: $EMAIL_ADMIN_EMAIL_ADDRESS\nPassword: $LDAP_ADMIN_USER_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" PENPOT_INIT_ENV=true updateConfigVar PENPOT_INIT_ENV $PENPOT_INIT_ENV fi inner_block="" inner_block=$inner_block">>https://$SUB_PENPOT.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://penpot-frontend:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PENPOT $MANAGETLS_PENPOT "$is_integrate_hshq" $NETDEFAULT_PENPOT "$inner_block" insertSubAuthelia $SUB_PENPOT.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll penpot "$FMLNAME_PENPOT" $USERTYPE_PENPOT "https://$SUB_PENPOT.$HOMESERVER_DOMAIN" "penpot.png" "$(getHeimdallOrderFromSub $SUB_PENPOT $USERTYPE_PENPOT)" restartAllCaddyContainers checkAddDBConnection true penpot "$FMLNAME_PENPOT" postgres penpot-db $PENPOT_DATABASE_NAME $PENPOT_DATABASE_USER $PENPOT_DATABASE_USER_PASSWORD fi docker image rm $IMG_TOMCAT > /dev/null 2>&1 } function outputConfigPenpot() { cat < $HOME/penpot-compose.yml $STACK_VERSION_PREFIX penpot $(getScriptStackVersion penpot) services: penpot-db: image: $(getScriptImageByContainerName penpot-db) container_name: penpot-db hostname: penpot-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-penpot-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/penpot/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/penpot/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.penpot-hourly-db.schedule=@every 1h" - "ofelia.job-exec.penpot-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.penpot-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.penpot-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.penpot-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.penpot-hourly-db.email-from=Penpot Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.penpot-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.penpot-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.penpot-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.penpot-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.penpot-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.penpot-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.penpot-monthly-db.email-from=Penpot Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.penpot-monthly-db.mail-only-on-error=false" penpot-frontend: image: $(getScriptImageByContainerName penpot-frontend) container_name: penpot-frontend hostname: penpot-frontend restart: unless-stopped env_file: stack.env depends_on: - penpot-backend - penpot-exporter networks: - int-penpot-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-penpot-assets:/opt/data/assets penpot-backend: image: $(getScriptImageByContainerName penpot-backend) container_name: penpot-backend hostname: penpot-backend restart: unless-stopped env_file: stack.env depends_on: - penpot-db - penpot-redis networks: - int-penpot-net - dock-ldap-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/penpot/ssl/cacerts:/opt/jdk/lib/security/cacerts:ro - v-penpot-assets:/opt/data/assets penpot-exporter: image: $(getScriptImageByContainerName penpot-exporter) container_name: penpot-exporter hostname: penpot-exporter restart: unless-stopped env_file: stack.env depends_on: - penpot-db networks: - int-penpot-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro environment: - PENPOT_PUBLIC_URI=http://penpot-frontend penpot-redis: image: $(getScriptImageByContainerName penpot-redis) container_name: penpot-redis hostname: penpot-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $PENPOT_REDIS_PASSWORD --appendonly no networks: - int-penpot-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-penpot-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/penpot/db v-penpot-assets: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/penpot/assets networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true int-penpot-net: driver: bridge internal: true ipam: driver: default EOFPI cat < $HOME/penpot.env TZ=\${PORTAINER_TZ} PENPOT_FLAGS=enable-webhooks enable-smtp enable-prepl-server enable-login-with-ldap enable-backend-api-doc disable-login-with-password disable-onboarding PENPOT_PUBLIC_URI=https://$SUB_PENPOT.$HOMESERVER_DOMAIN PENPOT_TELEMETRY_ENABLED=false PENPOT_SECRET_KEY=$PENPOT_SECRET_KEY PENPOT_SMTP_DEFAULT_FROM=Penpot $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> PENPOT_SMTP_DEFAULT_REPLY_TO=$EMAIL_ADMIN_EMAIL_ADDRESS PENPOT_SMTP_HOST=$SMTP_HOSTNAME PENPOT_SMTP_PORT=$SMTP_HOSTPORT PENPOT_SMTP_TLS=true PENPOT_ASSETS_STORAGE_BACKEND=assets-fs PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets PENPOT_DATABASE_URI=postgresql://penpot-db/$PENPOT_DATABASE_NAME PENPOT_DATABASE_USERNAME=$PENPOT_DATABASE_USER PENPOT_DATABASE_PASSWORD=$PENPOT_DATABASE_USER_PASSWORD PENPOT_REDIS_URI=redis://:$PENPOT_REDIS_PASSWORD@penpot-redis:6379/0 POSTGRES_DB=$PENPOT_DATABASE_NAME POSTGRES_USER=$PENPOT_DATABASE_USER POSTGRES_PASSWORD=$PENPOT_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--data-checksums PENPOT_LDAP_HOST=ldapserver PENPOT_LDAP_PORT=389 PENPOT_LDAP_SSL=false PENPOT_LDAP_STARTTLS=true PENPOT_LDAP_BASE_DN=ou=people,$LDAP_BASE_DN PENPOT_LDAP_BIND_DN=$LDAP_READONLY_USER_BIND_DN PENPOT_LDAP_BIND_PASSWORD=$LDAP_READONLY_USER_PASSWORD PENPOT_LDAP_USER_QUERY=(&(|(uid=:username)(mail=:username))(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)) PENPOT_LDAP_ATTRS_USERNAME=uid PENPOT_LDAP_ATTRS_EMAIL=mail PENPOT_LDAP_ATTRS_FULLNAME=cn PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto EOFPI } function performUpdatePenpot() { perform_stack_name=penpot prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=postgres:15.0-bullseye,penpotapp/backend:2.2.1,penpotapp/frontend:2.2.1,penpotapp/exporter:2.2.1,bitnami/redis:7.0.5 image_update_map[0]="postgres:15.0-bullseye,postgres:15.0-bullseye" image_update_map[1]="penpotapp/backend:2.2.1,penpotapp/backend:2.5.4" image_update_map[2]="penpotapp/frontend:2.2.1,penpotapp/frontend:2.5.4" image_update_map[3]="penpotapp/exporter:2.2.1,penpotapp/exporter:2.5.4" image_update_map[4]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" mfPenpotFixPort false perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v3 curImageList=postgres:15.0-bullseye,penpotapp/backend:2.5.4,penpotapp/frontend:2.5.4,penpotapp/exporter:2.5.4,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="penpotapp/backend:2.5.4,mirror.gcr.io/penpotapp/backend:2.8.1" image_update_map[2]="penpotapp/frontend:2.5.4,mirror.gcr.io/penpotapp/frontend:2.8.1" image_update_map[3]="penpotapp/exporter:2.5.4,mirror.gcr.io/penpotapp/exporter:2.8.1" image_update_map[4]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPenpotFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v4 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/penpotapp/backend:2.8.1,mirror.gcr.io/penpotapp/frontend:2.8.1,mirror.gcr.io/penpotapp/exporter:2.8.1,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/penpotapp/backend:2.8.1,mirror.gcr.io/penpotapp/backend:2.11.1" image_update_map[2]="mirror.gcr.io/penpotapp/frontend:2.8.1,mirror.gcr.io/penpotapp/frontend:2.11.1" image_update_map[3]="mirror.gcr.io/penpotapp/exporter:2.8.1,mirror.gcr.io/penpotapp/exporter:2.11.1" image_update_map[4]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 4) newVer=v4 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/penpotapp/backend:2.11.1,mirror.gcr.io/penpotapp/frontend:2.11.1,mirror.gcr.io/penpotapp/exporter:2.11.1,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/penpotapp/backend:2.11.1,mirror.gcr.io/penpotapp/backend:2.11.1" image_update_map[2]="mirror.gcr.io/penpotapp/frontend:2.11.1,mirror.gcr.io/penpotapp/frontend:2.11.1" image_update_map[3]="mirror.gcr.io/penpotapp/exporter:2.11.1,mirror.gcr.io/penpotapp/exporter:2.11.1" image_update_map[4]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfPenpotFixPort() { inner_block="" inner_block=$inner_block">>https://$SUB_PENPOT.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://penpot-frontend:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PENPOT $MANAGETLS_PENPOT "$is_integrate_hshq" $NETDEFAULT_PENPOT "$inner_block" set +e restartAllCaddyContainers } function mfPenpotFixRedisCompose() { replaceRedisBlock penpot penpot-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/penpot/redis/* } # EspoCRM function installEspoCRM() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory espocrm "$FMLNAME_ESPOCRM" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName espocrm-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName espocrm-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName espocrm-daemon) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName espocrm-websocket) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/espocrm mkdir $HSHQ_STACKS_DIR/espocrm/db mkdir $HSHQ_STACKS_DIR/espocrm/dbexport mkdir $HSHQ_STACKS_DIR/espocrm/app chmod 777 $HSHQ_STACKS_DIR/espocrm/dbexport initServicesCredentials set +e outputConfigEspoCRM installStack espocrm espocrm-app "apache2 -D FOREGROUND" $HOME/espocrm.env 5 retval=$? if [ $retval -ne 0 ]; then return $retval fi sleep 3 addReadOnlyUserToDatabase EspoCRM mysql espocrm-db $ESPOCRM_DATABASE_NAME root $ESPOCRM_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/espocrm/dbexport $ESPOCRM_DATABASE_READONLYUSER $ESPOCRM_DATABASE_READONLYUSER_PASSWORD if ! [ "$ESPOCRM_INIT_ENV" = "true" ]; then sendEmail -s "EspoCRM Login Info" -b "EspoCRM Admin Username: $ESPOCRM_ADMIN_USERNAME\nPassword: $ESPOCRM_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" ESPOCRM_INIT_ENV=true updateConfigVar ESPOCRM_INIT_ENV $ESPOCRM_INIT_ENV fi inner_block="" inner_block=$inner_block">>https://$SUB_ESPOCRM.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy /ws espocrm-websocket:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://espocrm-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ESPOCRM $MANAGETLS_ESPOCRM "$is_integrate_hshq" $NETDEFAULT_ESPOCRM "$inner_block" insertSubAuthelia $SUB_ESPOCRM.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll espocrm "$FMLNAME_ESPOCRM" $USERTYPE_ESPOCRM "https://$SUB_ESPOCRM.$HOMESERVER_DOMAIN" "espocrm.png" "$(getHeimdallOrderFromSub $SUB_ESPOCRM $USERTYPE_ESPOCRM)" restartAllCaddyContainers checkAddDBConnection true espocrm "$FMLNAME_ESPOCRM" mysql espocrm-db $ESPOCRM_DATABASE_NAME $ESPOCRM_DATABASE_USER $ESPOCRM_DATABASE_USER_PASSWORD fi } function outputConfigEspoCRM() { cat < $HOME/espocrm-compose.yml $STACK_VERSION_PREFIX espocrm $(getScriptStackVersion espocrm) services: espocrm-db: image: $(getScriptImageByContainerName espocrm-db) container_name: espocrm-db hostname: espocrm-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-espocrm-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-espocrm-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/espocrm/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.espocrm-hourly-db.schedule=@every 1h" - "ofelia.job-exec.espocrm-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.espocrm-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.espocrm-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.espocrm-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.espocrm-hourly-db.email-from=EspoCRM Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.espocrm-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.espocrm-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.espocrm-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.espocrm-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.espocrm-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.espocrm-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.espocrm-monthly-db.email-from=EspoCRM Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.espocrm-monthly-db.mail-only-on-error=false" espocrm-app: image: $(getScriptImageByContainerName espocrm-app) container_name: espocrm-app hostname: espocrm-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - espocrm-db networks: - int-espocrm-net - dock-internalmail-net - dock-ldap-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-espocrm-app:/var/www/html espocrm-daemon: image: $(getScriptImageByContainerName espocrm-daemon) container_name: espocrm-daemon hostname: espocrm-daemon restart: unless-stopped env_file: stack.env entrypoint: docker-daemon.sh security_opt: - no-new-privileges:true depends_on: - espocrm-db networks: - int-espocrm-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-espocrm-app:/var/www/html espocrm-websocket: image: $(getScriptImageByContainerName espocrm-websocket) container_name: espocrm-websocket hostname: espocrm-websocket restart: unless-stopped env_file: stack.env entrypoint: docker-websocket.sh security_opt: - no-new-privileges:true depends_on: - espocrm-db networks: - int-espocrm-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-espocrm-app:/var/www/html volumes: v-espocrm-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/espocrm/db v-espocrm-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/espocrm/app networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true dock-ldap-net: name: dock-ldap external: true int-espocrm-net: driver: bridge internal: true ipam: driver: default EOFPI cat < $HOME/espocrm.env ESPOCRM_TIME_ZONE=\${PORTAINER_TZ} ESPOCRM_DATE_FORMAT=YYYY-MM-DD ESPOCRM_DATABASE_HOST=espocrm-db ESPOCRM_DATABASE_PORT=3306 ESPOCRM_DATABASE_NAME=$ESPOCRM_DATABASE_NAME ESPOCRM_DATABASE_USER=$ESPOCRM_DATABASE_USER ESPOCRM_DATABASE_PASSWORD=$ESPOCRM_DATABASE_USER_PASSWORD MYSQL_DATABASE=$ESPOCRM_DATABASE_NAME MYSQL_ROOT_PASSWORD=$ESPOCRM_DATABASE_ROOT_PASSWORD MYSQL_USER=$ESPOCRM_DATABASE_USER MYSQL_PASSWORD=$ESPOCRM_DATABASE_USER_PASSWORD ESPOCRM_ADMIN_USERNAME=$ESPOCRM_ADMIN_USERNAME ESPOCRM_ADMIN_PASSWORD=$ESPOCRM_ADMIN_PASSWORD ESPOCRM_SITE_URL=https://$SUB_ESPOCRM.$HOMESERVER_DOMAIN ESPOCRM_CONFIG_USE_WEB_SOCKET=true ESPOCRM_CONFIG_WEB_SOCKET_URL=wss://$SUB_ESPOCRM.$HOMESERVER_DOMAIN/ws ESPOCRM_CONFIG_WEB_SOCKET_ZERO_M_Q_SUBSCRIBER_DSN=tcp://*:7777 ESPOCRM_CONFIG_WEB_SOCKET_ZERO_M_Q_SUBMISSION_DSN=tcp://espocrm-websocket:7777 ESPOCRM_CONFIG_OUTBOUND_EMAIL_IS_SHARED=false ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_NAME=EspoCRM $(getAdminEmailName) ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_ADDRESS=$EMAIL_SMTP_EMAIL_ADDRESS ESPOCRM_CONFIG_SMTP_SERVER=$SMTP_HOSTNAME ESPOCRM_CONFIG_SMTP_PORT=$SMTP_HOSTPORT ESPOCRM_CONFIG_SMTP_AUTH=false ESPOCRM_CONFIG_SMTP_SECURITY=TLS ESPOCRM_CONFIG_AUTHENTICATION_METHOD=LDAP ESPOCRM_CONFIG_THEME=Dark ESPOCRM_CONFIG_LDAP_HOST=ldapserver ESPOCRM_CONFIG_LDAP_PORT=389 ESPOCRM_CONFIG_LDAP_SECURITY=TLS ESPOCRM_CONFIG_LDAP_BASE_DN=ou=people,$LDAP_BASE_DN ESPOCRM_CONFIG_LDAP_USER_NAME_ATTRIBUTE=uid ESPOCRM_CONFIG_LDAP_USER_LOGIN_FILTER=memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN ESPOCRM_CONFIG_LDAP_AUTH=true ESPOCRM_CONFIG_LDAP_USERNAME=$LDAP_READONLY_USER_BIND_DN ESPOCRM_CONFIG_LDAP_PASSWORD=$LDAP_READONLY_USER_PASSWORD ESPOCRM_CONFIG_LDAP_ACCOUNT_CANONICAL_FORM=Dn ESPOCRM_CONFIG_LDAP_CREATE_ESPO_USER=true EOFPI } function performUpdateEspoCRM() { perform_stack_name=espocrm prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mariadb:10.7.3,espocrm/espocrm:8.4.2-apache image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="espocrm/espocrm:8.4.2-apache,espocrm/espocrm:9.0.6-apache" ;; 2) newVer=v3 curImageList=mariadb:10.7.3,espocrm/espocrm:9.0.6-apache image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="espocrm/espocrm:9.0.6-apache,mirror.gcr.io/espocrm/espocrm:9.1.8-apache" ;; 3) newVer=v4 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/espocrm/espocrm:9.1.8-apache image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/espocrm/espocrm:9.1.8-apache,mirror.gcr.io/espocrm/espocrm:9.2.5-apache" ;; 4) newVer=v4 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/espocrm/espocrm:9.2.5-apache image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/espocrm/espocrm:9.2.5-apache,mirror.gcr.io/espocrm/espocrm:9.2.5-apache" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Immich function installImmich() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory immich "$FMLNAME_IMMICH_APP" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName immich-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName immich-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName immich-ml) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName immich-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName immich-mcp) if [ $? -ne 0 ]; then return 1 fi mkdir $HSHQ_STACKS_DIR/immich mkdir $HSHQ_STACKS_DIR/immich/db mkdir $HSHQ_STACKS_DIR/immich/dbexport mkdir $HSHQ_STACKS_DIR/immich/app mkdir $HSHQ_STACKS_DIR/immich/config chmod 777 $HSHQ_STACKS_DIR/immich/dbexport mkdir $HSHQ_NONBACKUP_DIR/immich mkdir $HSHQ_NONBACKUP_DIR/immich/cache initServicesCredentials set +e addUserMailu alias $IMMICH_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS IMMICH_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $IMMICH_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2b/') immich_admin_uuid=$(uuidgen) outputConfigImmich oidcBlock=$(cat $HOME/immich.oidc) rm -f $HOME/immich.oidc insertOIDCClientAuthelia immich "$oidcBlock" set +e installStack immich immich-app "Immich Server is listening on" $HOME/immich.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi addMCPServerLiteLLM "immich" "imm" "http://immich-mcp:80/mcp" http none "" docker exec -u postgres immich-db psql immichdb immich-user -f /dbexport/addadmin.sql > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/immich/dbexport/addadmin.sql addImmichAPIKeyDB "$IMMICH_ADMIN_EMAIL_ADDRESS" "$IMMICH_API_KEY" addReadOnlyUserToDatabase Immich postgres immich-db $IMMICH_DATABASE_NAME $IMMICH_DATABASE_USER $IMMICH_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/immich/dbexport $IMMICH_DATABASE_READONLYUSER $IMMICH_DATABASE_READONLYUSER_PASSWORD if ! [ "$IMMICH_INIT_ENV" = "true" ]; then sendEmail -s "Immich Login Info" -b "Immich Admin Username: $IMMICH_ADMIN_EMAIL_ADDRESS\nPassword: $IMMICH_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" IMMICH_INIT_ENV=true updateConfigVar IMMICH_INIT_ENV $IMMICH_INIT_ENV fi set +e inner_block="" inner_block=$inner_block">>https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://immich-app:2283 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_IMMICH_APP $MANAGETLS_IMMICH_APP "$is_integrate_hshq" $NETDEFAULT_IMMICH_APP "$inner_block" insertSubAuthelia $SUB_IMMICH_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll immich "$FMLNAME_IMMICH_APP" $USERTYPE_IMMICH_APP "https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN" "immich.png" "$(getHeimdallOrderFromSub $SUB_IMMICH_APP $USERTYPE_IMMICH_APP)" restartAllCaddyContainers checkAddDBConnection true immich "$FMLNAME_IMMICH_APP" postgres immich-db $IMMICH_DATABASE_NAME $IMMICH_DATABASE_USER $IMMICH_DATABASE_USER_PASSWORD fi sleep 3 jsonbody="{ \"name\": \"HSHQ SuperAdmin's Library\", \"ownerId\": \"$immich_admin_uuid\", \"type\": \"EXTERNAL\", \"importPaths\": [\"/shared/Images/$NEXTCLOUD_ADMIN_USERNAME/Images\"] }" curl -s -X POST "https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/api/libraries" -H "Content-Type: application/json" -H "x-api-key: $IMMICH_API_KEY" -d "$jsonbody" > /dev/null 2>&1 } function outputConfigImmich() { outputComposeImmich cat < $HOME/immich.env TZ=\${PORTAINER_TZ} NO_COLOR=true IMMICH_CONFIG_FILE=/config/immich.json IMMICH_PROCESS_INVALID_IMAGES=true IMMICH_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 DB_HOSTNAME=immich-db DB_DATABASE_NAME=$IMMICH_DATABASE_NAME DB_USERNAME=$IMMICH_DATABASE_USER DB_PASSWORD=$IMMICH_DATABASE_USER_PASSWORD POSTGRES_PASSWORD=$IMMICH_DATABASE_USER_PASSWORD POSTGRES_USER=$IMMICH_DATABASE_USER POSTGRES_DB=$IMMICH_DATABASE_NAME POSTGRES_INITDB_ARGS=--data-checksums REDIS_HOSTNAME=immich-redis REDIS_PASSWORD=$IMMICH_REDIS_PASSWORD NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt IMMICH_BASE_URL=http://immich-app:2283 IMMICH_EXT_URL=https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN MCP_LOG_LEVEL=Information DOWNLOAD_MODE=url MAX_PAGE_SIZE=100 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false IMMICH_PUBLIC_URL=https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN EOFPI outputImmichJSONConfig outputImmichAutheliaOIDC curdt=$(date '+%Y-%m-%d %H:%M:%S.%3N') cat < $HSHQ_STACKS_DIR/immich/dbexport/addadmin.sql insert into "user"(id,email,password,"createdAt","profileImagePath","isAdmin","shouldChangePassword","deletedAt","oauthId","updatedAt","storageLabel",name,"quotaSizeInBytes","quotaUsageInBytes",status,"profileChangedAt") values('$immich_admin_uuid','$IMMICH_ADMIN_EMAIL_ADDRESS','$IMMICH_ADMIN_PASSWORD_HASH','$curdt','',true,true,NULL,'','$curdt','admin','$(getAdminEmailName) Immich',NULL,0,'active','$curdt'); EOFIM } function outputComposeImmich() { cat < $HOME/immich-compose.yml $STACK_VERSION_PREFIX immich $(getScriptStackVersion immich) services: immich-db: image: $(getScriptImageByContainerName immich-db) container_name: immich-db hostname: immich-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-immich-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/immich/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/immich/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.immich-hourly-db.schedule=@every 1h" - "ofelia.job-exec.immich-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.immich-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.immich-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.immich-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.immich-hourly-db.email-from=Immich Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.immich-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.immich-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.immich-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.immich-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.immich-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.immich-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.immich-monthly-db.email-from=Immich Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.immich-monthly-db.mail-only-on-error=false" immich-app: image: $(getScriptImageByContainerName immich-app) container_name: immich-app hostname: immich-app restart: unless-stopped env_file: stack.env depends_on: - immich-db networks: - int-immich-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/immich/app:/usr/src/app/upload - \${PORTAINER_HSHQ_STACKS_DIR}/immich/config/immich.json:/config/immich.json - \${PORTAINER_HSHQ_STACKS_DIR}/shared/Images:/shared/Images:ro immich-ml: image: $(getScriptImageByContainerName immich-ml) container_name: immich-ml hostname: immich-ml restart: unless-stopped env_file: stack.env healthcheck: disable: false depends_on: - immich-db networks: - int-immich-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-immich-cache:/cache immich-mcp: image: $(getScriptImageByContainerName immich-mcp) container_name: immich-mcp hostname: immich-mcp restart: unless-stopped env_file: stack.env healthcheck: disable: true security_opt: - no-new-privileges:true pull_policy: always networks: - int-immich-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro immich-redis: image: $(getScriptImageByContainerName immich-redis) container_name: immich-redis hostname: immich-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $IMMICH_REDIS_PASSWORD --appendonly no networks: - int-immich-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-immich-cache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/immich/cache networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true int-immich-net: driver: bridge internal: true ipam: driver: default EOFPI } function outputImmichJSONConfig() { rm -f $HSHQ_STACKS_DIR/immich/config/immich.json cat < $HSHQ_STACKS_DIR/immich/config/immich.json { "ffmpeg": { "crf": 23, "threads": 0, "preset": "ultrafast", "targetVideoCodec": "h264", "acceptedVideoCodecs": ["h264"], "targetAudioCodec": "aac", "acceptedAudioCodecs": ["aac", "mp3", "opus", "pcm_s16le"], "acceptedContainers": ["mov", "ogg", "webm"], "targetResolution": "720", "maxBitrate": "0", "bframes": -1, "refs": 0, "gopSize": 0, "temporalAQ": false, "cqMode": "auto", "twoPass": false, "preferredHwDevice": "auto", "transcode": "required", "tonemap": "hable", "accel": "disabled", "accelDecode": false }, "backup": { "database": { "enabled": false, "cronExpression": "0 02 * * *", "keepLastAmount": 14 } }, "job": { "backgroundTask": { "concurrency": 5 }, "smartSearch": { "concurrency": 2 }, "metadataExtraction": { "concurrency": 5 }, "faceDetection": { "concurrency": 2 }, "search": { "concurrency": 5 }, "sidecar": { "concurrency": 5 }, "library": { "concurrency": 5 }, "migration": { "concurrency": 5 }, "thumbnailGeneration": { "concurrency": 3 }, "videoConversion": { "concurrency": 1 }, "notifications": { "concurrency": 5 } }, "logging": { "enabled": true, "level": "log" }, "machineLearning": { "enabled": true, "urls": ["http://immich-ml:3003"], "clip": { "enabled": true, "modelName": "ViT-B-32__openai" }, "duplicateDetection": { "enabled": true, "maxDistance": 0.01 }, "facialRecognition": { "enabled": true, "modelName": "buffalo_l", "minScore": 0.7, "maxDistance": 0.5, "minFaces": 3 } }, "map": { "enabled": true, "lightStyle": "https://tiles.immich.cloud/v1/style/light.json", "darkStyle": "https://tiles.immich.cloud/v1/style/dark.json" }, "reverseGeocoding": { "enabled": true }, "metadata": { "faces": { "import": false } }, "oauth": { "autoLaunch": false, "autoRegister": true, "buttonText": "Login with Authelia", "clientId": "immich", "clientSecret": "$IMMICH_OIDC_CLIENT_SECRET", "defaultStorageQuota": 0, "enabled": true, "issuerUrl": "https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/.well-known/openid-configuration", "mobileOverrideEnabled": false, "mobileRedirectUri": "", "scope": "openid profile email", "signingAlgorithm": "RS256", "profileSigningAlgorithm": "none", "storageLabelClaim": "preferred_username", "storageQuotaClaim": "immich_quota" }, "passwordLogin": { "enabled": true }, "storageTemplate": { "enabled": false, "hashVerificationEnabled": true, "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" }, "image": { "thumbnail": { "format": "webp", "size": 250, "quality": 80 }, "preview": { "format": "jpeg", "size": 1440, "quality": 80 }, "colorspace": "p3", "extractEmbedded": false }, "newVersionCheck": { "enabled": true }, "trash": { "enabled": true, "days": 30 }, "theme": { "customCss": "" }, "library": { "scan": { "enabled": true, "cronExpression": "*/15 * * * *" }, "watch": { "enabled": true } }, "server": { "externalDomain": "https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/", "loginPageMessage": "Welcome to Immich on $HOMESERVER_NAME!" }, "notifications": { "smtp": { "enabled": true, "from": "Immich $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>", "replyTo": "$EMAIL_ADMIN_EMAIL_ADDRESS", "transport": { "ignoreCert": false, "host": "$SMTP_HOSTNAME", "port": $SMTP_HOSTPORT, "username": "", "password": "" } } }, "user": { "deleteDelay": 7 } } EOFIM } function outputImmichAutheliaOIDC() { cat < $HOME/immich.oidc # Authelia OIDC Client immich BEGIN - client_id: immich client_name: Immich client_secret: '$(htpasswd -bnBC 10 "" $IMMICH_OIDC_CLIENT_SECRET | tr -d ':\n')' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth redirect_uris: - https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/auth/login - https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN/user-settings - app.immich:///oauth-callback scopes: - openid - profile - email - groups response_types: - code grant_types: - authorization_code access_token_signed_response_alg: none userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_post # Authelia OIDC Client immich END EOFIM } function buildImageImmichMCPV1() { set +e echo -e "\n========================================================================" echo -e " The Immich MCP image is being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/ImmichMCP-Multitenant cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 https://github.com/homeserverhq/ImmichMCP-Multitenant.git cd $HSHQ_BUILD_DIR/ImmichMCP-Multitenant docker image build -t hshq/immich-mcp:v1 -f ./Dockerfile . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/ImmichMCP-Multitenant return $rtval } function performUpdateImmich() { perform_stack_name=immich prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0,ghcr.io/immich-app/immich-server:v1.121.0,ghcr.io/immich-app/immich-machine-learning:v1.121.0,bitnami/redis:7.0.5 image_update_map[0]="docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0,tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0" image_update_map[1]="ghcr.io/immich-app/immich-server:v1.121.0,ghcr.io/immich-app/immich-server:v1.131.3" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v1.121.0,ghcr.io/immich-app/immich-machine-learning:v1.131.3" image_update_map[3]="bitnami/redis:7.0.5,bitnami/redis:7.4.2" ;; 2) # Must go to v1.132.0 first, then to v1.137.3 newVer=v3 curImageList=tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0,ghcr.io/immich-app/immich-server:v1.131.3,ghcr.io/immich-app/immich-machine-learning:v1.131.3,bitnami/redis:7.4.2 image_update_map[0]="tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0,mirror.gcr.io/tensorchord/pgvecto-rs:pg14-v0.3.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v1.131.3,ghcr.io/immich-app/immich-server:v1.132.0" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v1.131.3,ghcr.io/immich-app/immich-machine-learning:v1.132.0" image_update_map[3]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfImmichFixRedisCompose if [ $? -eq 0 ] && [ "$is_upgrade_error" = "false" ]; then is_upgrade_error=true perform_update_report="WARNING ($perform_stack_name): If the Immich service does not work after this upgrade, then try restarting the stack (in Portainer)." else # Handle special case for a different V2 curImageList=tensorchord/pgvecto-rs:pg14-v0.3.0,ghcr.io/immich-app/immich-server:v1.131.3,ghcr.io/immich-app/immich-machine-learning:v1.131.3,bitnami/redis:7.4.2 image_update_map[0]="tensorchord/pgvecto-rs:pg14-v0.3.0,mirror.gcr.io/tensorchord/pgvecto-rs:pg14-v0.3.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v1.131.3,ghcr.io/immich-app/immich-server:v1.132.0" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v1.131.3,ghcr.io/immich-app/immich-machine-learning:v1.132.0" image_update_map[3]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" is_upgrade_error=false stack_upgrade_report="" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfImmichFixRedisCompose if [ $? -eq 0 ] && [ "$is_upgrade_error" = "false" ]; then is_upgrade_error=true perform_update_report="WARNING ($perform_stack_name): If the Immich service does not work after this upgrade, then try restarting the stack (in Portainer)." else perform_update_report="${perform_update_report}$stack_upgrade_report" fi fi return ;; 3) newVer=v4 curImageList=tensorchord/pgvecto-rs:pg14-v0.3.0,ghcr.io/immich-app/immich-server:v1.132.0,ghcr.io/immich-app/immich-machine-learning:v1.132.0,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="tensorchord/pgvecto-rs:pg14-v0.3.0,mirror.gcr.io/tensorchord/pgvecto-rs:pg14-v0.3.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v1.132.0,ghcr.io/immich-app/immich-server:v1.137.3" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v1.132.0,ghcr.io/immich-app/immich-machine-learning:v1.137.3" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" ;; 4) newVer=v6 curImageList=tensorchord/pgvecto-rs:pg14-v0.3.0,ghcr.io/immich-app/immich-server:v1.137.3,ghcr.io/immich-app/immich-machine-learning:v1.137.3,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="tensorchord/pgvecto-rs:pg14-v0.3.0,ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v1.137.3,ghcr.io/immich-app/immich-server:v2.0.1" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v1.137.3,ghcr.io/immich-app/immich-machine-learning:v2.0.1" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfImmichV6Update perform_update_report="${perform_update_report}$stack_upgrade_report" immichUpdateRestartStack return ;; 5) newVer=v6 curImageList=tensorchord/pgvecto-rs:pg14-v0.3.0,ghcr.io/immich-app/immich-server:v2.0.1,ghcr.io/immich-app/immich-machine-learning:v2.0.1,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="tensorchord/pgvecto-rs:pg14-v0.3.0,ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v2.0.1,ghcr.io/immich-app/immich-server:v2.0.1" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v2.0.1,ghcr.io/immich-app/immich-machine-learning:v2.0.1" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfImmichV6Update perform_update_report="${perform_update_report}$stack_upgrade_report" immichUpdateRestartStack return ;; 6) newVer=v7 curImageList=ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0,ghcr.io/immich-app/immich-server:v2.0.1,ghcr.io/immich-app/immich-machine-learning:v2.0.1,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0,ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v2.0.1,ghcr.io/immich-app/immich-server:v2.3.1" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v2.0.1,ghcr.io/immich-app/immich-machine-learning:v2.3.1" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 7) newVer=v8 curImageList=ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0,ghcr.io/immich-app/immich-server:v2.3.1,ghcr.io/immich-app/immich-machine-learning:v2.3.1,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0,ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v2.3.1,ghcr.io/immich-app/immich-server:v2.7.5" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v2.3.1,ghcr.io/immich-app/immich-machine-learning:v2.7.5" image_update_map[3]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfImmichV8Update perform_update_report="${perform_update_report}$stack_upgrade_report" waitForContainerLogString immich-app 5 180 "Immich Server is listening on" if [ $? -eq 0 ]; then addImmichAPIKeyDB "$IMMICH_ADMIN_EMAIL_ADDRESS" "$IMMICH_API_KEY" else echo "ERROR: There is a problem with the immich container after upgrading. Please check the logs." fi return ;; 8) newVer=v9 curImageList=ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0,ghcr.io/immich-app/immich-server:v2.7.5,ghcr.io/immich-app/immich-machine-learning:v2.7.5,mirror.gcr.io/valkey/valkey:alpine3.23,hshq/immich-mcp:v1 image_update_map[0]="ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0,ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v2.7.5,ghcr.io/immich-app/immich-server:v3.0.2" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v2.7.5,ghcr.io/immich-app/immich-machine-learning:v3.0.2" image_update_map[3]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[4]="hshq/immich-mcp:v1,ghcr.io/homeserverhq/immich-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfImmichV10Update perform_update_report="${perform_update_report}$stack_upgrade_report" immichUpdateRestartStack return ;; 9) newVer=v9 curImageList=ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0,ghcr.io/immich-app/immich-server:v3.0.2,ghcr.io/immich-app/immich-machine-learning:v3.0.2,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/immich-mcp:v2 image_update_map[0]="ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0,ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0" image_update_map[1]="ghcr.io/immich-app/immich-server:v3.0.2,ghcr.io/immich-app/immich-server:v3.0.2" image_update_map[2]="ghcr.io/immich-app/immich-machine-learning:v3.0.2,ghcr.io/immich-app/immich-machine-learning:v3.0.2" image_update_map[3]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[4]="ghcr.io/homeserverhq/immich-mcp:v2,ghcr.io/homeserverhq/immich-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" immichUpdateRestartStack } function mfImmichFixRedisCompose() { replaceRedisBlock immich immich-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/immich/redis/* outputImmichAutheliaOIDC oidcBlock=$(cat $HOME/immich.oidc) rm -f $HOME/immich.oidc insertOIDCClientAuthelia immich "$oidcBlock" } function mfImmichV6Update() { outputImmichJSONConfig sed -i "/command: \['postgres'.*/d" $HOME/immich-compose.yml sudo rm -fr $HSHQ_NONBACKUP_DIR/immich/redis/* } function immichUpdateRestartStack() { echo "Update complete, restarting stack..." waitForStack "Immich Server is listening" immich-app if [ "$isStackReady" = "true" ]; then startStopStack immich stop sleep 1 startStopStack immich start echo "Stack restarted!" else echo "ERROR: There was a problem with the Immich update. You might need to restart the immich stack." fi } function mfImmichV8Update() { set +e initServicesCredentials buildImageImmichMCPV1 if [ $? -ne 0 ]; then return 1 fi if [ "$stackStatus" = "1" ];then # Stack is running, stop it, then clear data startStopStack immich stop isStartStackFromStopped=true sleep 1 fi sudo rm -fr $HSHQ_NONBACKUP_DIR/immich/redis/* cat < $HOME/immich-compose.yml $STACK_VERSION_PREFIX immich v8 services: immich-db: image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.3.0 container_name: immich-db hostname: immich-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-immich-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/immich/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/immich/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.immich-hourly-db.schedule=@every 1h" - "ofelia.job-exec.immich-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.immich-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.immich-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.immich-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.immich-hourly-db.email-from=Immich Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.immich-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.immich-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.immich-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.immich-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.immich-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.immich-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.immich-monthly-db.email-from=Immich Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.immich-monthly-db.mail-only-on-error=false" immich-app: image: ghcr.io/immich-app/immich-server:v2.7.5 container_name: immich-app hostname: immich-app restart: unless-stopped env_file: stack.env depends_on: - immich-db networks: - int-immich-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/immich/app:/usr/src/app/upload - \${PORTAINER_HSHQ_STACKS_DIR}/immich/config/immich.json:/config/immich.json - \${PORTAINER_HSHQ_STACKS_DIR}/shared/Images:/shared/Images:ro immich-ml: image: hcr.io/immich-app/immich-machine-learning:v2.7.5 container_name: immich-ml hostname: immich-ml restart: unless-stopped env_file: stack.env healthcheck: disable: false depends_on: - immich-db networks: - int-immich-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-immich-cache:/cache immich-mcp: image: hshq/immich-mcp:v1 container_name: immich-mcp hostname: immich-mcp restart: unless-stopped env_file: stack.env healthcheck: disable: true security_opt: - no-new-privileges:true pull_policy: always networks: - int-immich-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro immich-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: immich-redis hostname: immich-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $IMMICH_REDIS_PASSWORD --appendonly no networks: - int-immich-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-immich-cache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/immich/cache networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true dock-aipriv-net: name: dock-aipriv external: true int-immich-net: driver: bridge internal: true ipam: driver: default EOFPI addMCPServerLiteLLM "immich" "imm" "http://immich-mcp:80/mcp" http none "" grep -q IMMICH_BASE_URL $HOME/immich.env if [ $? -ne 0 ]; then echo "IMMICH_BASE_URL=http://immich-app:2283" >> $HOME/immich.env echo "IMMICH_EXT_URL=https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN" >> $HOME/immich.env echo "MCP_LOG_LEVEL=Information" >> $HOME/immich.env echo "DOWNLOAD_MODE=url" >> $HOME/immich.env echo "MAX_PAGE_SIZE=100" >> $HOME/immich.env echo "IMMICH_API_KEY=$IMMICH_API_KEY" >> $HOME/immich.env fi } function addImmichAPIKeyDB() { set +e add_immich_email="$1" add_immich_api_key="$2" add_immich_api_hash=$(echo -n "$add_immich_api_key" | openssl dgst -sha256 -hex | rev | cut -d" " -f1 | rev) randUUID1=$(uuidgen) randUUID2=$(uuidgen) dtnow=$(date '+%Y-%m-%d %H:%M:%S.%3N') cat < $HSHQ_STACKS_DIR/immich/dbexport/setAPIKey.sh #!/bin/bash PGPASSWORD=$IMMICH_DATABASE_USER_PASSWORD userUUID=\$(echo "select id from \"user\" where email='$add_immich_email';" | psql -t -A -U $IMMICH_DATABASE_USER $IMMICH_DATABASE_NAME 2> /dev/null) if [ -z "\$userUUID" ]; then echo "Could not find user, key not added..." return fi echo "insert into api_key(name,key,\"userId\",\"createdAt\",\"updatedAt\",id,permissions,\"updateId\") values('API Key',decode('$add_immich_api_hash', 'hex'),'\$userUUID','$dtnow','$dtnow','$randUUID1', ARRAY [ 'all' ], '$randUUID2');" | psql -U $IMMICH_DATABASE_USER $IMMICH_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/immich/dbexport/setAPIKey.sh docker exec immich-db /dbexport/setAPIKey.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/immich/dbexport/setAPIKey.sh } function mfImmichV10Update() { buildOrPullImage $(getScriptImageByContainerName immich-mcp) if [ $? -ne 0 ]; then return 1 fi sudo sed -i "s/libopus/opus/" $HSHQ_STACKS_DIR/immich/config/immich.json cat < $HOME/immich.env TZ=\${PORTAINER_TZ} NO_COLOR=true IMMICH_CONFIG_FILE=/config/immich.json IMMICH_PROCESS_INVALID_IMAGES=true IMMICH_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 DB_HOSTNAME=immich-db DB_DATABASE_NAME=$IMMICH_DATABASE_NAME DB_USERNAME=$IMMICH_DATABASE_USER DB_PASSWORD=$IMMICH_DATABASE_USER_PASSWORD POSTGRES_PASSWORD=$IMMICH_DATABASE_USER_PASSWORD POSTGRES_USER=$IMMICH_DATABASE_USER POSTGRES_DB=$IMMICH_DATABASE_NAME POSTGRES_INITDB_ARGS=--data-checksums REDIS_HOSTNAME=immich-redis REDIS_PASSWORD=$IMMICH_REDIS_PASSWORD NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt IMMICH_BASE_URL=http://immich-app:2283 IMMICH_EXT_URL=https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN MCP_LOG_LEVEL=Information DOWNLOAD_MODE=url MAX_PAGE_SIZE=100 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false IMMICH_PUBLIC_URL=https://$SUB_IMMICH_APP.$HOMESERVER_DOMAIN EOFPI } # Homarr function installHomarr() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory homarr "$FMLNAME_HOMARR" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName homarr-app) if [ $? -ne 0 ]; then return 1 fi mkdir $HSHQ_STACKS_DIR/homarr mkdir $HSHQ_STACKS_DIR/homarr/data initServicesCredentials set +e addUserMailu alias $HOMARR_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS HOMARR_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $HOMARR_OIDC_CLIENT_SECRET | tr -d ':\n') HOMARR_NEXTAUTH_SECRET=$(pwgen -c -n 32 1) HOMARR_SECRET_ENCRYPTION_KEY=$(openssl rand -hex 32) outputConfigHomarr oidcBlock=$(cat $HOME/homarr.oidc) rm -f $HOME/homarr.oidc insertOIDCClientAuthelia homarr "$oidcBlock" set +e installStack homarr homarr-app "Ready to accept connections" $HOME/homarr.env 3 retval=$? if [ $retval -ne 0 ]; then return $retval fi rand_uid=$(pwgen -c -n -A 24 1) HOMARR_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $HOMARR_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2a/') HOMARR_PASSWORD_SALT_HASH=$(htpasswd -bnBC 10 "" $(pwgen -c -n 32 1) | tr -d ':\n' | sed 's/\$2y/\$2a/') rand_groupid=$(pwgen -c -n -A 24 1) everyone_gid=$(sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "select id from 'group' where name='everyone';") sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "insert into user(id,name,email,password,salt) values('$rand_uid', '$HOMARR_ADMIN_USERNAME','$HOMARR_ADMIN_EMAIL_ADDRESS','$HOMARR_ADMIN_PASSWORD_HASH','$HOMARR_PASSWORD_SALT_HASH');" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "insert into 'group'(id,name,owner_id,position) values('$rand_groupid','$LDAP_ADMIN_USER_GROUP_NAME','$rand_uid',1);" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "insert into groupMember(group_id,user_id) values('$everyone_gid','$rand_uid');" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "insert into groupMember(group_id,user_id) values('$rand_groupid','$rand_uid');" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "insert into groupPermission(group_id,permission) values('$rand_groupid','admin');" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "update search_engine set icon_url='https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/brave.svg', name='Brave', short='b', description='Search the web with Brave', url_template='https://search.brave.com/search?q=%s' where name='Google';" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "update serverSetting set value='{\"json\":{\"enableGeneral\":false,\"enableWidgetData\":false,\"enableIntegrationData\":false,\"enableUserData\":false}}' where setting_key='analytics';" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "update serverSetting set value='{\"json\":{\"defaultColorScheme\":\"dark\"}}' where setting_key='appearance';" searchEngID=$(sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "select id from 'search_engine' where name='Brave';") # Change the default search engine since the homarr docs search is broken sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "update serverSetting set value='{\"json\":{\"defaultSearchEngineId\":\"$searchEngID\"}}' where setting_key='search';" sudo sqlite3 $HSHQ_STACKS_DIR/homarr/data/db/db.sqlite "update onboarding set step='finish', previous_step='settings';" sleep 1 startStopStack homarr stop sleep 1 startStopStack homarr start set +e inner_block="" inner_block=$inner_block">>https://$SUB_HOMARR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://homarr-app:7575 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_HOMARR $MANAGETLS_HOMARR "$is_integrate_hshq" $NETDEFAULT_HOMARR "$inner_block" insertSubAuthelia $SUB_HOMARR.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll homarr "$FMLNAME_HOMARR" $USERTYPE_HOMARR "https://$SUB_HOMARR.$HOMESERVER_DOMAIN" "homarr.png" "$(getHeimdallOrderFromSub $SUB_HOMARR $USERTYPE_HOMARR)" restartAllCaddyContainers fi } function outputConfigHomarr() { cat < $HOME/homarr-compose.yml $STACK_VERSION_PREFIX homarr $(getScriptStackVersion homarr) services: homarr-app: image: $(getScriptImageByContainerName homarr-app) container_name: homarr-app hostname: homarr-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/homarr/data:/appdata networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFPI cat < $HOME/homarr.env TZ=\${PORTAINER_TZ} AUTH_TRUST_HOST=true NEXTAUTH_SECRET=$HOMARR_NEXTAUTH_SECRET SECRET_ENCRYPTION_KEY=$HOMARR_SECRET_ENCRYPTION_KEY NEXT_PUBLIC_DISABLE_ANALYTICS=true DEFAULT_COLOR_SCHEME=dark NEXTAUTH_URL=https://$SUB_HOMARR.$HOMESERVER_DOMAIN NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt NODE_TLS_REJECT_UNAUTHORIZED=0 AUTH_PROVIDERS=credentials,oidc AUTH_OIDC_ISSUER=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN AUTH_OIDC_CLIENT_ID=homarr AUTH_OIDC_CLIENT_SECRET=$HOMARR_OIDC_CLIENT_SECRET AUTH_OIDC_CLIENT_NAME=Authelia AUTH_OIDC_ADMIN_GROUP=${LDAP_ADMIN_USER_GROUP_NAME} AUTH_OIDC_OWNER_GROUP=${LDAP_PRIMARY_USER_GROUP_NAME} AUTH_OIDC_AUTO_LOGIN=false AUTH_OIDC_SCOPE_OVERWRITE=openid email profile groups AUTH_OIDC_GROUPS_ATTRIBUTE=groups AUTH_OIDC_FORCE_USERINFO=true AUTH_LOGOUT_REDIRECT_URL=https://$SUB_HOMARR.$HOMESERVER_DOMAIN EOFPI cat < $HOME/homarr.oidc # Authelia OIDC Client homarr BEGIN - client_id: homarr client_name: Homarr client_secret: $HOMARR_OIDC_CLIENT_SECRET_HASH public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth redirect_uris: - https://$SUB_HOMARR.$HOMESERVER_DOMAIN/api/auth/callback/oidc scopes: - openid - profile - groups - email userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client homarr END EOFIM } function performUpdateHomarr() { perform_stack_name=homarr prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=ghcr.io/ajnart/homarr:0.15.7 image_update_map[0]="ghcr.io/ajnart/homarr:0.15.7,ghcr.io/ajnart/homarr:0.15.10" ;; 2) newVer=v2 curImageList=ghcr.io/ajnart/homarr:0.15.10 image_update_map[0]="ghcr.io/ajnart/homarr:0.15.10,ghcr.io/ajnart/homarr:0.15.10" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of Homarr cannot be upgraded to the next version. You must export your data from this instance, uninstall and reinstall the homarr stack, then import your data into the new instance. See https://homarr.dev/blog/2025/01/19/migration-guide-1.0/ for migration instructions." return ;; 3) newVer=v5 curImageList=ghcr.io/homarr-labs/homarr:v1.18.1 image_update_map[0]="ghcr.io/homarr-labs/homarr:v1.18.1,ghcr.io/homarr-labs/homarr:v1.45.1" ;; 4) newVer=v5 curImageList=ghcr.io/homarr-labs/homarr:v1.32.0 image_update_map[0]="ghcr.io/homarr-labs/homarr:v1.32.0,ghcr.io/homarr-labs/homarr:v1.45.1" ;; 5) newVer=v5 curImageList=ghcr.io/homarr-labs/homarr:v1.45.1 image_update_map[0]="ghcr.io/homarr-labs/homarr:v1.45.1,ghcr.io/homarr-labs/homarr:v1.45.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Matomo function installMatomo() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory matomo "Matomo" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName matomo-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName matomo-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName matomo-web) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/matomo mkdir $HSHQ_STACKS_DIR/matomo/config mkdir $HSHQ_STACKS_DIR/matomo/db mkdir $HSHQ_STACKS_DIR/matomo/dbexport mkdir $HSHQ_STACKS_DIR/matomo/web chmod 777 $HSHQ_STACKS_DIR/matomo/dbexport initServicesCredentials set +e addUserMailu alias $MATOMO_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS MATOMO_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $MATOMO_ADMIN_PASSWORD | tr -d ':\n') outputConfigMatomo installStack matomo matomo-app "ready to handle connections" $HOME/matomo.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$MATOMO_INIT_ENV" = "true" ]; then sendEmail -s "Matomo Admin Login Info" -b "Matomo Admin Username: $MATOMO_ADMIN_USERNAME\nMatomo Admin Email: ${MATOMO_ADMIN_USERNAME}@${HOMESERVER_DOMAIN}\nMatomo Admin Password: $MATOMO_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" MATOMO_INIT_ENV=true updateConfigVar MATOMO_INIT_ENV $MATOMO_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase Matomo mysql matomo-db $MATOMO_DATABASE_NAME root $MATOMO_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/matomo/dbexport $MATOMO_DATABASE_READONLYUSER $MATOMO_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_MATOMO.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://matomo-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MATOMO $MANAGETLS_MATOMO "$is_integrate_hshq" $NETDEFAULT_MATOMO "$inner_block" insertSubAuthelia $SUB_MATOMO.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll matomo "$FMLNAME_MATOMO" $USERTYPE_MATOMO "https://$SUB_MATOMO.$HOMESERVER_DOMAIN" "matomo.png" "$(getHeimdallOrderFromSub $SUB_MATOMO $USERTYPE_MATOMO)" restartAllCaddyContainers checkAddDBConnection true matomo "$FMLNAME_MATOMO" mysql matomo-db $MATOMO_DATABASE_NAME $MATOMO_DATABASE_USER $MATOMO_DATABASE_USER_PASSWORD fi } function outputConfigMatomo() { cat < $HOME/matomo-compose.yml $STACK_VERSION_PREFIX matomo $(getScriptStackVersion matomo) services: matomo-db: image: $(getScriptImageByContainerName matomo-db) container_name: matomo-db hostname: matomo-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-matomo-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-matomo-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/matomo/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.matomo-hourly-db.schedule=@every 1h" - "ofelia.job-exec.matomo-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.matomo-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.matomo-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.matomo-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.matomo-hourly-db.email-from=Matomo Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.matomo-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.matomo-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.matomo-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.matomo-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.matomo-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.matomo-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.matomo-monthly-db.email-from=Matomo Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.matomo-monthly-db.mail-only-on-error=false" matomo-app: image: $(getScriptImageByContainerName matomo-app) container_name: matomo-app hostname: matomo-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - matomo-db networks: - int-matomo-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-matomo-web:/var/www/html:z matomo-web: image: $(getScriptImageByContainerName matomo-web) container_name: matomo-web hostname: matomo-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-matomo-net - dock-proxy-net depends_on: - matomo-app volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/matomo/config/nginx.conf:/etc/nginx/conf.d/default.conf:z,ro - v-matomo-web:/var/www/html:z,ro volumes: v-matomo-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/matomo/db v-matomo-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/matomo/web networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-matomo-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/matomo.env TZ=\${PORTAINER_TZ} MYSQL_DATABASE=$MATOMO_DATABASE_NAME MYSQL_ROOT_PASSWORD=$MATOMO_DATABASE_ROOT_PASSWORD MYSQL_USER=$MATOMO_DATABASE_USER MYSQL_PASSWORD=$MATOMO_DATABASE_USER_PASSWORD MATOMO_DATABASE_HOST=matomo-db MATOMO_DATABASE_ADAPTER=mysql MATOMO_DATABASE_TABLES_PREFIX=matomo_ MATOMO_DATABASE_USERNAME=$MATOMO_DATABASE_USER MATOMO_DATABASE_PASSWORD=$MATOMO_DATABASE_USER_PASSWORD MATOMO_DATABASE_DBNAME=$MATOMO_DATABASE_NAME EOFMT cat < $HSHQ_STACKS_DIR/matomo/config/nginx.conf upstream php-handler { server matomo-app:9000; } server { listen 80; add_header Referrer-Policy origin; # make sure outgoing links don't show the URL to the Matomo instance root /var/www/html; # replace with path to your matomo instance index index.php; try_files \$uri \$uri/ =404; ## only allow accessing the following php files location ~ ^/(index|matomo|piwik|js/index|plugins/HeatmapSessionRecording/configs).php { # regex to split \$uri to \$fastcgi_script_name and \$fastcgi_path fastcgi_split_path_info ^(.+\.php)(/.+)\$; # Check that the PHP script exists before passing it try_files \$fastcgi_script_name =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; fastcgi_param PATH_INFO \$fastcgi_path_info; fastcgi_param HTTP_PROXY ""; # prohibit httpoxy: https://httpoxy.org/ fastcgi_pass php-handler; } ## deny access to all other .php files location ~* ^.+\.php\$ { deny all; return 403; } ## disable all access to the following directories location ~ /(config|tmp|core|lang) { deny all; return 403; # replace with 404 to not show these directories exist } location ~ /\.ht { deny all; return 403; } location ~ js/container_.*_preview\.js\$ { expires off; add_header Cache-Control 'private, no-cache, no-store'; } location ~ \.(gif|ico|jpg|png|svg|js|css|htm|html|mp3|mp4|wav|ogg|avi|ttf|eot|woff|woff2|json)\$ { allow all; ## Cache images,CSS,JS and webfonts for an hour ## Increasing the duration may improve the load-time, but may cause old files to show after an Matomo upgrade expires 1h; add_header Pragma public; add_header Cache-Control "public"; } location ~ /(libs|vendor|plugins|misc/user) { deny all; return 403; } ## properly display textfiles in root directory location ~/(.*\.md|LEGALNOTICE|LICENSE) { default_type text/plain; } } EOFMT } function performUpdateMatomo() { perform_stack_name=matomo prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=mariadb:10.7.3,matomo:5.3.1-fpm-alpine,nginx:1.27.4-alpine image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="matomo:5.3.1-fpm-alpine,mirror.gcr.io/matomo:5.6.1-fpm-alpine" image_update_map[2]="nginx:1.27.4-alpine,mirror.gcr.io/nginx:1.29.3-alpine" ;; 2) newVer=v3 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/matomo:5.3.2-fpm-alpine,mirror.gcr.io/nginx:1.28.0-alpine image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/matomo:5.3.2-fpm-alpine,mirror.gcr.io/matomo:5.6.1-fpm-alpine" image_update_map[2]="mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/nginx:1.29.3-alpine" ;; 3) newVer=v3 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/matomo:5.6.1-fpm-alpine,mirror.gcr.io/nginx:1.29.3-alpine image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/matomo:5.6.1-fpm-alpine,mirror.gcr.io/matomo:5.6.1-fpm-alpine" image_update_map[2]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Pastefy function installPastefy() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory pastefy "Pastefy" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName pastefy-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName pastefy-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/pastefy mkdir $HSHQ_STACKS_DIR/pastefy/db mkdir $HSHQ_STACKS_DIR/pastefy/dbexport chmod 777 $HSHQ_STACKS_DIR/pastefy/dbexport initServicesCredentials set +e addUserMailu alias $PASTEFY_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS PASTEFY_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $PASTEFY_ADMIN_PASSWORD | tr -d ':\n') outputConfigPastefy installStack pastefy pastefy-app "HTTP-Server started on port 80" $HOME/pastefy.env 3 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$PASTEFY_INIT_ENV" = "true" ]; then #sendEmail -s "Pastefy Admin Login Info" -b "Pastefy Admin Username: $PASTEFY_ADMIN_USERNAME\nPastefy Admin Password: $PASTEFY_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" PASTEFY_INIT_ENV=true updateConfigVar PASTEFY_INIT_ENV $PASTEFY_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase Pastefy mysql pastefy-db $PASTEFY_DATABASE_NAME root $PASTEFY_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/pastefy/dbexport $PASTEFY_DATABASE_READONLYUSER $PASTEFY_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_PASTEFY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://pastefy-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PASTEFY $MANAGETLS_PASTEFY "$is_integrate_hshq" $NETDEFAULT_PASTEFY "$inner_block" insertSubAuthelia $SUB_PASTEFY.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll pastefy "$FMLNAME_PASTEFY" $USERTYPE_PASTEFY "https://$SUB_PASTEFY.$HOMESERVER_DOMAIN" "pastefy.png" "$(getHeimdallOrderFromSub $SUB_PASTEFY $USERTYPE_PASTEFY)" restartAllCaddyContainers checkAddDBConnection true pastefy "$FMLNAME_PASTEFY" mysql pastefy-db $PASTEFY_DATABASE_NAME $PASTEFY_DATABASE_USER $PASTEFY_DATABASE_USER_PASSWORD fi } function outputConfigPastefy() { cat < $HOME/pastefy-compose.yml $STACK_VERSION_PREFIX pastefy $(getScriptStackVersion pastefy) services: pastefy-db: image: $(getScriptImageByContainerName pastefy-db) container_name: pastefy-db hostname: pastefy-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-pastefy-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-pastefy-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/pastefy/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.pastefy-hourly-db.schedule=@every 1h" - "ofelia.job-exec.pastefy-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.pastefy-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.pastefy-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.pastefy-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.pastefy-hourly-db.email-from=Pastefy Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.pastefy-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.pastefy-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.pastefy-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.pastefy-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.pastefy-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.pastefy-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.pastefy-monthly-db.email-from=Pastefy Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.pastefy-monthly-db.mail-only-on-error=false" pastefy-app: image: $(getScriptImageByContainerName pastefy-app) container_name: pastefy-app hostname: pastefy-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - pastefy-db networks: - int-pastefy-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-pastefy-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/pastefy/db networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true int-pastefy-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/pastefy.env TZ=\${PORTAINER_TZ} MYSQL_DATABASE=$PASTEFY_DATABASE_NAME MYSQL_ROOT_PASSWORD=$PASTEFY_DATABASE_ROOT_PASSWORD MYSQL_USER=$PASTEFY_DATABASE_USER MYSQL_PASSWORD=$PASTEFY_DATABASE_USER_PASSWORD DATABASE_HOST=pastefy-db DATABASE_DRIVER=mysql DATABASE_NAME=$PASTEFY_DATABASE_NAME DATABASE_USER=$PASTEFY_DATABASE_USER DATABASE_PASSWORD=$PASTEFY_DATABASE_USER_PASSWORD SERVER_NAME=https://$SUB_PASTEFY.$HOMESERVER_DOMAIN PASTEFY_INFO_CUSTOM_NAME=$HOMESERVER_NAME Pastefy PASTEFY_LOGIN_REQUIRED=false PASTEFY_LOGIN_REQUIRED_CREATE=false PASTEFY_LOGIN_REQUIRED_READ=false PASTEFY_ENCRYPTION_DEFAULT=false PASTEFY_GRANT_ACCESS_REQUIRED=false PASTEFY_LIST_PASTES=true PASTEFY_PUBLIC_STATS=true PASTEFY_PUBLIC_PASTES=true EOFMT } function performUpdatePastefy() { perform_stack_name=pastefy prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=mariadb:10.7.3,interaapps/pastefy:7.0.3 image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="interaapps/pastefy:7.0.3,mirror.gcr.io/interaapps/pastefy:7.1.5" ;; 2) newVer=v3 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/interaapps/pastefy:7.1.4 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/interaapps/pastefy:7.1.4,mirror.gcr.io/interaapps/pastefy:7.1.5" ;; 3) newVer=v3 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/interaapps/pastefy:7.1.5 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/interaapps/pastefy:7.1.5,mirror.gcr.io/interaapps/pastefy:7.1.5" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # SnippetBox function installSnippetBox() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory snippetbox "SnippetBox" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName snippetbox) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/snippetbox mkdir $HSHQ_STACKS_DIR/snippetbox/data set +e outputConfigSnippetBox installStack snippetbox snippetbox "Server is working on port" $HOME/snippetbox.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 3 set -e inner_block="" inner_block=$inner_block">>https://$SUB_SNIPPETBOX.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://snippetbox:5000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SNIPPETBOX $MANAGETLS_SNIPPETBOX "$is_integrate_hshq" $NETDEFAULT_SNIPPETBOX "$inner_block" insertSubAuthelia $SUB_SNIPPETBOX.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll snippetbox "$FMLNAME_SNIPPETBOX" $USERTYPE_SNIPPETBOX "https://$SUB_SNIPPETBOX.$HOMESERVER_DOMAIN" "snippetbox.png" "$(getHeimdallOrderFromSub $SUB_SNIPPETBOX $USERTYPE_SNIPPETBOX)" restartAllCaddyContainers fi } function outputConfigSnippetBox() { cat < $HOME/snippetbox-compose.yml $STACK_VERSION_PREFIX snippetbox $(getScriptStackVersion snippetbox) services: snippetbox: image: $(getScriptImageByContainerName snippetbox) container_name: snippetbox hostname: snippetbox restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/snippetbox/data:/app/data networks: dock-proxy-net: name: dock-proxy external: true EOFMT cat < $HOME/snippetbox.env TZ=\${PORTAINER_TZ} EOFMT } function performUpdateSnippetBox() { perform_stack_name=snippetbox prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=pawelmalak/snippet-box:latest image_update_map[0]="pawelmalak/snippet-box:latest,pawelmalak/snippet-box:latest" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # AIStack (Deprecated) function installAIStack() { set +e if true; then echo "ERROR: This stack is deprecated, returning..." return 1 fi is_integrate_hshq=$1 checkDeleteStackAndDirectory aistack "AIStack" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName aistack-mindsdb-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName aistack-mindsdb-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName aistack-otel) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName aistack-langfuse) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName aistack-redis) if [ $? -ne 0 ]; then return 1 fi buildImageMindsDBV1 if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/aistack mkdir $HSHQ_STACKS_DIR/aistack/mindsdb mkdir $HSHQ_STACKS_DIR/aistack/mindsdb/app mkdir $HSHQ_STACKS_DIR/aistack/mindsdb/db mkdir $HSHQ_STACKS_DIR/aistack/mindsdb/dbexport mkdir $HSHQ_STACKS_DIR/aistack/mindsdb/dbimport mkdir $HSHQ_STACKS_DIR/aistack/otel chmod 777 $HSHQ_STACKS_DIR/aistack/mindsdb/dbexport initServicesCredentials set +e addUserMailu alias $AISTACK_MINDSDB_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS addUserMailu alias $AISTACK_LANGFUSE_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigAIStack cd ~ docker compose -f $HOME/aistack-compose-tmp.yml up -d echo "Waiting at least 5 seconds before continuing..." sleep 5 search="mindsdb: http API: started on 47334" isFound=false i=0 set +e while [ $i -le 300 ] do findtext=$(docker logs aistack-mindsdb-app 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "Container not ready, sleeping 10 seconds, total wait=$i seconds..." sleep 10 i=$((i+10)) done if ! [ "$isFound" = "true" ]; then echo "MindsDB did not start up correctly..." cd ~ docker compose -f $HOME/aistack-compose-tmp.yml down -v rm -f $HOME/aistack-compose-tmp.yml rm -f $HOME/aistack.env rm -f $HOME/aistack-compose.yml return 1 fi sleep 5 # See the code at the end of this function, as it informs the user regarding the import #importDBConnectionsAIStack docker exec aistack-mindsdb-db bash /dbimport/insertAdmin.sh rm -f $HSHQ_STACKS_DIR/aistack/mindsdb/dbimport/insertAdmin.sh sleep 5 cd ~ docker compose -f $HOME/aistack-compose-tmp.yml down -v rm -f $HOME/aistack-compose-tmp.yml rm -f $HSHQ_STACKS_DIR/aistack/mindsdb/dbexport/init-dbs.sh installStack aistack aistack-mindsdb-app "mindsdb: http API: started on 47334" $HOME/aistack.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$AISTACK_INIT_ENV" = "true" ]; then sendEmail -s "AIStack Login Info" -b "MindsDB Admin Username: $AISTACK_MINDSDB_ADMIN_USERNAME\nMindsDB Admin Password: $AISTACK_MINDSDB_ADMIN_PASSWORD\nLangfuse Admin Username: $AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS\nLangfuse Admin Password: $AISTACK_LANGFUSE_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" AISTACK_INIT_ENV=true updateConfigVar AISTACK_INIT_ENV $AISTACK_INIT_ENV fi sleep 3 set -e inner_block="" inner_block=$inner_block">>https://$SUB_AISTACK_MINDSDB_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://aistack-mindsdb-app:47334 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AISTACK_MINDSDB_APP $MANAGETLS_AISTACK_MINDSDB_APP "$is_integrate_hshq" $NETDEFAULT_AISTACK_MINDSDB_APP "$inner_block" insertSubAuthelia $SUB_AISTACK_MINDSDB_APP.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_AISTACK_LANGFUSE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://aistack-langfuse:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AISTACK_LANGFUSE $MANAGETLS_AISTACK_LANGFUSE "$is_integrate_hshq" $NETDEFAULT_AISTACK_LANGFUSE "$inner_block" insertSubAuthelia $SUB_AISTACK_LANGFUSE.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll aistack "$FMLNAME_AISTACK_MINDSDB_APP" $USERTYPE_AISTACK_MINDSDB_APP "https://$SUB_AISTACK_MINDSDB_APP.$HOMESERVER_DOMAIN" "mindsdb.png" "$(getHeimdallOrderFromSub $SUB_AISTACK_MINDSDB_APP $USERTYPE_AISTACK_MINDSDB_APP)" insertEnableSvcAll aistack "$FMLNAME_AISTACK_LANGFUSE" $USERTYPE_AISTACK_LANGFUSE "https://$SUB_AISTACK_LANGFUSE.$HOMESERVER_DOMAIN" "langfuse.png" "$(getHeimdallOrderFromSub $SUB_AISTACK_LANGFUSE $USERTYPE_AISTACK_LANGFUSE)" restartAllCaddyContainers checkAddDBConnection false aistack-mindsdb "$FMLNAME_AISTACK_MINDSDB_APP" postgres aistack-mindsdb-db $AISTACK_MINDSDB_DATABASE_NAME $AISTACK_MINDSDB_DATABASE_USER $AISTACK_MINDSDB_DATABASE_USER_PASSWORD checkAddDBConnection false aistack-langfuse "$FMLNAME_AISTACK_LANGFUSE" postgres aistack-mindsdb-db $AISTACK_LANGFUSE_DATABASE_NAME $AISTACK_MINDSDB_DATABASE_USER $AISTACK_MINDSDB_DATABASE_USER_PASSWORD fi } function outputConfigAIStack() { AISTACK_LANGFUSE_PUBKEY=$(pwgen -c -n 32 1) AISTACK_LANGFUSE_SECRETKEY=$(pwgen -c -n 32 1) cat < $HOME/aistack-compose-tmp.yml $STACK_VERSION_PREFIX aistack $(getScriptStackVersion aistack) services: aistack-mindsdb-db: image: $(getScriptImageByContainerName aistack-mindsdb-db) container_name: aistack-mindsdb-db hostname: aistack-mindsdb-db user: "${USERID}:${GROUPID}" restart: unless-stopped env_file: aistack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-aistack-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - ${HSHQ_STACKS_DIR}/aistack/mindsdb/db:/var/lib/postgresql/data - ${HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - ${HSHQ_STACKS_DIR}/aistack/mindsdb/dbexport:/dbexport - ${HSHQ_STACKS_DIR}/aistack/mindsdb/dbimport:/dbimport - ${HSHQ_STACKS_DIR}/aistack/mindsdb/dbexport/init-dbs.sh:/docker-entrypoint-initdb.d/init-dbs.sh aistack-mindsdb-app: image: $(getScriptImageByContainerName aistack-mindsdb-mod-app) container_name: aistack-mindsdb-app hostname: aistack-mindsdb-app restart: unless-stopped env_file: aistack.env security_opt: - no-new-privileges:true depends_on: - aistack-mindsdb-db networks: - int-aistack-net - dock-ext-net - dock-internalmail-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-aistack-mindsdb-app:/mindsdb aistack-otel: image: $(getScriptImageByContainerName aistack-otel) container_name: aistack-otel hostname: aistack-otel restart: unless-stopped env_file: aistack.env security_opt: - no-new-privileges:true networks: - int-aistack-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ${HSHQ_STACKS_DIR}/aistack/otel/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml aistack-langfuse: image: $(getScriptImageByContainerName aistack-langfuse) container_name: aistack-langfuse hostname: aistack-langfuse restart: unless-stopped env_file: aistack.env security_opt: - no-new-privileges:true depends_on: - aistack-mindsdb-db networks: - int-aistack-net - dock-ext-net environment: - HOSTNAME=0.0.0.0 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro aistack-redis: image: $(getScriptImageByContainerName aistack-redis) container_name: aistack-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $AISTACK_REDIS_PASSWORD --appendonly no networks: - int-aistack-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-aistack-mindsdb-app: driver: local driver_opts: type: none o: bind device: ${HSHQ_STACKS_DIR}/aistack/mindsdb/app networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-aistack-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HSHQ_STACKS_DIR/aistack/mindsdb/dbexport/init-dbs.sh #!/bin/bash set -e # Create additional databases psql -v ON_ERROR_STOP=1 --username "$AISTACK_MINDSDB_DATABASE_USER" --dbname "$AISTACK_MINDSDB_DATABASE_NAME" <<-EOSQL CREATE DATABASE langfuse; EOSQL EOFDB chmod 755 $HSHQ_STACKS_DIR/aistack/mindsdb/dbexport/init-dbs.sh cat < $HOME/aistack-compose.yml $STACK_VERSION_PREFIX aistack $(getScriptStackVersion aistack) services: aistack-mindsdb-db: image: $(getScriptImageByContainerName aistack-mindsdb-db) container_name: aistack-mindsdb-db hostname: aistack-mindsdb-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-aistack-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/aistack/mindsdb/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/aistack/mindsdb/dbexport:/dbexport - \${PORTAINER_HSHQ_STACKS_DIR}/aistack/mindsdb/dbimport:/dbimport labels: - "ofelia.enabled=true" - "ofelia.job-exec.aistack-mindsdb-hourly-db.schedule=@every 1h" - "ofelia.job-exec.aistack-mindsdb-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.aistack-mindsdb-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.aistack-mindsdb-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.aistack-mindsdb-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.aistack-mindsdb-hourly-db.email-from=AIStack MindsDB Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.aistack-mindsdb-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.aistack-mindsdb-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.aistack-mindsdb-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.aistack-mindsdb-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.aistack-mindsdb-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.aistack-mindsdb-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.aistack-mindsdb-monthly-db.email-from=AIStack MindsDB Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.aistack-mindsdb-monthly-db.mail-only-on-error=false" aistack-mindsdb-app: image: $(getScriptImageByContainerName aistack-mindsdb-mod-app) container_name: aistack-mindsdb-app hostname: aistack-mindsdb-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - aistack-mindsdb-db networks: - int-aistack-net - dock-ext-net - dock-internalmail-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-aistack-mindsdb-app:/mindsdb aistack-otel: image: $(getScriptImageByContainerName aistack-otel) container_name: aistack-otel hostname: aistack-otel restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-aistack-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/aistack/otel/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml aistack-langfuse: image: $(getScriptImageByContainerName aistack-langfuse) container_name: aistack-langfuse hostname: aistack-langfuse restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - aistack-mindsdb-db networks: - int-aistack-net - dock-ext-net environment: - HOSTNAME=0.0.0.0 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro aistack-redis: image: $(getScriptImageByContainerName aistack-redis) container_name: aistack-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $AISTACK_REDIS_PASSWORD --appendonly no networks: - int-aistack-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-aistack-mindsdb-app: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/aistack/mindsdb/app networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-aistack-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/aistack.env TZ=${TZ} MINDSDB_DB_CON=postgresql://$AISTACK_MINDSDB_DATABASE_USER:$AISTACK_MINDSDB_DATABASE_USER_PASSWORD@aistack-mindsdb-db:5432/$AISTACK_MINDSDB_DATABASE_NAME MINDSDB_DOCKER_ENV=True MINDSDB_STORAGE_DIR=/mindsdb/var MINDSDB_USERNAME=$AISTACK_MINDSDB_ADMIN_USERNAME MINDSDB_PASSWORD=$AISTACK_MINDSDB_ADMIN_PASSWORD SENTRY_IO_DSN= SENTRY_IO_ENVIRONMENT=local LANGFUSE_HOST=http://aistack-langfuse:3000 LANGFUSE_PUBLIC_KEY=$AISTACK_LANGFUSE_PUBKEY LANGFUSE_SECRET_KEY=$AISTACK_LANGFUSE_SECRETKEY LANGFUSE_ENVIRONMENT=local LANGFUSE_RELEASE=local LANGFUSE_TIMEOUT=10 LANGFUSE_SAMPLE_RATE=1.0 OTEL_EXPORTER_TYPE=otlp OTEL_EXPORTER_PROTOCOL=grpc OTEL_OTLP_ENDPOINT=http://aistack-otel:4317 OTEL_SERVICE_NAME=aistack OTEL_SERVICE_INSTANCE_ID=aistack-instance OTEL_SERVICE_ENVIRONMENT=local OTEL_SERVICE_RELEASE=local OTEL_TRACE_SAMPLE_RATE=1.0 OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true DATABASE_URL=postgresql://$AISTACK_MINDSDB_DATABASE_USER:$AISTACK_MINDSDB_DATABASE_USER_PASSWORD@aistack-mindsdb-db:5432/$AISTACK_LANGFUSE_DATABASE_NAME NEXTAUTH_SECRET=$(openssl rand -base64 32) SALT=$(openssl rand -base64 32) ENCRYPTION_KEY=$(openssl rand -hex 32) NEXTAUTH_URL=https://$SUB_AISTACK_LANGFUSE.$HOMESERVER_DOMAIN LANGFUSE_INIT_ORG_ID=aistack LANGFUSE_INIT_ORG_NAME=AIStack LANGFUSE_INIT_PROJECT_ID=$(uuidgen) LANGFUSE_INIT_PROJECT_NAME=AIStack LANGFUSE_INIT_PROJECT_PUBLIC_KEY=$AISTACK_LANGFUSE_PUBKEY LANGFUSE_INIT_PROJECT_SECRET_KEY=$AISTACK_LANGFUSE_SECRETKEY LANGFUSE_INIT_USER_EMAIL=$AISTACK_LANGFUSE_ADMIN_EMAIL_ADDRESS LANGFUSE_INIT_USER_NAME=$AISTACK_LANGFUSE_ADMIN_USERNAME LANGFUSE_INIT_USER_PASSWORD=$AISTACK_LANGFUSE_ADMIN_PASSWORD POSTGRES_USER=$AISTACK_MINDSDB_DATABASE_USER POSTGRES_PASSWORD=$AISTACK_MINDSDB_DATABASE_USER_PASSWORD POSTGRES_DB=$AISTACK_MINDSDB_DATABASE_NAME EOFMT cat < $HSHQ_STACKS_DIR/aistack/otel/otel-collector-config.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 exporters: debug: verbosity: normal processors: batch: extensions: health_check: pprof: zpages: service: extensions: [health_check, pprof, zpages] telemetry: logs: level: "debug" encoding: "console" metrics: level: "detailed" pipelines: logs: receivers: [otlp] processors: [batch] exporters: [debug] metrics: receivers: [otlp] processors: [batch] exporters: [debug] traces: receivers: [otlp] processors: [batch] exporters: [debug] EOFOT } function importDBConnectionsAIStack() { # Let's not automatically add DB connections to the MindsDB as it may pose a security risk # See https://github.com/mindsdb/mindsdb/issues/10803 and https://cybernews.com/security/aws-cloud-storage-bucket-ransomware-attacks/ rm -f $HSHQ_STACKS_DIR/aistack/mindsdb/dbimport/$ALLDBS_IMPORT_FILENAME if true; then return; fi # This function assumes the file, ~/hshq/data/stacks/aistack/mindsdb/dbimport/DBCImport.txt # exists and has been populated with the connection info # that will be imported into the MindsDB database. set +e if ! [ -f $HSHQ_STACKS_DIR/aistack/mindsdb/dbimport/$ALLDBS_IMPORT_FILENAME ]; then echo "$ALLDBS_IMPORT_FILENAME does not exist, returning..." return fi docker ps | grep aistack-mindsdb-db > /dev/null 2>&1 if [ $? -eq 0 ]; then docker exec aistack-mindsdb-db bash /dbimport/importConnections.sh else echo "aistack-mindsdb-db container does not exist, returning..." fi rm -f $HSHQ_STACKS_DIR/aistack/mindsdb/dbimport/$ALLDBS_IMPORT_FILENAME } function buildImageMindsDBV1() { # Capture the return value deliberately, just so we don't forget down the road buildMindsDBImage "mindsdb/mindsdb:v25.4.5.0" "hshq/mindsdb:v1" rtval=$? return $rtval } function buildImageMindsDBV2() { # Capture the return value deliberately, just so we don't forget down the road buildMindsDBImage "mirror.gcr.io/mindsdb/mindsdb:v25.7.4.0" "hshq/mindsdb:v2" rtval=$? return $rtval } function buildMindsDBImage() { baseBuildImage="$1" newBuildImage="$2" set +e pullImage $baseBuildImage if [ $? -ne 0 ]; then return 1 fi is_build_error=0 if [ $is_build_error -eq 0 ]; then docker run --name mindsdb_build -e MINDSDB_APIS=http -d $baseBuildImage rtval=$? is_build_error=$(($is_build_error+$rtval)) fi if [ $is_build_error -eq 0 ]; then # Wait for string search="mindsdb: http API: started on 47334" isFound=false i=0 while [ $i -le 300 ] do findtext=$(docker logs mindsdb_build 2>&1 | grep "$search") if ! [ -z "$findtext" ]; then isFound=true break fi echo "MindsDB build container not ready, sleeping 5 seconds, total wait=$i seconds..." sleep 5 i=$((i+5)) done if ! [ "$isFound" = "true" ]; then echo "MindsDB build container did not start up correctly..." is_build_error=1 fi fi if [ $is_build_error -eq 0 ]; then echo "Installing polars-lts-cpu, please be patient, this might take a few minutes..." docker exec mindsdb_build pip install polars-lts-cpu rtval=$? is_build_error=$(($is_build_error+$rtval)) sleep 5 fi if [ $is_build_error -eq 0 ]; then docker image rm $newBuildImage > /dev/null 2>&1 docker commit mindsdb_build $newBuildImage rtval=$? is_build_error=$(($is_build_error+$rtval)) fi docker container stop mindsdb_build > /dev/null 2>&1 docker container rm mindsdb_build > /dev/null 2>&1 return $is_build_error } function performUpdateAIStack() { perform_stack_name=aistack prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) # There's a bug when upgrading MindsDB: # ModuleNotFoundError: No module named 'langfuse' newVer=v2 curImageList=postgres:15.0-bullseye,hshq/mindsdb:v1,otel/opentelemetry-collector-contrib:0.116.1,langfuse/langfuse:2.87.0,ollama/ollama:0.6.8,ghcr.io/open-webui/open-webui:git-aa37482,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="hshq/mindsdb:v1,hshq/mindsdb:v2" image_update_map[2]="otel/opentelemetry-collector-contrib:0.116.1,mirror.gcr.io/otel/opentelemetry-collector-contrib:0.131.1" image_update_map[3]="langfuse/langfuse:2.87.0,mirror.gcr.io/langfuse/langfuse:2.87.0" image_update_map[4]="ollama/ollama:0.6.8,mirror.gcr.io/ollama/ollama:0.11.4" image_update_map[5]="ghcr.io/open-webui/open-webui:git-aa37482,ghcr.io/open-webui/open-webui:0.6.22" image_update_map[6]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" buildImageMindsDBV2 if [ $? -ne 0 ]; then perform_update_report="ERROR ($perform_stack_name): Could not build new MindsDB image" return fi upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfAIStackFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:15.0-bullseye,hshq/mindsdb:v2,mirror.gcr.io/otel/opentelemetry-collector-contrib:0.131.1,mirror.gcr.io/langfuse/langfuse:2.87.0,mirror.gcr.io/ollama/ollama:0.11.4,ghcr.io/open-webui/open-webui:0.6.22,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="hshq/mindsdb:v2,hshq/mindsdb:v2" image_update_map[2]="mirror.gcr.io/otel/opentelemetry-collector-contrib:0.131.1,mirror.gcr.io/otel/opentelemetry-collector-contrib:0.131.1" image_update_map[3]="mirror.gcr.io/langfuse/langfuse:2.87.0,mirror.gcr.io/langfuse/langfuse:2.87.0" image_update_map[4]="mirror.gcr.io/ollama/ollama:0.11.4,mirror.gcr.io/ollama/ollama:0.11.4" image_update_map[5]="ghcr.io/open-webui/open-webui:0.6.22,ghcr.io/open-webui/open-webui:0.6.22" image_update_map[6]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfAIStackFixRedisCompose() { replaceRedisBlock aistack aistack-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/aistack/redis/* } # Pixelfed function installPixelfed() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory pixelfed "Pixelfed" cdRes=$? if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName pixelfed-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName pixelfed-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName pixelfed-app) if [ $? -ne 0 ]; then buildOrPullImage hshq/pixelfed:v1 fi if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/pixelfed mkdir $HSHQ_STACKS_DIR/pixelfed/config mkdir $HSHQ_STACKS_DIR/pixelfed/db mkdir $HSHQ_STACKS_DIR/pixelfed/dbexport mkdir $HSHQ_STACKS_DIR/pixelfed/appstorage mkdir $HSHQ_STACKS_DIR/pixelfed/appbootstrapcache chmod 777 $HSHQ_STACKS_DIR/pixelfed/dbexport initServicesCredentials set +e generateCert pixelfed-app pixelfed-app set +e outputConfigPixelfed installStack pixelfed pixelfed-app "ready to handle connections" $HOME/pixelfed.env 10 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$PIXELFED_INIT_ENV" = "true" ]; then sendEmail -s "Pixelfed Admin Login Info" -b "Pixelfed Admin Username: $EMAIL_ADMIN_EMAIL_ADDRESS\nPixelfed Admin Password: $LDAP_ADMIN_USER_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" PIXELFED_INIT_ENV=true updateConfigVar PIXELFED_INIT_ENV $PIXELFED_INIT_ENV fi set +e sleep 3 docker exec pixelfed-app bash -c "php artisan ldap:import users --filter \"(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)\" -n -q" >/dev/null docker exec pixelfed-app bash -c "yes | php artisan user:admin $LDAP_ADMIN_USER_USERNAME" >/dev/null addReadOnlyUserToDatabase Pixelfed mysql pixelfed-db $PIXELFED_DATABASE_NAME root $PIXELFED_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/pixelfed/dbexport $PIXELFED_DATABASE_READONLYUSER $PIXELFED_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_PIXELFED.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://pixelfed-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PIXELFED $MANAGETLS_PIXELFED "$is_integrate_hshq" $NETDEFAULT_PIXELFED "$inner_block" insertSubAuthelia $SUB_PIXELFED.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll pixelfed "$FMLNAME_PIXELFED" $USERTYPE_PIXELFED "https://$SUB_PIXELFED.$HOMESERVER_DOMAIN" "pixelfed.png" "$(getHeimdallOrderFromSub $SUB_PIXELFED $USERTYPE_PIXELFED)" restartAllCaddyContainers checkAddDBConnection true pixelfed "$FMLNAME_PIXELFED" mysql pixelfed-db $PIXELFED_DATABASE_NAME $PIXELFED_DATABASE_USER $PIXELFED_DATABASE_USER_PASSWORD fi } function outputConfigPixelfed() { cat < $HOME/pixelfed-compose.yml $STACK_VERSION_PREFIX pixelfed $(getScriptStackVersion pixelfed) services: pixelfed-db: image: $(getScriptImageByContainerName pixelfed-db) container_name: pixelfed-db hostname: pixelfed-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --default-authentication-plugin=mysql_native_password --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-pixelfed-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-pixelfed-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.pixelfed-hourly-db.schedule=@every 1h" - "ofelia.job-exec.pixelfed-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.pixelfed-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.pixelfed-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.pixelfed-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.pixelfed-hourly-db.email-from=Pixelfed Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.pixelfed-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.pixelfed-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.pixelfed-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.pixelfed-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.pixelfed-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.pixelfed-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.pixelfed-monthly-db.email-from=Pixelfed Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.pixelfed-monthly-db.mail-only-on-error=false" pixelfed-app: image: $(getScriptImageByContainerName pixelfed-app) container_name: pixelfed-app hostname: pixelfed-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - pixelfed-db - pixelfed-redis networks: - int-pixelfed-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/ldap.conf:/etc/ldap/ldap.conf - \${PORTAINER_HSHQ_SSL_DIR}/pixelfed-app.crt:/ldapcerts/pixelfed-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/pixelfed-app.key:/ldapcerts/pixelfed-app.key - v-pixelfed-appstorage:/var/www/storage - v-pixelfed-appbootstrapcache:/var/www/bootstrap/cache - \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/docker.env:/var/www/.env pixelfed-worker: image: $(getScriptImageByContainerName pixelfed-worker) container_name: pixelfed-worker hostname: pixelfed-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: gosu www-data php artisan horizon depends_on: - pixelfed-app - pixelfed-db - pixelfed-redis networks: - int-pixelfed-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/ldap.conf:/etc/ldap/ldap.conf - \${PORTAINER_HSHQ_SSL_DIR}/pixelfed-app.crt:/ldapcerts/pixelfed-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/pixelfed-app.key:/ldapcerts/pixelfed-app.key - v-pixelfed-appstorage:/var/www/storage - v-pixelfed-appbootstrapcache:/var/www/bootstrap/cache - \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/docker.env:/var/www/.env pixelfed-redis: image: $(getScriptImageByContainerName pixelfed-redis) container_name: pixelfed-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $PIXELFED_REDIS_PASSWORD --appendonly no networks: - int-pixelfed-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-pixelfed-appstorage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/appstorage v-pixelfed-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/db v-pixelfed-appbootstrapcache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/pixelfed/appbootstrapcache networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ldap-net: name: dock-ldap external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-pixelfed-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/pixelfed.env TZ=\${PORTAINER_TZ} UID=${USERID} GID=${GROUPID} MYSQL_DATABASE=$PIXELFED_DATABASE_NAME MYSQL_ROOT_PASSWORD=$PIXELFED_DATABASE_ROOT_PASSWORD MYSQL_USER=$PIXELFED_DATABASE_USER MYSQL_PASSWORD=$PIXELFED_DATABASE_USER_PASSWORD REDIS_PASSWORD=$PIXELFED_REDIS_PASSWORD EOFMT cat < $HSHQ_STACKS_DIR/pixelfed/docker.env APP_NAME="Pixelfed" APP_ENV="production" APP_KEY= APP_DEBUG="false" OPEN_REGISTRATION="false" ENFORCE_EMAIL_VERIFICATION="false" PF_MAX_USERS="1000" OAUTH_ENABLED="true" ENABLE_CONFIG_CACHE=true INSTANCE_DISCOVER_PUBLIC="true" PF_OPTIMIZE_IMAGES="true" IMAGE_QUALITY="80" MAX_PHOTO_SIZE="15000" MAX_CAPTION_LENGTH="500" MAX_ALBUM_LENGTH="4" APP_URL="https://$SUB_PIXELFED.$HOMESERVER_DOMAIN" APP_DOMAIN="$SUB_PIXELFED.$HOMESERVER_DOMAIN" ADMIN_DOMAIN="$SUB_PIXELFED.$HOMESERVER_DOMAIN" SESSION_DOMAIN="$SUB_PIXELFED.$HOMESERVER_DOMAIN" TRUST_PROXIES="*" DB_CONNECTION="mysql" DB_HOST="pixelfed-db" DB_PORT="3306" DOCKER_DB_HOST_PORT="3306" DB_DATABASE="$PIXELFED_DATABASE_NAME" DB_USERNAME="$PIXELFED_DATABASE_USER" DB_PASSWORD="$PIXELFED_DATABASE_USER_PASSWORD" REDIS_CLIENT="predis" REDIS_SCHEME="tcp" REDIS_HOST="pixelfed-redis" REDIS_PASSWORD="$PIXELFED_REDIS_PASSWORD" REDIS_PORT="6379" SESSION_DRIVER="database" CACHE_DRIVER="redis" QUEUE_DRIVER="redis" BROADCAST_DRIVER="log" LOG_CHANNEL="stack" HORIZON_PREFIX="horizon-" ACTIVITY_PUB="true" AP_REMOTE_FOLLOW="true" AP_INBOX="false" AP_OUTBOX="false" AP_SHAREDINBOX="false" EXP_EMC="true" MAIL_DRIVER=smtp MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_USERNAME= MAIL_PASSWORD= MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS="$EMAIL_ADMIN_EMAIL_ADDRESS" MAIL_FROM_NAME="Pixelfed $(getAdminEmailName)" INSTANCE_CONTACT_EMAIL="$EMAIL_ADMIN_EMAIL_ADDRESS" PF_ENABLE_CLOUD=false FILESYSTEM_CLOUD=s3 LDAP_LOGGING=false LDAP_CONNECTION=default LDAP_CONNECTIONS=default LDAP_DEFAULT_HOSTS=ldapserver LDAP_DEFAULT_PORT=389 LDAP_DEFAULT_USERNAME="$LDAP_READONLY_USER_BIND_DN" LDAP_DEFAULT_PASSWORD="$LDAP_READONLY_USER_PASSWORD" LDAP_DEFAULT_BASE_DN="$LDAP_BASE_DN" LDAP_DEFAULT_TIMEOUT=5 LDAP_DEFAULT_SSL=false LDAP_DEFAULT_TLS=true LDAP_GROUPFILTER="cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN" DOCKER_PROXY_PROFILE="disabled" DOCKER_APP_PHP_VERSION="8.3" DOCKER_APP_RUNTIME="fpm" DOCKER_APP_DEBIAN_RELEASE="bookworm" DOCKER_APP_BASE_TYPE="fpm" EOFPF cat < $HSHQ_STACKS_DIR/pixelfed/ldap.conf TLS_CERT /ldapcerts/pixelfed-app.crt TLS_KEY /ldapcerts/pixelfed-app.key TLS_CACERT /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT never TLS_REQSAN never EOFPF } function buildImagePixelfedV1() { set +e echo -e "\n========================================================================" echo -e " The Pixelfed image is being built. It can take up to 15 minutes for" echo -e " the process to complete, so please be patient." echo -e "========================================================================\n" img_ver=v0.12.5.tar.gz cd $HSHQ_BUILD_DIR sudo rm -fr $HSHQ_BUILD_DIR/pixelfed* wget -q4 -O $HSHQ_BUILD_DIR/pixelfed.tar.gz https://github.com/pixelfed/pixelfed/archive/refs/tags/$img_ver rVal=$? if [ $rVal -ne 0 ]; then rm -f $HSHQ_BUILD_DIR/pixelfed.tar.gz cd ~ return 6 fi tar xvzf ./pixelfed.tar.gz >/dev/null rVal=$? rm -f $HSHQ_BUILD_DIR/pixelfed.tar.gz if [ $rVal -ne 0 ]; then cd ~ return 7 fi cd pixelfed* rm -f app/Http/Controllers/Auth/LoginController.php cat < app/Http/Controllers/Auth/LoginController.php middleware('guest')->except('logout'); } public function showLoginForm() { if(config('pixelfed.bouncer.cloud_ips.ban_logins')) { abort_if(BouncerService::checkIp(request()->ip()), 404); } return view('auth.login'); } protected function credentials(Request \$request) { return [ 'mail' => \$request->email, 'password' => \$request->password, ]; } /** * Validate the user login request. * * @param \Illuminate\Http\Request \$request * * @return void */ public function validateLogin(\$request) { if(config('pixelfed.bouncer.cloud_ips.ban_logins')) { abort_if(BouncerService::checkIp(\$request->ip()), 404); } \$rules = [ \$this->username() => 'required|email', 'password' => 'required|string|min:6', ]; \$messages = []; if( (bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.login') || ( (bool) config_cache('captcha.triggers.login.enabled') && request()->session()->has('login_attempts') && request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts') ) ) { \$rules['h-captcha-response'] = 'required|filled|captcha|min:5'; \$messages['h-captcha-response.required'] = 'The captcha must be filled'; } \$request->validate(\$rules, \$messages); } /** * The user has been authenticated. * * @param \Illuminate\Http\Request \$request * @param mixed \$user * * @return mixed */ protected function authenticated(\$request, \$user) { if(\$user->status == 'deleted') { return; } \$log = new AccountLog(); \$log->user_id = \$user->id; \$log->item_id = \$user->id; \$log->item_type = 'App\User'; \$log->action = 'auth.login'; \$log->message = 'Account Login'; \$log->link = null; \$log->ip_address = \$request->ip(); \$log->user_agent = \$request->userAgent(); \$log->save(); } /** * Get the failed login response instance. * * @param \Illuminate\Http\Request \$request * @return \Symfony\Component\HttpFoundation\Response * * @throws \Illuminate\Validation\ValidationException */ protected function sendFailedLoginResponse(Request \$request) { if(config('captcha.triggers.login.enabled')) { if (\$request->session()->has('login_attempts')) { \$ct = \$request->session()->get('login_attempts'); \$request->session()->put('login_attempts', \$ct + 1); } else { \$request->session()->put('login_attempts', 1); } } throw ValidationException::withMessages([ \$this->username() => [trans('auth.failed')], ]); } } EOFPF mkdir -p app/Ldap/Rules rm -f app/Ldap/Rules/OnlyPixelfedUsers.php cat < app/Ldap/Rules/OnlyPixelfedUsers.php groups()->exists( Group::findOrFail(\$groupSearch) ); } } EOFPF rm -f app/User.php cat < app/User.php 'datetime', 'email_verified_at' => 'datetime', '2fa_setup_at' => 'datetime', 'last_active_at' => 'datetime', ]; } /** * The attributes that are mass assignable. * * @var array */ protected \$fillable = [ 'name', 'username', 'email', 'password', 'app_register_ip', 'email_verified_at', 'last_active_at', 'register_source', 'expo_token', 'notify_enabled', 'notify_like', 'notify_follow', 'notify_mention', 'notify_comment', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected \$hidden = [ 'email', 'password', 'is_admin', 'remember_token', 'email_verified_at', '2fa_enabled', '2fa_secret', '2fa_backup_codes', '2fa_setup_at', 'deleted_at', 'updated_at', ]; public function profile() { return \$this->hasOne(Profile::class); } public function url() { return url(config('app.url').'/'.\$this->username); } public function settings() { return \$this->hasOne(UserSetting::class); } public function statuses() { return \$this->hasManyThrough( Status::class, Profile::class ); } public function filters() { return \$this->hasMany(UserFilter::class, 'user_id', 'profile_id'); } public function receivesBroadcastNotificationsOn() { return 'App.User.'.\$this->id; } public function devices() { return \$this->hasMany(UserDevice::class); } public function storageUsedKey() { return 'profile:storage:used:'.\$this->id; } public function accountLog() { return \$this->hasMany(AccountLog::class); } public function interstitials() { return \$this->hasMany(AccountInterstitial::class); } public function avatarUrl() { if (! \$this->profile_id || \$this->status) { return config('app.url').'/storage/avatars/default.jpg'; } return AvatarService::get(\$this->profile_id); } public function routeNotificationForExpo() { return \$this->expo_token; } } EOFPF rm -f config/auth.php cat < config/auth.php [ 'guard' => 'web', 'passwords' => 'users', ], /* |-------------------------------------------------------------------------- | Authentication Guards |-------------------------------------------------------------------------- | | Next, you may define every authentication guard for your application. | Of course, a great default configuration has been defined for you | here which uses session storage and the Eloquent user provider. | | All authentication drivers have a user provider. This defines how the | users are actually retrieved out of your database or other storage | mechanisms used by this application to persist your user's data. | | Supported: "session", "token" | */ 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], ], /* |-------------------------------------------------------------------------- | User Providers |-------------------------------------------------------------------------- | | All authentication drivers have a user provider. This defines how the | users are actually retrieved out of your database or other storage | mechanisms used by this application to persist your user's data. | | If you have multiple user tables or models you may configure multiple | sources which represent each model / table. These sources may then | be assigned to any extra authentication guards you have defined. | | Supported: "database", "eloquent" | */ 'providers' => [ 'users' => [ 'driver' => 'ldap', 'model' => LdapRecord\Models\OpenLDAP\User::class, 'rules' => [ App\Ldap\Rules\OnlyPixelfedUsers::class, ], 'database' => [ 'model' => App\User::class, 'sync_passwords' => true, 'sync_attributes' => [ 'username' => 'uid', 'name' => 'cn', 'email' => 'mail', ], ], ], ], /* |-------------------------------------------------------------------------- | Resetting Passwords |-------------------------------------------------------------------------- | | You may specify multiple password reset configurations if you have more | than one user table or model in the application and you want to have | separate password reset settings based on the specific user types. | | The expire time is the number of minutes that the reset token should be | considered valid. This security feature keeps tokens short-lived so | they have less time to be guessed. You may change this as needed. | */ 'passwords' => [ 'users' => [ 'provider' => 'users', 'table' => 'password_resets', 'expire' => 60, 'throttle' => 60, ], ], 'in_app_registration' => (bool) env('APP_REGISTER', true), ]; EOFPF rm -f config/ldap.php cat < config/ldap.php env('LDAP_CONNECTION', 'default'), /* |-------------------------------------------------------------------------- | LDAP Connections |-------------------------------------------------------------------------- | | Below you may configure each LDAP connection your application requires | access to. Be sure to include a valid base DN - otherwise you may | not receive any results when performing LDAP search operations. | */ 'connections' => [ 'default' => [ 'hosts' => [env('LDAP_HOST', '127.0.0.1')], 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'password' => env('LDAP_PASSWORD', 'secret'), 'port' => env('LDAP_PORT', 389), 'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'), 'timeout' => env('LDAP_TIMEOUT', 5), 'use_ssl' => env('LDAP_SSL', false), 'use_tls' => env('LDAP_TLS', false), ], ], 'groupfilter' => env('LDAP_GROUPFILTER', 'everyone'), /* |-------------------------------------------------------------------------- | LDAP Logging |-------------------------------------------------------------------------- | | When LDAP logging is enabled, all LDAP search and authentication | operations are logged using the default application logging | driver. This can assist in debugging issues and more. | */ 'logging' => env('LDAP_LOGGING', true), /* |-------------------------------------------------------------------------- | LDAP Cache |-------------------------------------------------------------------------- | | LDAP caching enables the ability of caching search results using the | query builder. This is great for running expensive operations that | may take many seconds to complete, such as a pagination request. | */ 'cache' => [ 'enabled' => env('LDAP_CACHE', false), 'driver' => env('CACHE_DRIVER', 'file'), ], ]; EOFPF rm -f Dockerfile cat < Dockerfile # syntax=docker/dockerfile:1 # See https://hub.docker.com/r/docker/dockerfile ####################################################### # Configuration ####################################################### # See: https://github.com/mlocati/docker-php-extension-installer ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80" # See: https://github.com/composer/composer ARG COMPOSER_VERSION="2.6" # See: https://nginx.org/ ARG NGINX_VERSION="1.25.3" # See: https://github.com/ddollar/forego ARG FOREGO_VERSION="0.17.2" # See: https://github.com/hairyhenderson/gomplate ARG GOMPLATE_VERSION="v3.11.6" # See: https://github.com/jippi/dottie ARG DOTTIE_VERSION="v0.9.5" ARG APT_PACKAGES_EXTRA="nano libldb-dev libldap2-dev libpq-dev" ### # PHP base configuration ### # See: https://hub.docker.com/_/php/tags ARG PHP_VERSION="8.3" # See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants ARG PHP_BASE_TYPE="fpm" ARG PHP_DEBIAN_RELEASE="bookworm" ARG RUNTIME_UID=33 # often called 'www-data' ARG RUNTIME_GID=33 # often called 'www-data' # APT extra packages ARG APT_PACKAGES_EXTRA= # Extensions installed via [pecl install] # ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't # ! been released yet (after +10 months)! # ! See: https://github.com/Imagick/imagick/pull/641 ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58" ARG PHP_PECL_EXTENSIONS_EXTRA= # Extensions installed via [docker-php-ext-install] ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd ldap" ARG PHP_EXTENSIONS_EXTRA="" ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite" # GPG key for nginx apt repository # ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62" ARG NGINX_GPGKEY="8540A6F18833A80E9C1653A42FD21310B49F6B46" # GPP key path for nginx apt repository ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg" ####################################################### # Docker "copy from" images ####################################################### # Composer docker image from Docker Hub # # NOTE: Docker will *not* pull this image unless it's referenced (via build target) FROM composer:\${COMPOSER_VERSION} AS composer-image # php-extension-installer image from Docker Hub # # NOTE: Docker will *not* pull this image unless it's referenced (via build target) FROM mlocati/php-extension-installer:\${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer # nginx webserver from Docker Hub. # Used to copy some docker-entrypoint files for [nginx-runtime] # # NOTE: Docker will *not* pull this image unless it's referenced (via build target) FROM nginx:\${NGINX_VERSION} AS nginx-image # Forego is a Procfile "runner" that makes it trival to run multiple # processes under a simple init / PID 1 process. # # NOTE: Docker will *not* pull this image unless it's referenced (via build target) # # See: https://github.com/nginx-proxy/forego FROM nginxproxy/forego:\${FOREGO_VERSION}-debian AS forego-image # Dottie makes working with .env files easier and safer # # NOTE: Docker will *not* pull this image unless it's referenced (via build target) # # See: https://github.com/jippi/dottie FROM ghcr.io/jippi/dottie:\${DOTTIE_VERSION} AS dottie-image # gomplate-image grabs the gomplate binary from GitHub releases # # It's in its own layer so it can be fetched in parallel with other build steps FROM php:\${PHP_VERSION}-\${PHP_BASE_TYPE}-\${PHP_DEBIAN_RELEASE} AS gomplate-image ARG TARGETARCH ARG TARGETOS ARG GOMPLATE_VERSION RUN set -ex \ && curl \ --silent \ --show-error \ --location \ --output /usr/local/bin/gomplate \ https://github.com/hairyhenderson/gomplate/releases/download/\${GOMPLATE_VERSION}/gomplate_\${TARGETOS}-\${TARGETARCH} \ && chmod +x /usr/local/bin/gomplate \ && /usr/local/bin/gomplate --version ####################################################### # Base image ####################################################### FROM php:\${PHP_VERSION}-\${PHP_BASE_TYPE}-\${PHP_DEBIAN_RELEASE} AS base ARG BUILDKIT_SBOM_SCAN_STAGE="true" ARG APT_PACKAGES_EXTRA ARG PHP_DEBIAN_RELEASE ARG PHP_VERSION ARG RUNTIME_GID ARG RUNTIME_UID ARG TARGETPLATFORM ENV DEBIAN_FRONTEND="noninteractive" # Ensure we run all scripts through 'bash' rather than 'sh' SHELL ["/bin/bash", "-c"] # Set www-data to be RUNTIME_UID/RUNTIME_GID RUN groupmod --gid \${RUNTIME_GID} www-data \ && usermod --uid \${RUNTIME_UID} --gid \${RUNTIME_GID} www-data RUN set -ex \ && mkdir -pv /var/www/ \ && chown -R \${RUNTIME_UID}:\${RUNTIME_GID} /var/www WORKDIR /var/www/ ENV APT_PACKAGES_EXTRA=\${APT_PACKAGES_EXTRA} # Install and configure base layer COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh RUN --mount=type=cache,id=pixelfed-apt-\${PHP_VERSION}-\${PHP_DEBIAN_RELEASE}-\${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-\${PHP_VERSION}-\${PHP_DEBIAN_RELEASE}-\${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ /docker/install/base.sh ####################################################### # PHP: extensions ####################################################### FROM base AS php-extensions ARG PHP_DEBIAN_RELEASE ARG PHP_EXTENSIONS ARG PHP_EXTENSIONS_DATABASE ARG PHP_EXTENSIONS_EXTRA ARG PHP_PECL_EXTENSIONS ARG PHP_PECL_EXTENSIONS_EXTRA ARG PHP_VERSION ARG TARGETPLATFORM COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh RUN --mount=type=cache,id=pixelfed-pear-\${PHP_VERSION}-\${PHP_DEBIAN_RELEASE}-\${TARGETPLATFORM},sharing=locked,target=/tmp/pear \ --mount=type=cache,id=pixelfed-apt-\${PHP_VERSION}-\${PHP_DEBIAN_RELEASE}-\${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \ --mount=type=cache,id=pixelfed-apt-cache-\${PHP_VERSION}-\${PHP_DEBIAN_RELEASE}-\${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \ PHP_EXTENSIONS=\${PHP_EXTENSIONS} \ PHP_EXTENSIONS_DATABASE=\${PHP_EXTENSIONS_DATABASE} \ PHP_EXTENSIONS_EXTRA=\${PHP_EXTENSIONS_EXTRA} \ PHP_PECL_EXTENSIONS=\${PHP_PECL_EXTENSIONS} \ PHP_PECL_EXTENSIONS_EXTRA=\${PHP_PECL_EXTENSIONS_EXTRA} \ /docker/install/php-extensions.sh ####################################################### # Node: Build frontend ####################################################### # NOTE: Since the nodejs build is CPU architecture agnostic, # we only want to build once and cache it for other architectures. # We force the (CPU) [--platform] here to be architecture # of the "builder"/"server" and not the *target* CPU architecture # (e.g.) building the ARM version of Pixelfed on AMD64. FROM --platform=\${BUILDARCH} node:lts AS frontend-build ARG BUILDARCH ARG BUILD_FRONTEND=0 ARG RUNTIME_UID ARG RUNTIME_GID ARG NODE_ENV=production ENV NODE_ENV=\$NODE_ENV WORKDIR /var/www/ SHELL [ "/usr/bin/bash", "-c" ] # Install NPM dependencies RUN --mount=type=cache,id=pixelfed-node-\${BUILDARCH},sharing=locked,target=/tmp/cache \ --mount=type=bind,source=package.json,target=/var/www/package.json \ --mount=type=bind,source=package-lock.json,target=/var/www/package-lock.json \ < "\$NGINX_GPGKEY_PATH" \ && echo "deb [signed-by=\${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ \${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \ && apt-get update \ && apt-get install -y --no-install-recommends nginx=\${NGINX_VERSION}* # copy docker entrypoints from the *real* nginx image directly COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/ COPY docker/nginx/root / COPY docker/nginx/Procfile . STOPSIGNAL SIGQUIT CMD ["forego", "start", "-r"] EOFPF rm -f resources/views/settings/home.blade.php cat < resources/views/settings/home.blade.php @extends('settings.template') @section('section')

{{__('settings.home.account_settings')}}


{{Auth::user()->username}}

@csrf

{{__('settings.home.must_be_a_jpeg_or_png_max_avatar_size')}}

{{__('settings.home.delete_profile_photo')}}

@csrf

0/{{config('pixelfed.max_bio_length')}}

@if((bool) config_cache('federation.activitypub.enabled'))
{{__('settings.home.manage_account_alias')}}

{{__('settings.home.to_move_from_another_account_to_this_one_first_etc')}}

@if((bool) config_cache('federation.migration'))
{{__('settings.home.migrate_to_another_account')}}

{{__('settings.home.to_redirect_this_account_to_a_different_one_etc')}}

@endif @endif @if(config_cache('pixelfed.enforce_account_limit'))

{{__('settings.home.storage_usage')}}

{{\$storage['percentUsed']}}% used {{\$storage['usedPretty']}} / {{\$storage['limitPretty']}}
@endif
@endsection @push('scripts') @endpush EOFPF DOCKER_BUILDKIT=1 docker build -t $(getScriptImageByContainerName pixelfed-app) . buildRetVal=$? cd ~ sudo rm -fr $HSHQ_BUILD_DIR/pixelfed* return $buildRetVal } function performUpdatePixelfed() { perform_stack_name=pixelfed prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mariadb:10.7.3,hshq/pixelfed:v1,bitnami/redis:7.4.2 image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="hshq/pixelfed:v1,hshq/pixelfed:v1" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfPixelfedFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v2 curImageList=mirror.gcr.io/mariadb:10.7.3,hshq/pixelfed:v1,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="hshq/pixelfed:v1,hshq/pixelfed:v1" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfPixelfedFixRedisCompose() { replaceRedisBlock pixelfed pixelfed-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/pixelfed/redis/* } # Yamtrack function installYamtrack() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory yamtrack "Yamtrack" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName yamtrack-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName yamtrack-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName yamtrack-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/yamtrack mkdir $HSHQ_STACKS_DIR/yamtrack/db mkdir $HSHQ_STACKS_DIR/yamtrack/dbexport chmod 777 $HSHQ_STACKS_DIR/yamtrack/dbexport initServicesCredentials set +e YAMTRACK_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $YAMTRACK_OIDC_CLIENT_SECRET | tr -d ':\n') outputConfigYamtrack oidcBlock=$(cat $HOME/yamtrack.oidc) rm -f $HOME/yamtrack.oidc insertOIDCClientAuthelia yamtrack "$oidcBlock" installStack yamtrack yamtrack-app "Listening at: http://127.0.0.1:8001" $HOME/yamtrack.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$YAMTRACK_INIT_ENV" = "true" ]; then sendEmail -s "Yamtrack Admin Login Info" -b "Yamtrack Admin Username: $YAMTRACK_ADMIN_USERNAME\nYamtrack Admin Password: $YAMTRACK_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" YAMTRACK_INIT_ENV=true updateConfigVar YAMTRACK_INIT_ENV $YAMTRACK_INIT_ENV fi sleep 3 docker exec -it yamtrack-app python manage.py shell -c "User.objects.create_user(username='$YAMTRACK_ADMIN_USERNAME', password='$YAMTRACK_ADMIN_PASSWORD', is_staff=True, is_superuser=True)" > /dev/null 2>&1 addReadOnlyUserToDatabase Yamtrack postgres yamtrack-db $YAMTRACK_DATABASE_NAME $YAMTRACK_DATABASE_USER $YAMTRACK_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/yamtrack/dbexport $YAMTRACK_DATABASE_READONLYUSER $YAMTRACK_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://yamtrack-app:8000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_YAMTRACK $MANAGETLS_YAMTRACK "$is_integrate_hshq" $NETDEFAULT_YAMTRACK "$inner_block" insertSubAuthelia $SUB_YAMTRACK.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll yamtrack "$FMLNAME_YAMTRACK" $USERTYPE_YAMTRACK "https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN" "yamtrack.png" "$(getHeimdallOrderFromSub $SUB_YAMTRACK $USERTYPE_YAMTRACK)" restartAllCaddyContainers checkAddDBConnection true yamtrack "$FMLNAME_YAMTRACK" postgres yamtrack-db $YAMTRACK_DATABASE_NAME $YAMTRACK_DATABASE_USER $YAMTRACK_DATABASE_USER_PASSWORD fi } function outputConfigYamtrack() { cat < $HOME/yamtrack-compose.yml $STACK_VERSION_PREFIX yamtrack $(getScriptStackVersion yamtrack) services: yamtrack-db: image: $(getScriptImageByContainerName yamtrack-db) container_name: yamtrack-db hostname: yamtrack-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-yamtrack-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/yamtrack/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/yamtrack/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.yamtrack-hourly-db.schedule=@every 1h" - "ofelia.job-exec.yamtrack-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.yamtrack-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.yamtrack-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.yamtrack-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.yamtrack-hourly-db.email-from=Yamtrack Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.yamtrack-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.yamtrack-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.yamtrack-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.yamtrack-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.yamtrack-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.yamtrack-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.yamtrack-monthly-db.email-from=Yamtrack Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.yamtrack-monthly-db.mail-only-on-error=false" yamtrack-app: image: $(getScriptImageByContainerName yamtrack-app) container_name: yamtrack-app hostname: yamtrack-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - yamtrack-db networks: - int-yamtrack-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro yamtrack-redis: image: $(getScriptImageByContainerName yamtrack-redis) container_name: yamtrack-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $YAMTRACK_REDIS_PASSWORD --appendonly no networks: - int-yamtrack-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-yamtrack-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/yamtrack/db networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-yamtrack-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/yamtrack.env TZ=\${PORTAINER_TZ} PUID=\${PORTAINER_UID} PGID=\${PORTAINER_GID} SOCIAL_PROVIDERS=allauth.socialaccount.providers.openid_connect SOCIALACCOUNT_PROVIDERS={"openid_connect":{"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"authelia","name":"Authelia","client_id":"$YAMTRACK_OIDC_CLIENT_ID","secret":"$YAMTRACK_OIDC_CLIENT_SECRET","settings":{"server_url":"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/.well-known/openid-configuration"}}]}} ACCOUNT_DEFAULT_HTTP_PROTOCOL=https REDIS_URL=redis://:$YAMTRACK_REDIS_PASSWORD@yamtrack-redis:6379/0 SECRET=$YAMTRACK_SECRET_KEY URLS=https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN ALLOWED_HOSTS=https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN CSRF=https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN REGISTRATION=true DEBUG=false ADMIN_ENABLED=true DB_HOST=yamtrack-db DB_PORT=5432 DB_NAME=$YAMTRACK_DATABASE_NAME DB_USER=$YAMTRACK_DATABASE_USER DB_PASSWORD=$YAMTRACK_DATABASE_USER_PASSWORD POSTGRES_DB=$YAMTRACK_DATABASE_NAME POSTGRES_USER=$YAMTRACK_DATABASE_USER POSTGRES_PASSWORD=$YAMTRACK_DATABASE_USER_PASSWORD POSTGRES_INITDB_ARGS=--data-checksums PYTHON_VER=python3.12 EOFMT rm -f $HOME/yamtrack.oidc cat < $HOME/yamtrack.oidc # Authelia OIDC Client yamtrack BEGIN - client_id: $YAMTRACK_OIDC_CLIENT_ID client_name: $FMLNAME_YAMTRACK client_secret: '$YAMTRACK_OIDC_CLIENT_SECRET_HASH' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth require_pkce: true pkce_challenge_method: S256 redirect_uris: - https://$SUB_YAMTRACK.$HOMESERVER_DOMAIN/accounts/oidc/authelia/login/callback/ scopes: - openid - profile - email - groups userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client yamtrack END EOFIM } function performUpdateYamtrack() { perform_stack_name=yamtrack prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=postgres:15.0-bullseye,ghcr.io/fuzzygrim/yamtrack:0.22.7,bitnami/redis:7.4.2 image_update_map[0]="postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/fuzzygrim/yamtrack:0.22.7,ghcr.io/fuzzygrim/yamtrack:0.24.7" image_update_map[2]="bitnami/redis:7.4.2,mirror.gcr.io/redis:8.2.0-bookworm" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfYamtrackFixRedisCompose perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/fuzzygrim/yamtrack:0.24.7,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/fuzzygrim/yamtrack:0.24.7,ghcr.io/fuzzygrim/yamtrack:0.24.8" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 3) newVer=v3 curImageList=mirror.gcr.io/postgres:15.0-bullseye,ghcr.io/fuzzygrim/yamtrack:0.24.8,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="ghcr.io/fuzzygrim/yamtrack:0.24.8,ghcr.io/fuzzygrim/yamtrack:0.24.8" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfYamtrackFixRedisCompose() { replaceRedisBlock yamtrack yamtrack-redis mirror.gcr.io/redis:8.2.0-bookworm true sudo rm -fr $HSHQ_NONBACKUP_DIR/yamtrack/redis/* } # Servarr function installServarr() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory servarr "Servarr" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName servarr-sonarr) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName servarr-radarr) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName servarr-lidarr) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName servarr-readarr) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName servarr-bazarr) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName servarr-mylar3) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName servarr-prowlarr) if [ $? -ne 0 ]; then return 1 fi set -e mkdir -p $HSHQ_STACKS_DIR/shared/rdata/media/{audio,books,comics,misc,movies,software,tv} mkdir -p $HSHQ_STACKS_DIR/servarr/sonarr/config mkdir -p $HSHQ_STACKS_DIR/servarr/radarr/config mkdir -p $HSHQ_STACKS_DIR/servarr/lidarr/config mkdir -p $HSHQ_STACKS_DIR/servarr/readarr/config mkdir -p $HSHQ_STACKS_DIR/servarr/bazarr/config mkdir -p $HSHQ_STACKS_DIR/servarr/mylar3/config mkdir -p $HSHQ_STACKS_DIR/servarr/prowlarr/config initServicesCredentials set +e outputConfigServarr installStack servarr servarr-sonarr "\[ls.io-init\] done" $HOME/servarr.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi waitForContainerLogString servarr-sonarr 1 300 "\[ls.io-init\] done" retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi waitForContainerLogString servarr-radarr 1 300 "\[ls.io-init\] done" retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi waitForContainerLogString servarr-lidarr 1 300 "\[ls.io-init\] done" retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi waitForContainerLogString servarr-readarr 1 300 "\[ls.io-init\] done" retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi waitForContainerLogString servarr-bazarr 1 300 "\[ls.io-init\] done" retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi waitForContainerLogString servarr-mylar3 1 300 "\[ls.io-init\] done" retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi waitForContainerLogString servarr-prowlarr 1 300 "\[ls.io-init\] done" retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 5 echo "Stopping servarr stack..." startStopStack servarr stop sonarr_api_key=$(cat $HSHQ_STACKS_DIR/servarr/sonarr/config/config.xml | grep ApiKey | cut -d">" -f2 | cut -d"<" -f1) radarr_api_key=$(cat $HSHQ_STACKS_DIR/servarr/radarr/config/config.xml | grep ApiKey | cut -d">" -f2 | cut -d"<" -f1) lidarr_api_key=$(cat $HSHQ_STACKS_DIR/servarr/lidarr/config/config.xml | grep ApiKey | cut -d">" -f2 | cut -d"<" -f1) readarr_api_key=$(cat $HSHQ_STACKS_DIR/servarr/readarr/config/config.xml | grep ApiKey | cut -d">" -f2 | cut -d"<" -f1) mylar3_api_key=$(uuidgen | sed "s/-//g") cat < /tmp/hashtmp.py import base64 import hashlib import secrets password='$SONARR_ADMIN_PASSWORD' salt='$(pwgen -c -n 16 1)' iterations=10000 key = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt.encode('utf-8'), iterations, dklen=32) print(base64.b64encode(key),base64.b64encode(salt.encode('utf-8'))); EOFPY sonarr_hashes=$(python3 /tmp/hashtmp.py) sonarr_pw_hash=$(echo "$sonarr_hashes" | cut -d" " -f1 | cut -d"'" -f2) sonarr_salt_hash=$(echo "$sonarr_hashes" | cut -d" " -f2 | cut -d"'" -f2) sqlite3 $HSHQ_STACKS_DIR/servarr/sonarr/config/sonarr.db "insert into Users("Id","Identifier","Username","Password","Salt","Iterations") values(1,'$(uuidgen)','$SONARR_ADMIN_USERNAME','$sonarr_pw_hash','$sonarr_salt_hash',10000);" sqlite3 $HSHQ_STACKS_DIR/servarr/sonarr/config/sonarr.db "insert into RootFolders("Path") values('/data/media/tv/');" sed -i 's/^.*AuthenticationMethod.*$/ Forms<\/AuthenticationMethod>/' $HSHQ_STACKS_DIR/servarr/sonarr/config/config.xml sed -i 's/<\/Config>/\ False<\/AnalyticsEnabled>\n<\/Config>/' $HSHQ_STACKS_DIR/servarr/sonarr/config/config.xml cat < /tmp/hashtmp.py import base64 import hashlib import secrets password='$RADARR_ADMIN_PASSWORD' salt='$(pwgen -c -n 16 1)' iterations=10000 key = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt.encode('utf-8'), iterations, dklen=32) print(base64.b64encode(key),base64.b64encode(salt.encode('utf-8'))); EOFPY radarr_hashes=$(python3 /tmp/hashtmp.py) radarr_pw_hash=$(echo "$radarr_hashes" | cut -d" " -f1 | cut -d"'" -f2) radarr_salt_hash=$(echo "$radarr_hashes" | cut -d" " -f2 | cut -d"'" -f2) sqlite3 $HSHQ_STACKS_DIR/servarr/radarr/config/radarr.db "insert into Users("Id","Identifier","Username","Password","Salt","Iterations") values(1,'$(uuidgen)','$RADARR_ADMIN_USERNAME','$radarr_pw_hash','$radarr_salt_hash',10000);" sqlite3 $HSHQ_STACKS_DIR/servarr/radarr/config/radarr.db "insert into RootFolders("Path") values('/data/media/movies/');" sed -i 's/^.*AuthenticationMethod.*$/ Forms<\/AuthenticationMethod>/' $HSHQ_STACKS_DIR/servarr/radarr/config/config.xml sed -i 's/<\/Config>/\ False<\/AnalyticsEnabled>\n<\/Config>/' $HSHQ_STACKS_DIR/servarr/radarr/config/config.xml cat < /tmp/hashtmp.py import base64 import hashlib import secrets password='$LIDARR_ADMIN_PASSWORD' salt='$(pwgen -c -n 16 1)' iterations=10000 key = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt.encode('utf-8'), iterations, dklen=32) print(base64.b64encode(key),base64.b64encode(salt.encode('utf-8'))); EOFPY lidarr_hashes=$(python3 /tmp/hashtmp.py) lidarr_pw_hash=$(echo "$lidarr_hashes" | cut -d" " -f1 | cut -d"'" -f2) lidarr_salt_hash=$(echo "$lidarr_hashes" | cut -d" " -f2 | cut -d"'" -f2) sqlite3 $HSHQ_STACKS_DIR/servarr/lidarr/config/lidarr.db "insert into Users("Id","Identifier","Username","Password","Salt","Iterations") values(1,'$(uuidgen)','$LIDARR_ADMIN_USERNAME','$lidarr_pw_hash','$lidarr_salt_hash',10000);" sqlite3 $HSHQ_STACKS_DIR/servarr/lidarr/config/lidarr.db "insert into RootFolders("Path","Name","DefaultMetadataProfileId","DefaultQualityProfileId","DefaultMonitorOption","DefaultTags","DefaultNewItemMonitorOption") values('/data/media/audio/','Audio',1,1,0,'[]',0);" sed -i 's/^.*AuthenticationMethod.*$/ Forms<\/AuthenticationMethod>/' $HSHQ_STACKS_DIR/servarr/lidarr/config/config.xml sed -i 's/<\/Config>/\ False<\/AnalyticsEnabled>\n<\/Config>/' $HSHQ_STACKS_DIR/servarr/lidarr/config/config.xml readarr_pw_hash=$(echo -n "$READARR_ADMIN_PASSWORD" | sha256sum | xargs | cut -d" " -f1) sqlite3 $HSHQ_STACKS_DIR/servarr/readarr/config/readarr.db "insert into Users("Id","Identifier","Username","Password") values(1,'$(uuidgen)','$READARR_ADMIN_USERNAME','$readarr_pw_hash');" sqlite3 $HSHQ_STACKS_DIR/servarr/readarr/config/readarr.db "insert into RootFolders("Path","Name","DefaultMetadataProfileId","DefaultQualityProfileId","DefaultMonitorOption","DefaultTags","IsCalibreLibrary","DefaultNewItemMonitorOption") values('/data/media/books/','Books',1,1,0,'[]',0,0);" sed -i 's/^.*AuthenticationMethod.*$/ Forms<\/AuthenticationMethod>/' $HSHQ_STACKS_DIR/servarr/readarr/config/config.xml sed -i 's/<\/Config>/\ False<\/AnalyticsEnabled>\n<\/Config>/' $HSHQ_STACKS_DIR/servarr/readarr/config/config.xml bazarr_pw_hash=$(echo -n "$BAZARR_ADMIN_PASSWORD" | md5sum | xargs | cut -d" " -f1) yq -r -i ".analytics.enabled = false" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".auth.password = \"$bazarr_pw_hash\"" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".auth.type = \"form\"" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".auth.username = \"$BAZARR_ADMIN_USERNAME\"" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".general.use_sonarr = true" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".sonarr.apikey = \"$sonarr_api_key\"" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".sonarr.ip = \"$SUB_SERVARR_SONARR.$HOMESERVER_DOMAIN\"" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".sonarr.port = 443" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".sonarr.ssl = true" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".general.use_radarr = true" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".radarr.apikey = \"$radarr_api_key\"" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".radarr.ip = \"$SUB_SERVARR_RADARR.$HOMESERVER_DOMAIN\"" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".radarr.port = 443" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml yq -r -i ".radarr.ssl = true" $HSHQ_STACKS_DIR/servarr/bazarr/config/config/config.yaml sed -i "s/^destination_dir.*/destination_dir = \/data\/media\/comics/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^replace_char.*/replace_char = \./" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^zero_level_n.*/zero_level_n = none/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "/^config_version.*/a\minimal_ini = False/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^http_username.*/http_username = $MYLAR3_ADMIN_USERNAME/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^http_password.*/http_password = $MYLAR3_ADMIN_PASSWORD/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^authentication.*/authentication = 2/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^api_enabled.*/api_enabled = True/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^api_key.*/api_key = $mylar3_api_key/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^log_dir.*/log_dir = \/config\/mylar\/logs/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^provider_order.*/provider_order = None/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^nzbget_priority.*/nzbget_priority = Default/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "/^extra_torznabs.*/a torznab_name = None\ntorznab_host = None\ntorznab_apikey = None\ntorznab_category = None\ntorznab_verify = False/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^email_enabled.*/email_enabled = True/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^email_from.*/email_from = $EMAIL_ADMIN_EMAIL_ADDRESS/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^email_to.*/email_to = $EMAIL_ADMIN_EMAIL_ADDRESS/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^email_server.*/email_server = $SMTP_HOSTNAME/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^email_port.*/email_port = $SMTP_HOSTPORT/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini sed -i "s/^email_enc.*/email_enc = 2/" $HSHQ_STACKS_DIR/servarr/mylar3/config/mylar/config.ini cat < /tmp/hashtmp.py import base64 import hashlib import secrets password='$PROWLARR_ADMIN_PASSWORD' salt='$(pwgen -c -n 16 1)' iterations=10000 key = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt.encode('utf-8'), iterations, dklen=32) print(base64.b64encode(key),base64.b64encode(salt.encode('utf-8'))); EOFPY prowlarr_hashes=$(python3 /tmp/hashtmp.py) prowlarr_pw_hash=$(echo "$prowlarr_hashes" | cut -d" " -f1 | cut -d"'" -f2) prowlarr_salt_hash=$(echo "$prowlarr_hashes" | cut -d" " -f2 | cut -d"'" -f2) sqlite3 $HSHQ_STACKS_DIR/servarr/prowlarr/config/prowlarr.db "insert into Users("Id","Identifier","Username","Password","Salt","Iterations") values(1,'$(uuidgen)','$PROWLARR_ADMIN_USERNAME','$prowlarr_pw_hash','$prowlarr_salt_hash',10000);" sed -i 's/^.*AuthenticationMethod.*$/ Forms<\/AuthenticationMethod>/' $HSHQ_STACKS_DIR/servarr/prowlarr/config/config.xml sed -i 's/<\/Config>/\ False<\/AnalyticsEnabled>\n<\/Config>/' $HSHQ_STACKS_DIR/servarr/prowlarr/config/config.xml rm -f /tmp/hashtmp.py config_settings=$(cat <&to=$EMAIL_ADMIN_EMAIL_ADDRESS' where name='E-Mail';" mail_settings=$(cat <" SERVARR_INIT_ENV=true updateConfigVar SERVARR_INIT_ENV $SERVARR_INIT_ENV fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_SERVARR_SONARR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://servarr-sonarr:8989 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SERVARR_SONARR $MANAGETLS_SERVARR_SONARR "$is_integrate_hshq" $NETDEFAULT_SERVARR_SONARR "$inner_block" insertSubAuthelia $SUB_SERVARR_SONARR.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_SERVARR_RADARR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://servarr-radarr:7878 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SERVARR_RADARR $MANAGETLS_SERVARR_RADARR "$is_integrate_hshq" $NETDEFAULT_SERVARR_RADARR "$inner_block" insertSubAuthelia $SUB_SERVARR_RADARR.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_SERVARR_LIDARR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://servarr-lidarr:8686 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SERVARR_LIDARR $MANAGETLS_SERVARR_LIDARR "$is_integrate_hshq" $NETDEFAULT_SERVARR_LIDARR "$inner_block" insertSubAuthelia $SUB_SERVARR_LIDARR.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_SERVARR_READARR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://servarr-readarr:8787 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SERVARR_READARR $MANAGETLS_SERVARR_READARR "$is_integrate_hshq" $NETDEFAULT_SERVARR_READARR "$inner_block" insertSubAuthelia $SUB_SERVARR_READARR.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_SERVARR_BAZARR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://servarr-bazarr:6767 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SERVARR_BAZARR $MANAGETLS_SERVARR_BAZARR "$is_integrate_hshq" $NETDEFAULT_SERVARR_BAZARR "$inner_block" insertSubAuthelia $SUB_SERVARR_BAZARR.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_SERVARR_MYLAR3.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://servarr-mylar3:8090 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SERVARR_MYLAR3 $MANAGETLS_SERVARR_MYLAR3 "$is_integrate_hshq" $NETDEFAULT_SERVARR_MYLAR3 "$inner_block" insertSubAuthelia $SUB_SERVARR_MYLAR3.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_SERVARR_PROWLARR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://servarr-prowlarr:9696 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SERVARR_PROWLARR $MANAGETLS_SERVARR_PROWLARR "$is_integrate_hshq" $NETDEFAULT_SERVARR_PROWLARR "$inner_block" insertSubAuthelia $SUB_SERVARR_PROWLARR.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll servarr "$FMLNAME_SERVARR_SONARR" $USERTYPE_SERVARR_SONARR "https://$SUB_SERVARR_SONARR.$HOMESERVER_DOMAIN" "sonarr.png" "$(getHeimdallOrderFromSub $SUB_SERVARR_SONARR $USERTYPE_SERVARR_SONARR)" insertEnableSvcAll servarr "$FMLNAME_SERVARR_RADARR" $USERTYPE_SERVARR_RADARR "https://$SUB_SERVARR_RADARR.$HOMESERVER_DOMAIN" "radarr.png" "$(getHeimdallOrderFromSub $SUB_SERVARR_RADARR $USERTYPE_SERVARR_RADARR)" insertEnableSvcAll servarr "$FMLNAME_SERVARR_LIDARR" $USERTYPE_SERVARR_LIDARR "https://$SUB_SERVARR_LIDARR.$HOMESERVER_DOMAIN" "lidarr.png" "$(getHeimdallOrderFromSub $SUB_SERVARR_LIDARR $USERTYPE_SERVARR_LIDARR)" insertEnableSvcAll servarr "$FMLNAME_SERVARR_READARR" $USERTYPE_SERVARR_READARR "https://$SUB_SERVARR_READARR.$HOMESERVER_DOMAIN" "readarr.png" "$(getHeimdallOrderFromSub $SUB_SERVARR_READARR $USERTYPE_SERVARR_READARR)" insertEnableSvcAll servarr "$FMLNAME_SERVARR_BAZARR" $USERTYPE_SERVARR_BAZARR "https://$SUB_SERVARR_BAZARR.$HOMESERVER_DOMAIN" "bazarr.png" "$(getHeimdallOrderFromSub $SUB_SERVARR_BAZARR $USERTYPE_SERVARR_BAZARR)" insertEnableSvcAll servarr "$FMLNAME_SERVARR_MYLAR3" $USERTYPE_SERVARR_MYLAR3 "https://$SUB_SERVARR_MYLAR3.$HOMESERVER_DOMAIN" "mylar3.png" "$(getHeimdallOrderFromSub $SUB_SERVARR_MYLAR3 $USERTYPE_SERVARR_MYLAR3)" insertEnableSvcAll servarr "$FMLNAME_SERVARR_PROWLARR" $USERTYPE_SERVARR_PROWLARR "https://$SUB_SERVARR_PROWLARR.$HOMESERVER_DOMAIN" "prowlarr.png" "$(getHeimdallOrderFromSub $SUB_SERVARR_PROWLARR $USERTYPE_SERVARR_PROWLARR)" restartAllCaddyContainers fi } function outputConfigServarr() { cat < $HOME/servarr-compose.yml $STACK_VERSION_PREFIX servarr $(getScriptStackVersion servarr) services: servarr-sonarr: image: $(getScriptImageByContainerName servarr-sonarr) container_name: servarr-sonarr hostname: servarr-sonarr restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata:/data - \${PORTAINER_HSHQ_STACKS_DIR}/servarr/sonarr/config:/config servarr-radarr: image: $(getScriptImageByContainerName servarr-radarr) container_name: servarr-radarr hostname: servarr-radarr restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata:/data - \${PORTAINER_HSHQ_STACKS_DIR}/servarr/radarr/config:/config servarr-lidarr: image: $(getScriptImageByContainerName servarr-lidarr) container_name: servarr-lidarr hostname: servarr-lidarr restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata:/data - \${PORTAINER_HSHQ_STACKS_DIR}/servarr/lidarr/config:/config servarr-readarr: image: $(getScriptImageByContainerName servarr-readarr) container_name: servarr-readarr hostname: servarr-readarr restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata:/data - \${PORTAINER_HSHQ_STACKS_DIR}/servarr/readarr/config:/config servarr-bazarr: image: $(getScriptImageByContainerName servarr-bazarr) container_name: servarr-bazarr hostname: servarr-bazarr restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata:/data - \${PORTAINER_HSHQ_STACKS_DIR}/servarr/bazarr/config:/config servarr-mylar3: image: $(getScriptImageByContainerName servarr-mylar3) container_name: servarr-mylar3 hostname: servarr-mylar3 restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata:/data - \${PORTAINER_HSHQ_STACKS_DIR}/servarr/mylar3/config:/config servarr-prowlarr: image: $(getScriptImageByContainerName servarr-prowlarr) container_name: servarr-prowlarr hostname: servarr-prowlarr restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/servarr/prowlarr/config:/config networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true EOFMT cat < $HOME/servarr.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID EOFMT } function performUpdateServarr() { perform_stack_name=servarr prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=linuxserver/sonarr:4.0.15,linuxserver/radarr:5.27.1-nightly,linuxserver/lidarr:2.13.1-nightly,linuxserver/readarr:0.4.19-nightly,linuxserver/bazarr:1.5.2,linuxserver/mylar3:0.8.2,linuxserver/prowlarr:2.0.1-nightly image_update_map[0]="linuxserver/sonarr:4.0.15,mirror.gcr.io/linuxserver/sonarr:4.0.16" image_update_map[1]="linuxserver/radarr:5.27.1-nightly,mirror.gcr.io/linuxserver/radarr:6.1.0-nightly" image_update_map[2]="linuxserver/lidarr:2.13.1-nightly,mirror.gcr.io/linuxserver/lidarr:3.1.0" image_update_map[3]="linuxserver/readarr:0.4.19-nightly,mirror.gcr.io/linuxserver/readarr:0.4.19-nightly" image_update_map[4]="linuxserver/bazarr:1.5.2,mirror.gcr.io/linuxserver/bazarr:1.5.3" image_update_map[5]="linuxserver/mylar3:0.8.2,mirror.gcr.io/linuxserver/mylar3:0.8.3" image_update_map[6]="linuxserver/prowlarr:2.0.1-nightly,mirror.gcr.io/linuxserver/prowlarr:2.3.1-nightly" ;; 2) newVer=v2 curImageList=mirror.gcr.io/linuxserver/sonarr:4.0.16,mirror.gcr.io/linuxserver/radarr:6.1.0-nightly,mirror.gcr.io/linuxserver/lidarr:3.1.0,mirror.gcr.io/linuxserver/readarr:0.4.19-nightly,mirror.gcr.io/linuxserver/bazarr:1.5.3,mirror.gcr.io/linuxserver/mylar3:0.8.3,mirror.gcr.io/linuxserver/prowlarr:2.3.1-nightly image_update_map[0]="mirror.gcr.io/linuxserver/sonarr:4.0.16,mirror.gcr.io/linuxserver/sonarr:4.0.16" image_update_map[1]="mirror.gcr.io/linuxserver/radarr:6.1.0-nightly,mirror.gcr.io/linuxserver/radarr:6.1.0-nightly" image_update_map[2]="mirror.gcr.io/linuxserver/lidarr:3.1.0,mirror.gcr.io/linuxserver/lidarr:3.1.0" image_update_map[3]="mirror.gcr.io/linuxserver/readarr:0.4.19-nightly,mirror.gcr.io/linuxserver/readarr:0.4.19-nightly" image_update_map[4]="mirror.gcr.io/linuxserver/bazarr:1.5.3,mirror.gcr.io/linuxserver/bazarr:1.5.3" image_update_map[5]="mirror.gcr.io/linuxserver/mylar3:0.8.3,mirror.gcr.io/linuxserver/mylar3:0.8.3" image_update_map[6]="mirror.gcr.io/linuxserver/prowlarr:2.3.1-nightly,mirror.gcr.io/linuxserver/prowlarr:2.3.1-nightly" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # SABnzbd function installSABnzbd() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory sabnzbd "SABnzbd" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName sabnzbd) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/sabnzbd mkdir $HSHQ_STACKS_DIR/sabnzbd/config mkdir -p $HSHQ_STACKS_DIR/shared/rdata/usenet/incomplete mkdir -p $HSHQ_STACKS_DIR/shared/rdata/usenet/complete/{audio,books,comics,misc,movies,software,tv} initServicesCredentials set +e outputConfigSABnzbd installStack sabnzbd sabnzbd "Serving SSDP" $HOME/sabnzbd.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$SABNZBD_INIT_ENV" = "true" ]; then sendEmail -s "SABnzbd Admin Login Info" -b "SABnzbd Admin Username: $SABNZBD_ADMIN_USERNAME\nSABnzbd Admin Password: $SABNZBD_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" SABNZBD_INIT_ENV=true updateConfigVar SABNZBD_INIT_ENV $SABNZBD_INIT_ENV fi sleep 3 startStopStack sabnzbd stop sed -i "s/^refresh_rate =.*/refresh_rate = 1/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^username =.*/username = $SABNZBD_ADMIN_USERNAME/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^password =.*/password = $SABNZBD_ADMIN_PASSWORD/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^web_color =.*/web_color = Night/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^download_dir =.*/download_dir = \/data\/usenet\/incomplete/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^complete_dir =.*/complete_dir = \/data\/usenet\/complete/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^history_retention_number =.*/history_retention_number = 1/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^host_whitelist =.*/host_whitelist = sabnzbd, $SUB_SABNZBD.$HOMESERVER_DOMAIN/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^email_server =.*/email_server = $SMTP_HOSTNAME/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^email_to =.*/email_to = $EMAIL_ADMIN_EMAIL_ADDRESS,/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^email_from =.*/email_from = $EMAIL_ADMIN_EMAIL_ADDRESS/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^email_full =.*/email_full = 1/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^email_rss =.*/email_rss = 1/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ncenter_prio_complete =.*/ncenter_prio_complete = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ncenter_prio_failed =.*/ncenter_prio_failed = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ncenter_prio_disk_full =.*/ncenter_prio_disk_full = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ncenter_prio_other =.*/ncenter_prio_other = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^acenter_prio_complete =.*/acenter_prio_complete = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^acenter_prio_failed =.*/acenter_prio_failed = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^acenter_prio_disk_full =.*/acenter_prio_disk_full = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^acenter_prio_other =.*/acenter_prio_other = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ntfosd_enable =.*/ntfosd_enable = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ntfosd_prio_complete =.*/ntfosd_prio_complete = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ntfosd_prio_failed =.*/ntfosd_prio_failed = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ntfosd_prio_disk_full =.*/ntfosd_prio_disk_full = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^ntfosd_prio_other =.*/ntfosd_prio_other = 0/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^prowl_prio_complete =.*/prowl_prio_complete = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^prowl_prio_failed =.*/prowl_prio_failed = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^prowl_prio_disk_full =.*/prowl_prio_disk_full = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^prowl_prio_other =.*/prowl_prio_other = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_download =.*/pushover_prio_download = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_pause_resume =.*/pushover_prio_pause_resume = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_complete =.*/pushover_prio_complete = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_failed =.*/pushover_prio_failed = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_disk_full =.*/pushover_prio_disk_full = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_warning =.*/pushover_prio_warning = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_error =.*/pushover_prio_error = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^pushover_prio_other =.*/pushover_prio_other = -3/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^nscript_script =.*/nscript_script = None/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini sed -i "s/^nscript_prio_other =.*/nscript_prio_other = 1/" $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini tee -a $HSHQ_STACKS_DIR/sabnzbd/config/sabnzbd.ini >/dev/null <>https://$SUB_SABNZBD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://sabnzbd:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_SABNZBD $MANAGETLS_SABNZBD "$is_integrate_hshq" $NETDEFAULT_SABNZBD "$inner_block" insertSubAuthelia $SUB_SABNZBD.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll sabnzbd "$FMLNAME_SABNZBD" $USERTYPE_SABNZBD "https://$SUB_SABNZBD.$HOMESERVER_DOMAIN" "sabnzbd.png" "$(getHeimdallOrderFromSub $SUB_SABNZBD $USERTYPE_SABNZBD)" restartAllCaddyContainers fi } function outputConfigSABnzbd() { cat < $HOME/sabnzbd-compose.yml $STACK_VERSION_PREFIX sabnzbd $(getScriptStackVersion sabnzbd) services: sabnzbd: image: $(getScriptImageByContainerName sabnzbd) container_name: sabnzbd hostname: sabnzbd restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/sabnzbd/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata/usenet:/data/usenet networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true EOFMT cat < $HOME/sabnzbd.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID EOFMT } function performUpdateSABnzbd() { perform_stack_name=sabnzbd prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=linuxserver/sabnzbd:4.5.1 image_update_map[0]="linuxserver/sabnzbd:4.5.1,mirror.gcr.io/linuxserver/sabnzbd:4.5.5" ;; 2) newVer=v2 curImageList=mirror.gcr.io/linuxserver/sabnzbd:4.5.5 image_update_map[0]="mirror.gcr.io/linuxserver/sabnzbd:4.5.5,mirror.gcr.io/linuxserver/sabnzbd:4.5.5" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # qBittorrent function installqBittorrent() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory qbittorrent "qBittorrent" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName qbittorrent) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/qbittorrent mkdir $HSHQ_STACKS_DIR/qbittorrent/config mkdir -p $HSHQ_STACKS_DIR/shared/rdata/torrents/{audio,books,comics,misc,movies,software,tv} initServicesCredentials set +e outputConfigqBittorrent installStack qbittorrent qbittorrent "\[ls.io-init\] done" $HOME/qbittorrent.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$QBITTORRENT_INIT_ENV" = "true" ]; then sendEmail -s "qBittorrent Admin Login Info" -b "qBittorrent Admin Username: $QBITTORRENT_ADMIN_USERNAME\nqBittorrent Admin Password: $QBITTORRENT_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" QBITTORRENT_INIT_ENV=true updateConfigVar QBITTORRENT_INIT_ENV $QBITTORRENT_INIT_ENV fi sleep 3 echo "Stopping qbittorrent stack..." startStopStack qbittorrent stop set +e sed -i "/^\[BitTorrent\].*/a Session\\\\Preallocation=true" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf grep -q AutoDeleteAddedTorrentFile $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf if [ $? -eq 0 ]; then sed -i "s/^AutoDeleteAddedTorrentFile=.*/AutoDeleteAddedTorrentFile=IfAdded/" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf else echo -e "\n[Core]\nAutoDeleteAddedTorrentFile=IfAdded\n" >> $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf fi sed -i "s/^Session\\\\DefaultSavePath=.*/Session\\\\DefaultSavePath=\/data\/torrents\//" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[BitTorrent\].*/a Session\\\\DisableAutoTMMByDefault=false" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[BitTorrent\].*/a Session\\\\DisableAutoTMMTriggers\\\\CategorySavePathChanged=false" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[BitTorrent\].*/a Session\\\\DisableAutoTMMTriggers\\\\DefaultSavePathChanged=false" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[Preferences\].*/a MailNotification\\\\email=$EMAIL_ADMIN_EMAIL_ADDRESS" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[Preferences\].*/a MailNotification\\\\enabled=true" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[Preferences\].*/a MailNotification\\\\req_auth=false" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[Preferences\].*/a MailNotification\\\\req_ssl=false" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[Preferences\].*/a MailNotification\\\\sender=$EMAIL_ADMIN_EMAIL_ADDRESS" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[Preferences\].*/a MailNotification\\\\smtp_server=$SMTP_HOSTNAME" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf cat < /tmp/hashtmp.py import base64 import hashlib import secrets password='$QBITTORRENT_ADMIN_PASSWORD' salt='$(pwgen -c -n 16 1)' iterations=100000 key = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt.encode('utf-8'), iterations, dklen=64) print(base64.b64encode(key),base64.b64encode(salt.encode('utf-8'))); EOFPY qbittorrent_hashes=$(python3 /tmp/hashtmp.py) qbittorrent_pw_hash=$(echo "$qbittorrent_hashes" | cut -d" " -f1 | cut -d"'" -f2) qbittorrent_salt_hash=$(echo "$qbittorrent_hashes" | cut -d" " -f2 | cut -d"'" -f2) rm -f /tmp/hashtmp.py sed -i "/^\[Preferences\].*/a WebUI\\\\Password_PBKDF2=\"@ByteArray($qbittorrent_salt_hash:$qbittorrent_pw_hash)\"" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf sed -i "/^\[Preferences\].*/a WebUI\\\\Username=$QBITTORRENT_ADMIN_USERNAME" $HSHQ_STACKS_DIR/qbittorrent/config/qBittorrent/qBittorrent.conf echo "Starting qbittorrent stack..." startStopStack qbittorrent start set -e inner_block="" inner_block=$inner_block">>https://$SUB_QBITTORRENT.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://qbittorrent:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_QBITTORRENT $MANAGETLS_QBITTORRENT "$is_integrate_hshq" $NETDEFAULT_QBITTORRENT "$inner_block" insertSubAuthelia $SUB_QBITTORRENT.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll qbittorrent "$FMLNAME_QBITTORRENT" $USERTYPE_QBITTORRENT "https://$SUB_QBITTORRENT.$HOMESERVER_DOMAIN" "qbittorrent.png" "$(getHeimdallOrderFromSub $SUB_QBITTORRENT $USERTYPE_QBITTORRENT)" restartAllCaddyContainers fi } function outputConfigqBittorrent() { cat < $HOME/qbittorrent-compose.yml $STACK_VERSION_PREFIX qbittorrent $(getScriptStackVersion qbittorrent) services: qbittorrent: image: $(getScriptImageByContainerName qbittorrent) container_name: qbittorrent hostname: qbittorrent restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net ports: - "$QBITTORRENT_PORT:$QBITTORRENT_PORT" - "$QBITTORRENT_PORT:$QBITTORRENT_PORT/udp" volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/qbittorrent/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata/torrents:/data/torrents networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true EOFMT cat < $HOME/qbittorrent.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID EOFMT } function performUpdateqBittorrent() { perform_stack_name=qbittorrent prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=linuxserver/qbittorrent:5.1.2 image_update_map[0]="linuxserver/qbittorrent:5.1.2,mirror.gcr.io/linuxserver/qbittorrent:5.1.4" ;; 2) newVer=v2 curImageList=mirror.gcr.io/linuxserver/qbittorrent:5.1.4 image_update_map[0]="mirror.gcr.io/linuxserver/qbittorrent:5.1.4,mirror.gcr.io/linuxserver/qbittorrent:5.1.4" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Ombi function installOmbi() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory ombi "Ombi" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ombi-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/ombi mkdir $HSHQ_STACKS_DIR/ombi/config mkdir $HSHQ_STACKS_DIR/ombi/db mkdir $HSHQ_STACKS_DIR/ombi/dbexport chmod 777 $HSHQ_STACKS_DIR/ombi/dbexport initServicesCredentials set +e addUserMailu alias $OMBI_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS OMBI_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $OMBI_ADMIN_PASSWORD | tr -d ':\n') outputConfigOmbi installStack ombi ombi-app "\[ls.io-init\] done" $HOME/ombi.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$OMBI_INIT_ENV" = "true" ]; then sendEmail -s "Ombi Admin Login Info" -b "Ombi Admin Username: $OMBI_ADMIN_USERNAME\nOmbi Admin Password: $OMBI_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" OMBI_INIT_ENV=true updateConfigVar OMBI_INIT_ENV $OMBI_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase Ombi mysql ombi-db $OMBI_DATABASE_NAME root $OMBI_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/ombi/dbexport $OMBI_DATABASE_READONLYUSER $OMBI_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_OMBI_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://ombi-app:3579 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OMBI_APP $MANAGETLS_OMBI_APP "$is_integrate_hshq" $NETDEFAULT_OMBI_APP "$inner_block" insertSubAuthelia $SUB_OMBI_APP.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll ombi "$FMLNAME_OMBI_APP" $USERTYPE_OMBI_APP "https://$SUB_OMBI_APP.$HOMESERVER_DOMAIN" "ombi.png" "$(getHeimdallOrderFromSub $SUB_OMBI_APP $USERTYPE_OMBI_APP)" restartAllCaddyContainers checkAddDBConnection true ombi "$FMLNAME_OMBI_APP" mysql ombi-db $OMBI_DATABASE_NAME $OMBI_DATABASE_USER $OMBI_DATABASE_USER_PASSWORD fi set +e apik=$(docker exec -i ombi-db mysql -N -u $OMBI_DATABASE_USER -p$OMBI_DATABASE_USER_PASSWORD -e "use $OMBI_DATABASE_NAME; select Content from GlobalSettings where SettingsName='OmbiSettings';" | jq -r .ApiKey) curl -X "POST" -H "ApiKey: $apik" "https://$SUB_OMBI_APP.$HOMESERVER_DOMAIN/api/v1/Settings/notifications/email" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"notificationTemplates\": [],\"enabled\": true,\"host\": \"$SMTP_HOSTNAME\",\"port\": \"$SMTP_HOSTPORT\",\"senderName\":\"Ombi $(getAdminEmailName)\",\"senderAddress\": \"$EMAIL_ADMIN_EMAIL_ADDRESS\",\"authentication\": false,\"disableTLS\": false,\"disableCertificateChecking\": true}" > /dev/null 2>&1 set -e } function outputConfigOmbi() { cat < $HOME/ombi-compose.yml $STACK_VERSION_PREFIX ombi $(getScriptStackVersion ombi) services: ombi-db: image: $(getScriptImageByContainerName ombi-db) container_name: ombi-db hostname: ombi-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-ombi-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ombi-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ombi/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.ombi-hourly-db.schedule=@every 1h" - "ofelia.job-exec.ombi-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.ombi-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ombi-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ombi-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ombi-hourly-db.email-from=Ombi Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ombi-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.ombi-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.ombi-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.ombi-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ombi-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ombi-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ombi-monthly-db.email-from=Ombi Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ombi-monthly-db.mail-only-on-error=false" ombi-app: image: $(getScriptImageByContainerName ombi-app) container_name: ombi-app hostname: ombi-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ombi-db networks: - int-ombi-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ombi/config:/config volumes: v-ombi-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ombi/db networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-ombi-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/ombi.env TZ=\${PORTAINER_TZ} PUID=$USERID PGID=$GROUPID MYSQL_DATABASE=$OMBI_DATABASE_NAME MYSQL_ROOT_PASSWORD=$OMBI_DATABASE_ROOT_PASSWORD MYSQL_USER=$OMBI_DATABASE_USER MYSQL_PASSWORD=$OMBI_DATABASE_USER_PASSWORD EOFMT cat < $HSHQ_STACKS_DIR/ombi/config/database.json { "OmbiDatabase": { "Type": "MySQL", "ConnectionString": "Server=ombi-db;Port=3306;User ID=$OMBI_DATABASE_USER;Password=$OMBI_DATABASE_USER_PASSWORD;Database=$OMBI_DATABASE_NAME" }, "SettingsDatabase": { "Type": "MySQL", "ConnectionString": "Server=ombi-db;Port=3306;User ID=$OMBI_DATABASE_USER;Password=$OMBI_DATABASE_USER_PASSWORD;Database=$OMBI_DATABASE_NAME" }, "ExternalDatabase": { "Type": "MySQL", "ConnectionString": "Server=ombi-db;Port=3306;User ID=$OMBI_DATABASE_USER;Password=$OMBI_DATABASE_USER_PASSWORD;Database=$OMBI_DATABASE_NAME" } } EOFOB } function performUpdateOmbi() { perform_stack_name=ombi prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mariadb:10.7.3,linuxserver/ombi:4.47.1 image_update_map[0]="mariadb:10.7.3,mariadb:10.7.3" image_update_map[1]="linuxserver/ombi:4.47.1,linuxserver/ombi:4.47.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # MeshCentral function installMeshCentral() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory meshcentral "MeshCentral" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName meshcentral-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName meshcentral-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/meshcentral mkdir $HSHQ_STACKS_DIR/meshcentral/config mkdir $HSHQ_STACKS_DIR/meshcentral/db mkdir $HSHQ_STACKS_DIR/meshcentral/dbexport mkdir $HSHQ_STACKS_DIR/meshcentral/data mkdir $HSHQ_STACKS_DIR/meshcentral/files mkdir $HSHQ_STACKS_DIR/meshcentral/web mkdir $HSHQ_STACKS_DIR/meshcentral/backups chmod 777 $HSHQ_STACKS_DIR/meshcentral/dbexport initServicesCredentials set +e outputConfigMeshCentral installStack meshcentral meshcentral-app "MeshCentral HTTPS server running on" $HOME/meshcentral.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 3 addReadOnlyUserToDatabase MeshCentral mysql meshcentral-db $MESHCENTRAL_DATABASE_NAME root $MESHCENTRAL_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/meshcentral/dbexport $MESHCENTRAL_DATABASE_READONLYUSER $MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD if ! [ "$MESHCENTRAL_INIT_ENV" = "true" ]; then sendEmail -s "MeshCentral Admin Login Info" -b "MeshCentral Admin Username: $LDAP_ADMIN_USER_USERNAME\nMeshCentral Admin Password: $LDAP_ADMIN_USER_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" MESHCENTRAL_INIT_ENV=true updateConfigVar MESHCENTRAL_INIT_ENV $MESHCENTRAL_INIT_ENV fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://meshcentral-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>transport http {\n" inner_block=$inner_block">>>>>>>>>>tls\n" inner_block=$inner_block">>>>>>>>>>tls_insecure_skip_verify\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MESHCENTRAL $MANAGETLS_MESHCENTRAL "$is_integrate_hshq" $NETDEFAULT_MESHCENTRAL "$inner_block" insertSubAuthelia $SUB_MESHCENTRAL.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll meshcentral "$FMLNAME_MESHCENTRAL" $USERTYPE_MESHCENTRAL "https://$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN" "meshcentral.png" "$(getHeimdallOrderFromSub $SUB_MESHCENTRAL $USERTYPE_MESHCENTRAL)" restartAllCaddyContainers checkAddDBConnection true meshcentral "$FMLNAME_MESHCENTRAL" mysql meshcentral-db $MESHCENTRAL_DATABASE_NAME $MESHCENTRAL_DATABASE_USER $MESHCENTRAL_DATABASE_USER_PASSWORD fi } function outputConfigMeshCentral() { cat < $HOME/meshcentral-compose.yml $STACK_VERSION_PREFIX meshcentral $(getScriptStackVersion meshcentral) services: meshcentral-db: image: $(getScriptImageByContainerName meshcentral-db) container_name: meshcentral-db hostname: meshcentral-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-meshcentral-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-meshcentral-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/meshcentral/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.meshcentral-hourly-db.schedule=@every 1h" - "ofelia.job-exec.meshcentral-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.meshcentral-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.meshcentral-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.meshcentral-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.meshcentral-hourly-db.email-from=MeshCentral Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.meshcentral-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.meshcentral-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.meshcentral-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.meshcentral-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.meshcentral-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.meshcentral-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.meshcentral-monthly-db.email-from=MeshCentral Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.meshcentral-monthly-db.mail-only-on-error=false" environment: - MYSQL_DATABASE=$MESHCENTRAL_DATABASE_NAME - MYSQL_ROOT_PASSWORD=$MESHCENTRAL_DATABASE_ROOT_PASSWORD - MYSQL_USER=$MESHCENTRAL_DATABASE_USER - MYSQL_PASSWORD=$MESHCENTRAL_DATABASE_USER_PASSWORD meshcentral-app: image: $(getScriptImageByContainerName meshcentral-app) container_name: meshcentral-app hostname: meshcentral-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - meshcentral-db networks: - int-meshcentral-net - dock-internalmail-net - dock-ldap-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.crt:/opt/meshcentral/meshcentral-data/root-cert-public.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/${CERTS_ROOT_CA_NAME}.key:/opt/meshcentral/meshcentral-data/root-cert-private.key:ro - v-meshcentral-data:/opt/meshcentral/meshcentral-data - v-meshcentral-files:/opt/meshcentral/meshcentral-files - v-meshcentral-web:/opt/meshcentral/meshcentral-web - v-meshcentral-backups:/opt/meshcentral/meshcentral-backups volumes: v-meshcentral-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/meshcentral/db v-meshcentral-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/meshcentral/data v-meshcentral-files: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/meshcentral/files v-meshcentral-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/meshcentral/web v-meshcentral-backups: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/meshcentral/backups networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ldap-net: name: dock-ldap external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-meshcentral-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/meshcentral.env TZ=\${PORTAINER_TZ} NODE_ENV=production NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt DYNAMIC_CONFIG=false HOSTNAME=$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN EOFMT cat < $HSHQ_STACKS_DIR/meshcentral/data/config.json { "\$schema": "https://raw.githubusercontent.com/Ylianst/MeshCentral/master/meshcentral-config-schema.json", "settings": { "plugins":{ "enabled": false }, "cert": "$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN", "_WANonly": true, "_LANonly": true, "sessionKey": "$(pwgen -c -n 32 1)", "port": 443, "_aliasPort": 443, "redirPort": 80, "_redirAliasPort": 80, "AgentPong": 300, "trustedProxy": true, "SelfUpdate": false, "AllowFraming": false, "WebRTC": false, "_mongoDb": "", "_postgres": { "host": "", "port": "", "user": "", "password": "", "database": "" }, "mariaDB": { "host": "meshcentral-db", "port": "3306", "user": "$MESHCENTRAL_DATABASE_USER", "password": "$MESHCENTRAL_DATABASE_USER_PASSWORD", "database": "$MESHCENTRAL_DATABASE_NAME" } }, "domains": { "": { "title": "$HOMESERVER_NAME", "title2": "MeshCentral", "minify": false, "NewAccounts": true, "localSessionRecording": true, "certUrl": "https://$SUB_MESHCENTRAL.$HOMESERVER_DOMAIN/", "allowedOrigin": false, "auth": "ldap", "ldapUserName": "uid", "ldapUserKey": "uid", "ldapUserGroups": "memberOf", "ldapUserRequiredGroupMembership": [ "cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN" ], "ldapSiteAdminGroups": [ "cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN" ], "ldapSyncWithUserGroups": true, "ldapOptions": { "url": "$LDAPS_URI", "bindDN": "$LDAP_READONLY_USER_BIND_DN", "bindCredentials": "$LDAP_READONLY_USER_PASSWORD", "searchBase": "ou=people,$LDAP_BASE_DN", "searchFilter": "(uid={{username}})", "searchAttributes": [ "*", "memberOf" ] } } }, "smtp": { "host": "$SMTP_HOSTNAME", "port": $SMTP_HOSTPORT, "from": "$EMAIL_SMTP_EMAIL_ADDRESS", "tls": false, "verifyEmail": false } } EOFMT } function performUpdateMeshCentral() { perform_stack_name=meshcentral prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mariadb:10.7.3,ghcr.io/ylianst/meshcentral:1.1.48 image_update_map[0]="mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="ghcr.io/ylianst/meshcentral:1.1.48,ghcr.io/ylianst/meshcentral:1.1.53" ;; 2) # When upgrading to 1.1.54, ensure to add NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt to environment newVer=v2 curImageList=mirror.gcr.io/mariadb:10.7.3,ghcr.io/ylianst/meshcentral:1.1.53 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="ghcr.io/ylianst/meshcentral:1.1.53,ghcr.io/ylianst/meshcentral:1.1.53" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Navidrome function installNavidrome() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory navidrome "Navidrome" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName navidrome-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/navidrome mkdir $HSHQ_STACKS_DIR/navidrome/config mkdir $HSHQ_STACKS_DIR/navidrome/data initServicesCredentials set +e addUserMailu alias $NAVIDROME_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigNavidrome installStack navidrome navidrome-app "Navidrome server is ready" $HOME/navidrome.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$NAVIDROME_INIT_ENV" = "true" ]; then sendEmail -s "Navidrome Admin Login Info" -b "Navidrome Admin Username: $NAVIDROME_ADMIN_USERNAME\nNavidrome Admin Password: $NAVIDROME_ADMIN_PASSWORD\nNavidrome Admin Email: $NAVIDROME_ADMIN_EMAIL_ADDRESS\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" NAVIDROME_INIT_ENV=true updateConfigVar NAVIDROME_INIT_ENV $NAVIDROME_INIT_ENV fi sleep 3 set -e inner_block="" inner_block=$inner_block">>https://$SUB_NAVIDROME.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://navidrome-app:4533 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_NAVIDROME $MANAGETLS_NAVIDROME "$is_integrate_hshq" $NETDEFAULT_NAVIDROME "$inner_block" insertSubAuthelia $SUB_NAVIDROME.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll navidrome "$FMLNAME_NAVIDROME" $USERTYPE_NAVIDROME "https://$SUB_NAVIDROME.$HOMESERVER_DOMAIN" "navidrome.png" "$(getHeimdallOrderFromSub $SUB_NAVIDROME $USERTYPE_NAVIDROME)" restartAllCaddyContainers fi } function outputConfigNavidrome() { cat < $HOME/navidrome-compose.yml $STACK_VERSION_PREFIX navidrome $(getScriptStackVersion navidrome) services: navidrome-app: image: $(getScriptImageByContainerName navidrome-app) container_name: navidrome-app hostname: navidrome-app user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/navidrome/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata/media/audio:/sharedaudio:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true EOFMT cat < $HOME/navidrome.env TZ=\${PORTAINER_TZ} ND_ENABLEINSIGHTSCOLLECTOR=false ND_BASEURL=https://$SUB_NAVIDROME.$HOMESERVER_DOMAIN EOFMT } function performUpdateNavidrome() { perform_stack_name=navidrome prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=deluan/navidrome:0.58.0 image_update_map[0]="deluan/navidrome:0.58.0,mirror.gcr.io/deluan/navidrome:0.58.5" ;; 2) newVer=v2 curImageList=mirror.gcr.io/deluan/navidrome:0.58.5 image_update_map[0]="mirror.gcr.io/deluan/navidrome:0.58.5,mirror.gcr.io/deluan/navidrome:0.58.5" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Adminer function installAdminer() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory adminer "Adminer" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName adminer-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName adminer-db) if [ $? -ne 0 ]; then return 1 fi if [ -z "$SUB_ADMINER" ]; then echo "ERROR: Adminer subdomain undefined" return 1 fi set -e mkdir $HSHQ_STACKS_DIR/adminer mkdir $HSHQ_STACKS_DIR/adminer/db mkdir $HSHQ_STACKS_DIR/adminer/dbexport chmod 777 $HSHQ_STACKS_DIR/adminer/dbexport initServicesCredentials set +e outputConfigAdminer installStack adminer adminer-app "Development Server" $HOME/adminer.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 10 sendEmail -s "Adminer Info" -b "Adminer is capable of connecting to any database. It is connected to the private database network, so just enter the requisite login info. Additionally, an empty database has been created within the adminer stack. Here is the login information:\n\nSystem: MySQL/MariaDB\nServer: adminer-db\nUsername: $ADMINER_DATABASE_USER\nPassword: $ADMINER_DATABASE_USER_PASSWORD\nDatabase: $ADMINER_DATABASE_NAME\n\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" addReadOnlyUserToDatabase Adminer mysql adminer-db $ADMINER_DATABASE_NAME root $ADMINER_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/adminer/dbexport $ADMINER_DATABASE_READONLYUSER $ADMINER_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_ADMINER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://adminer-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ADMINER $MANAGETLS_ADMINER "$is_integrate_hshq" $NETDEFAULT_ADMINER "$inner_block" insertSubAuthelia $SUB_ADMINER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcHeimdall adminer "$FMLNAME_ADMINER" $USERTYPE_ADMINER "https://$SUB_ADMINER.$HOMESERVER_DOMAIN" "adminer.png" true "$(getHeimdallOrderFromSub $SUB_ADMINER $USERTYPE_ADMINER)" restartAllCaddyContainers checkAddDBConnection true adminer "$FMLNAME_ADMINER" mysql adminer-db $ADMINER_DATABASE_NAME $ADMINER_DATABASE_USER $ADMINER_DATABASE_USER_PASSWORD fi } function outputConfigAdminer() { cat < $HOME/adminer-compose.yml $STACK_VERSION_PREFIX adminer $(getScriptStackVersion adminer) services: adminer-db: image: $(getScriptImageByContainerName adminer-db) container_name: adminer-db hostname: adminer-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-adminer-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-adminer-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/adminer/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.adminer-hourly-db.schedule=@every 1h" - "ofelia.job-exec.adminer-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.adminer-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.adminer-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.adminer-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.adminer-hourly-db.email-from=Adminer Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.adminer-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.adminer-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.adminer-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.adminer-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.adminer-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.adminer-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.adminer-monthly-db.email-from=Adminer Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.adminer-monthly-db.mail-only-on-error=false" adminer-app: image: $(getScriptImageByContainerName adminer-app) container_name: adminer-app hostname: adminer-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - adminer-db configs: - source: adminer-index.php target: /var/www/html/index.php mode: 0755 networks: - int-adminer-net - dock-proxy-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro configs: adminer-index.php: content: | \$\$_ENV['ADMINER_DEFAULT_SERVER'], 'driver' => 'server', 'username' => \$\$_ENV['ADMINER_DEFAULT_USERNAME'], 'password' => \$\$_ENV['ADMINER_DEFAULT_PASSWORD'], 'db' => \$\$_ENV['ADMINER_DEFAULT_DB'], ]; } include './adminer.php'; volumes: v-adminer-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/adminer/db networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true int-adminer-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/adminer.env TZ=\${PORTAINER_TZ} ADMINER_DESIGN=galkaev ADMINER_DEFAULT_SERVER=adminer-db ADMINER_DEFAULT_USERNAME=$ADMINER_DATABASE_USER ADMINER_DEFAULT_PASSWORD=$ADMINER_DATABASE_USER_PASSWORD ADMINER_DEFAULT_DB=$ADMINER_DATABASE_NAME MYSQL_DATABASE=$ADMINER_DATABASE_NAME MYSQL_ROOT_PASSWORD=$ADMINER_DATABASE_ROOT_PASSWORD MYSQL_USER=$ADMINER_DATABASE_USER MYSQL_PASSWORD=$ADMINER_DATABASE_USER_PASSWORD EOFMT } function performUpdateAdminer() { perform_stack_name=adminer prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/adminer:5.3.0 image_update_map[0]="mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/mariadb:12.0.2-ubi10" image_update_map[1]="mirror.gcr.io/adminer:5.3.0,mirror.gcr.io/adminer:5.4.1" ;; 2) newVer=v2 curImageList=mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/adminer:5.4.1 image_update_map[0]="mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/mariadb:12.0.2-ubi10" image_update_map[1]="mirror.gcr.io/adminer:5.4.1,mirror.gcr.io/adminer:5.4.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Budibase function installBudibase() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory budibase "Budibase" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName budibase-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName budibase-couchdb) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName budibase-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName budibase-worker) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName budibase-minio) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName budibase-proxy) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName budibase-redis) if [ $? -ne 0 ]; then return 1 fi if [ -z "$SUB_BUDIBASE" ]; then echo "ERROR: Budibase subdomain undefined" return 1 fi set -e mkdir $HSHQ_STACKS_DIR/budibase mkdir $HSHQ_STACKS_DIR/budibase/db mkdir $HSHQ_STACKS_DIR/budibase/dbexport mkdir $HSHQ_STACKS_DIR/budibase/plugins mkdir $HSHQ_STACKS_DIR/budibase/couchdb mkdir $HSHQ_STACKS_DIR/budibase/minio chmod 777 $HSHQ_STACKS_DIR/budibase/dbexport initServicesCredentials set +e addUserMailu alias $ADMIN_USERNAME_BASE"_budibase" $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigBudibase installStack budibase budibase-app "Admin account automatically created" $HOME/budibase.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$BUDIBASE_INIT_ENV" = "true" ]; then sendEmail -s "Budibase Admin Login Info" -b "Budibase Admin Username: $BUDIBASE_ADMIN_EMAIL_ADDRESS\nBudibase Admin Password: $BUDIBASE_ADMIN_PASSWORD\nMYSQL_DATABASE_HOST=budibase-db\nMYSQL_DATABASE_PORT=3306\nMYSQL_USER=$BUDIBASE_DATABASE_USER\nMYSQL_PASSWORD=$BUDIBASE_DATABASE_USER_PASSWORD\nMYSQL_DATABASE_NAME=$BUDIBASE_DATABASE_NAME\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" BUDIBASE_INIT_ENV=true updateConfigVar BUDIBASE_INIT_ENV $BUDIBASE_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase Budibase mysql budibase-db $BUDIBASE_DATABASE_NAME root $BUDIBASE_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/budibase/dbexport $BUDIBASE_DATABASE_READONLYUSER $BUDIBASE_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_BUDIBASE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://budibase-proxy:10000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_BUDIBASE $MANAGETLS_BUDIBASE "$is_integrate_hshq" $NETDEFAULT_BUDIBASE "$inner_block" insertSubAuthelia $SUB_BUDIBASE.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll budibase "$FMLNAME_BUDIBASE" $USERTYPE_BUDIBASE "https://$SUB_BUDIBASE.$HOMESERVER_DOMAIN" "budibase.png" "$(getHeimdallOrderFromSub $SUB_BUDIBASE $USERTYPE_BUDIBASE)" restartAllCaddyContainers checkAddDBConnection true budibase "$FMLNAME_BUDIBASE" mysql budibase-db $BUDIBASE_DATABASE_NAME $BUDIBASE_DATABASE_USER $BUDIBASE_DATABASE_USER_PASSWORD fi } function outputConfigBudibase() { cat < $HOME/budibase-compose.yml $STACK_VERSION_PREFIX budibase $(getScriptStackVersion budibase) services: budibase-db: image: $(getScriptImageByContainerName budibase-db) container_name: budibase-db hostname: budibase-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-budibase-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-budibase-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/budibase/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.budibase-hourly-db.schedule=@every 1h" - "ofelia.job-exec.budibase-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.budibase-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.budibase-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.budibase-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.budibase-hourly-db.email-from=Budibase Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.budibase-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.budibase-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.budibase-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.budibase-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.budibase-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.budibase-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.budibase-monthly-db.email-from=Budibase Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.budibase-monthly-db.mail-only-on-error=false" budibase-couchdb: image: $(getScriptImageByContainerName budibase-couchdb) container_name: budibase-couchdb hostname: budibase-couchdb restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-budibase-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-budibase-couchdb:/opt/couchdb/data budibase-app: image: $(getScriptImageByContainerName budibase-app) container_name: budibase-app hostname: budibase-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - budibase-worker - budibase-redis networks: - int-budibase-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/budibase/plugins:/plugins environment: - PORT=4002 budibase-worker: image: $(getScriptImageByContainerName budibase-worker) container_name: budibase-worker hostname: budibase-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - budibase-minio - budibase-redis networks: - int-budibase-net - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro environment: - PORT=4003 budibase-minio: image: $(getScriptImageByContainerName budibase-minio) container_name: budibase-minio hostname: budibase-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data --console-address ":9001" networks: - int-budibase-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/budibase/minio:/data budibase-proxy: image: $(getScriptImageByContainerName budibase-proxy) container_name: budibase-proxy hostname: budibase-proxy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - budibase-minio - budibase-worker - budibase-app - budibase-couchdb networks: - int-budibase-net - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro budibase-redis: image: $(getScriptImageByContainerName budibase-redis) container_name: budibase-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $BUDIBASE_REDIS_PASSWORD --appendonly no networks: - int-budibase-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-budibase-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/budibase/db v-budibase-couchdb: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/budibase/couchdb networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-budibase-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/budibase.env TZ=\${PORTAINER_TZ} MYSQL_DATABASE=$BUDIBASE_DATABASE_NAME MYSQL_ROOT_PASSWORD=$BUDIBASE_DATABASE_ROOT_PASSWORD MYSQL_USER=$BUDIBASE_DATABASE_USER MYSQL_PASSWORD=$BUDIBASE_DATABASE_USER_PASSWORD NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt APPS_UPSTREAM_URL=http://budibase-app:4002 APPS_URL=http://budibase-app:4002 BB_ADMIN_USER_EMAIL=$BUDIBASE_ADMIN_EMAIL_ADDRESS BB_ADMIN_USER_PASSWORD=$BUDIBASE_ADMIN_PASSWORD CLUSTER_PORT=10000 COUCH_DB_URL=http://${BUDIBASE_COUCHDB_USER}:${BUDIBASE_COUCHDB_PASSWORD}@budibase-couchdb:5984 COUCH_DB_USERNAME=$BUDIBASE_COUCHDB_USER COUCH_DB_PASSWORD=$BUDIBASE_COUCHDB_PASSWORD COUCHDB_UPSTREAM_URL=http://budibase-couchdb:5984 COUCHDB_USER=$BUDIBASE_COUCHDB_USER COUCHDB_PASSWORD=$BUDIBASE_COUCHDB_PASSWORD ENABLE_ANALYTICS=false API_ENCRYPTION_KEY=$BUDIBASE_API_ENCRYPTION_KEY INTERNAL_API_KEY=$BUDIBASE_INTERNAL_API_KEY JWT_SECRET=$BUDIBASE_JWT_SECRET LOG_LEVEL=info MINIO_BROWSER=on MINIO_ACCESS_KEY=$BUDIBASE_MINIO_ACCESS_KEY MINIO_SECRET_KEY=$BUDIBASE_MINIO_SECRET_KEY MINIO_UPSTREAM_URL=http://budibase-minio:9000 MINIO_URL=http://budibase-minio:9000 PROXY_RATE_LIMIT_API_PER_SECOND=20 PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10 REDIS_PASSWORD=$BUDIBASE_REDIS_PASSWORD REDIS_URL=budibase-redis:6379 RESOLVER=127.0.0.11 SELF_HOSTED=1 TARGETBUILD=docker-compose WORKER_UPSTREAM_URL=http://budibase-worker:4003 WORKER_URL=http://budibase-worker:4003 APP_PORT=4002 WORKER_PORT=4003 MINIO_PORT=4004 COUCH_DB_PORT=4005 COUCH_DB_SQS_PORT=4006 REDIS_PORT=6379 BUDIBASE_ENVIRONMENT=PRODUCTION SQL_MAX_ROWS= OFFLINE_MODE= PLUGINS_DIR=/plugins ROLLING_LOG_MAX_SIZE= EOFMT } function performUpdateBudibase() { perform_stack_name=budibase prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/budibase/apps:3.15.0,mirror.gcr.io/budibase/worker:3.15.0,mirror.gcr.io/budibase/proxy:3.15.0,mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/mariadb:12.0.2-ubi10 image_update_map[0]="mirror.gcr.io/budibase/apps:3.15.0,mirror.gcr.io/budibase/apps:3.23.36" image_update_map[1]="mirror.gcr.io/budibase/worker:3.15.0,mirror.gcr.io/budibase/worker:3.23.36" image_update_map[2]="mirror.gcr.io/budibase/proxy:3.15.0,mirror.gcr.io/budibase/proxy:3.23.36" image_update_map[3]="mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1,mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1" image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[5]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[6]="mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/mariadb:12.0.2-ubi10" ;; 2) newVer=v3 curImageList=mirror.gcr.io/budibase/apps:3.23.36,mirror.gcr.io/budibase/worker:3.23.36,mirror.gcr.io/budibase/proxy:3.23.36,mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/mariadb:12.0.2-ubi10 image_update_map[0]="mirror.gcr.io/budibase/apps:3.23.36,mirror.gcr.io/budibase/apps:3.23.36" image_update_map[1]="mirror.gcr.io/budibase/worker:3.23.36,mirror.gcr.io/budibase/worker:3.23.36" image_update_map[2]="mirror.gcr.io/budibase/proxy:3.23.36,mirror.gcr.io/budibase/proxy:3.23.36" image_update_map[3]="mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1,mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1" image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" image_update_map[5]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[6]="mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/mariadb:12.0.2-ubi10" ;; 3) newVer=v3 curImageList=mirror.gcr.io/budibase/apps:3.23.36,mirror.gcr.io/budibase/worker:3.23.36,mirror.gcr.io/budibase/proxy:3.23.36,mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1,ghcr.io/homeserverhq/minio:v1.0,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/mariadb:12.0.2-ubi10 image_update_map[0]="mirror.gcr.io/budibase/apps:3.23.36,mirror.gcr.io/budibase/apps:3.23.36" image_update_map[1]="mirror.gcr.io/budibase/worker:3.23.36,mirror.gcr.io/budibase/worker:3.23.36" image_update_map[2]="mirror.gcr.io/budibase/proxy:3.23.36,mirror.gcr.io/budibase/proxy:3.23.36" image_update_map[3]="mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1,mirror.gcr.io/budibase/couchdb:v3.3.3-sqs-v2.1.1" image_update_map[4]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" image_update_map[5]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[6]="mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/mariadb:12.0.2-ubi10" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # AudioBookshelf function installAudioBookshelf() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory audiobookshelf "AudioBookshelf" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName audiobookshelf-app) if [ $? -ne 0 ]; then return 1 fi installSQLite344 if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/audiobookshelf mkdir $HSHQ_STACKS_DIR/audiobookshelf/{config,metadata,audiobooks,podcasts} initServicesCredentials set +e addUserMailu alias $AUDIOBOOKSHELF_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS AUDIOBOOKSHELF_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 8 "" $AUDIOBOOKSHELF_ADMIN_PASSWORD | tr -d ':\n' | sed 's/$2y/$2a/') AUDIOBOOKSHELF_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $AUDIOBOOKSHELF_OIDC_CLIENT_SECRET | tr -d ':\n') outputConfigAudioBookshelf oidcBlock=$(cat $HOME/audiobookshelf.oidc) rm -f $HOME/audiobookshelf.oidc insertOIDCClientAuthelia audiobookshelf "$oidcBlock" installStack audiobookshelf audiobookshelf-app "Listening on port" $HOME/audiobookshelf.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$AUDIOBOOKSHELF_INIT_ENV" = "true" ]; then sendEmail -s "AudioBookshelf Admin Login Info" -b "AudioBookshelf Admin Username: $AUDIOBOOKSHELF_ADMIN_USERNAME\nAudioBookshelf Admin Password: $AUDIOBOOKSHELF_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" AUDIOBOOKSHELF_INIT_ENV=true updateConfigVar AUDIOBOOKSHELF_INIT_ENV $AUDIOBOOKSHELF_INIT_ENV fi sleep 3 admin_uuid=$(uuidgen) header_jwt=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 -w 0 | sed s/\+/-/ | sed -E s/=+$//) #echo "Header JWT: $header_jwt" admin_jwt=$(echo -n "{\"userId\":\"$admin_uuid\",\"username\":\"$AUDIOBOOKSHELF_ADMIN_USERNAME\",\"iat\":$(date +%s)}" | base64 -w 0 | sed s/\+/-/ | sed -E s/=+$// | sed s/\\n//) #echo "Admin JWT: $admin_jwt" hmac_jwt=$(echo -n "${header_jwt}.${admin_jwt}" | openssl dgst -sha256 -hmac $AUDIOBOOKSHELF_JWT_SECRET -binary | openssl base64 -e -A | sed s/\+/-/ | sed -E s/=+$//) #echo "Signed JWT ($AUDIOBOOKSHELF_JWT_SECRET): $hmac_jwt" full_jwt="${header_jwt}.${admin_jwt}.${hmac_jwt}" #echo "Full JWT: $full_jwt" adm_perms="{\"download\":true,\"update\":true,\"delete\":true,\"upload\":true,\"createEreader\":true,\"accessAllLibraries\":true,\"accessAllTags\":true,\"accessExplicitContent\":true,\"selectedTagsNotAccessible\":false,\"librariesAccessible\":[],\"itemTagsSelected\":[]}" adm_extra="{\"seriesHideFromContinueListening\":[]}" dtNow=$(date '+%Y-%m-%d %H:%M:%S.%3N %z') sudo $HSHQ_NONBACKUP_DIR/sqlite344/sqlite3 $HSHQ_STACKS_DIR/audiobookshelf/config/absdatabase.sqlite "insert into users(id,username,email,pash,type,token,isActive,isLocked,lastSeen,permissions,bookmarks,extraData,createdAt,updatedAt) values('$admin_uuid','$AUDIOBOOKSHELF_ADMIN_USERNAME','$AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS','$AUDIOBOOKSHELF_ADMIN_PASSWORD_HASH','root','$full_jwt',1,0,'$dtNow','$adm_perms','[]','$adm_extra','$dtNow','$dtNow');" abs_set="{\"id\":\"server-settings\",\"tokenSecret\":null,\"scannerFindCovers\":false,\"scannerCoverProvider\":\"google\",\"scannerParseSubtitle\":false,\"scannerPreferMatchedMetadata\":false,\"scannerDisableWatcher\":false,\"storeCoverWithItem\":false,\"storeMetadataWithItem\":false,\"metadataFileFormat\":\"json\",\"rateLimitLoginRequests\":10,\"rateLimitLoginWindow\":600000,\"allowIframe\":false,\"backupPath\":\"/metadata/backups\",\"backupSchedule\":false,\"backupsToKeep\":2,\"maxBackupSize\":1,\"loggerDailyLogsToKeep\":7,\"loggerScannerLogsToKeep\":2,\"homeBookshelfView\":1,\"bookshelfView\":1,\"podcastEpisodeSchedule\":\"0 * * * *\",\"sortingIgnorePrefix\":false,\"sortingPrefixes\":[\"the\",\"a\"],\"chromecastEnabled\":false,\"dateFormat\":\"MM/dd/yyyy\",\"timeFormat\":\"HH:mm\",\"language\":\"en-us\",\"allowedOrigins\":[],\"logLevel\":2,\"version\":\"2.28.0\",\"buildNumber\":1,\"authLoginCustomMessage\":null,\"authActiveAuthMethods\":[\"local\",\"openid\"],\"authOpenIDIssuerURL\":\"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN\",\"authOpenIDAuthorizationURL\":\"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/api/oidc/authorization\",\"authOpenIDTokenURL\":\"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/api/oidc/token\",\"authOpenIDUserInfoURL\":\"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/api/oidc/userinfo\",\"authOpenIDJwksURL\":\"https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/jwks.json\",\"authOpenIDLogoutURL\":null,\"authOpenIDClientID\":\"audiobookshelf\",\"authOpenIDClientSecret\":\"$AUDIOBOOKSHELF_OIDC_CLIENT_SECRET\",\"authOpenIDTokenSigningAlgorithm\":\"RS256\",\"authOpenIDButtonText\":\"Login with $FMLNAME_AUTHELIA\",\"authOpenIDAutoLaunch\":false,\"authOpenIDAutoRegister\":true,\"authOpenIDMatchExistingBy\":\"username\",\"authOpenIDMobileRedirectURIs\":[\"audiobookshelf://oauth\"],\"authOpenIDGroupClaim\":\"\",\"authOpenIDAdvancedPermsClaim\":\"\",\"authOpenIDSubfolderForRedirectURLs\":\"\"}" sudo $HSHQ_NONBACKUP_DIR/sqlite344/sqlite3 $HSHQ_STACKS_DIR/audiobookshelf/config/absdatabase.sqlite "delete from settings where key='server-settings';" sudo $HSHQ_NONBACKUP_DIR/sqlite344/sqlite3 $HSHQ_STACKS_DIR/audiobookshelf/config/absdatabase.sqlite "insert into settings(key,value,createdAt,updatedAt) values ('server-settings','$abs_set','$dtNow','$dtNow');" mail_set="{\"id\":\"email-settings\",\"host\":\"$SMTP_HOSTNAME\",\"port\":$SMTP_HOSTPORT,\"secure\":true,\"rejectUnauthorized\":true,\"user\":null,\"pass\":null,\"testAddress\":\"$AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS\",\"fromAddress\":\"Audiobookshelf $(getAdminEmailName) <$AUDIOBOOKSHELF_ADMIN_EMAIL_ADDRESS>\",\"ereaderDevices\":[]}" sudo $HSHQ_NONBACKUP_DIR/sqlite344/sqlite3 $HSHQ_STACKS_DIR/audiobookshelf/config/absdatabase.sqlite "delete from settings where key='email-settings';" sudo $HSHQ_NONBACKUP_DIR/sqlite344/sqlite3 $HSHQ_STACKS_DIR/audiobookshelf/config/absdatabase.sqlite "insert into settings(key,value,createdAt,updatedAt) values ('email-settings','$mail_set','$dtNow','$dtNow');" docker container restart audiobookshelf-app > /dev/null 2>&1 set -e inner_block="" inner_block=$inner_block">>https://$SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://audiobookshelf-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AUDIOBOOKSHELF $MANAGETLS_AUDIOBOOKSHELF "$is_integrate_hshq" $NETDEFAULT_AUDIOBOOKSHELF "$inner_block" insertSubAuthelia $SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll audiobookshelf "$FMLNAME_AUDIOBOOKSHELF" $USERTYPE_AUDIOBOOKSHELF "https://$SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN" "audiobookshelf.png" "$(getHeimdallOrderFromSub $SUB_AUDIOBOOKSHELF $USERTYPE_AUDIOBOOKSHELF)" restartAllCaddyContainers fi } function outputConfigAudioBookshelf() { cat < $HOME/audiobookshelf-compose.yml $STACK_VERSION_PREFIX audiobookshelf $(getScriptStackVersion audiobookshelf) services: audiobookshelf-app: image: $(getScriptImageByContainerName audiobookshelf-app) container_name: audiobookshelf-app hostname: audiobookshelf-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-internalmail-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/audiobookshelf/audiobooks:/audiobooks - \${PORTAINER_HSHQ_STACKS_DIR}/audiobookshelf/podcasts:/podcasts - \${PORTAINER_HSHQ_STACKS_DIR}/audiobookshelf/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/audiobookshelf/metadata:/metadata - \${PORTAINER_HSHQ_STACKS_DIR}/shared/rdata/media/audio:/sharedaudio:ro networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true EOFMT cat < $HOME/audiobookshelf.env TZ=\${PORTAINER_TZ} JWT_SECRET_KEY=$AUDIOBOOKSHELF_JWT_SECRET NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt EOFMT rm -f $HOME/audiobookshelf.oidc cat < $HOME/audiobookshelf.oidc # Authelia OIDC Client audiobookshelf BEGIN - client_id: audiobookshelf client_name: audiobookshelf client_secret: $AUDIOBOOKSHELF_OIDC_CLIENT_SECRET_HASH public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth require_pkce: true pkce_challenge_method: S256 redirect_uris: - https://$SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN/auth/openid/callback - https://$SUB_AUDIOBOOKSHELF.$HOMESERVER_DOMAIN/auth/openid/mobile-redirect - audiobookshelf://oauth scopes: - openid - profile - groups - email response_types: - code grant_types: - authorization_code access_token_signed_response_alg: none userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client audiobookshelf END EOFIM } function performUpdateAudioBookshelf() { perform_stack_name=audiobookshelf prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=ghcr.io/advplyr/audiobookshelf:2.28.0 image_update_map[0]="ghcr.io/advplyr/audiobookshelf:2.28.0,ghcr.io/advplyr/audiobookshelf:2.31.0" ;; 2) newVer=v2 curImageList=ghcr.io/advplyr/audiobookshelf:2.31.0 image_update_map[0]="ghcr.io/advplyr/audiobookshelf:2.31.0,ghcr.io/advplyr/audiobookshelf:2.31.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # StandardNotes function installStandardNotes() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory standardnotes "StandardNotes" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName standardnotes-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName standardnotes-server) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName standardnotes-localstack) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName standardnotes-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName standardnotes-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/standardnotes mkdir $HSHQ_STACKS_DIR/standardnotes/{db,dbexport,config,localstack,logs,uploads} chmod 777 $HSHQ_STACKS_DIR/standardnotes/dbexport initServicesCredentials set +e outputConfigStandardNotes installStack standardnotes standardnotes-server "syncing-server-worker entered RUNNING state" $HOME/standardnotes.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sendEmail -s "StandardNotes Setup Info" -b "To use your own locally hosted StandardNotes server instance, ensure to select Custom when creating your account (bottom left corner in web interface. Then for Sync Server, enter:\n\n https://$SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN \n\nThis will store all of your data on your own server. Ensure to do this for any other apps/devices.\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" sleep 3 addReadOnlyUserToDatabase StandardNotes mysql standardnotes-db $STANDARDNOTES_DATABASE_NAME root $STANDARDNOTES_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/standardnotes/dbexport $STANDARDNOTES_DATABASE_READONLYUSER $STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://standardnotes-server:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_STANDARDNOTES_SERVER $MANAGETLS_STANDARDNOTES_SERVER "$is_integrate_hshq" $NETDEFAULT_STANDARDNOTES_SERVER "$inner_block" insertSubAuthelia $SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_STANDARDNOTES_FILES.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://standardnotes-server:3104 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_STANDARDNOTES_FILES $MANAGETLS_STANDARDNOTES_FILES "$is_integrate_hshq" $NETDEFAULT_STANDARDNOTES_FILES "$inner_block" insertSubAuthelia $SUB_STANDARDNOTES_FILES.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_STANDARDNOTES_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://standardnotes-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_STANDARDNOTES_WEB $MANAGETLS_STANDARDNOTES_WEB "$is_integrate_hshq" $NETDEFAULT_STANDARDNOTES_WEB "$inner_block" insertSubAuthelia $SUB_STANDARDNOTES_WEB.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll standardnotes "$FMLNAME_STANDARDNOTES_WEB" $USERTYPE_STANDARDNOTES_WEB "https://$SUB_STANDARDNOTES_WEB.$HOMESERVER_DOMAIN" "standardnotes.png" "$(getHeimdallOrderFromSub $SUB_STANDARDNOTES_WEB $USERTYPE_STANDARDNOTES_WEB)" insertEnableSvcUptimeKuma standardnotes "$FMLNAME_STANDARDNOTES_SERVER" $USERTYPE_STANDARDNOTES_SERVER "https://$SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN" true restartAllCaddyContainers checkAddDBConnection true standardnotes "$FMLNAME_STANDARDNOTES_SERVER" mysql standardnotes-db $STANDARDNOTES_DATABASE_NAME $STANDARDNOTES_DATABASE_USER $STANDARDNOTES_DATABASE_USER_PASSWORD fi } function outputConfigStandardNotes() { cat < $HOME/standardnotes-compose.yml $STACK_VERSION_PREFIX standardnotes $(getScriptStackVersion standardnotes) services: standardnotes-db: image: $(getScriptImageByContainerName standardnotes-db) container_name: standardnotes-db hostname: standardnotes-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-standardnotes-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-standardnotes-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/standardnotes/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.standardnotes-hourly-db.schedule=@every 1h" - "ofelia.job-exec.standardnotes-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.standardnotes-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.standardnotes-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.standardnotes-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.standardnotes-hourly-db.email-from=StandardNotes Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.standardnotes-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.standardnotes-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.standardnotes-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.standardnotes-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.standardnotes-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.standardnotes-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.standardnotes-monthly-db.email-from=StandardNotes Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.standardnotes-monthly-db.mail-only-on-error=false" standardnotes-server: image: $(getScriptImageByContainerName standardnotes-server) container_name: standardnotes-server hostname: standardnotes-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - standardnotes-db networks: - int-standardnotes-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/standardnotes/logs:/var/lib/server/logs - \${PORTAINER_HSHQ_STACKS_DIR}/standardnotes/uploads:/opt/server/packages/files/dist/uploads localstack: image: $(getScriptImageByContainerName standardnotes-localstack) container_name: standardnotes-localstack hostname: localstack restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - standardnotes-db networks: - int-standardnotes-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/standardnotes/localstack/localstack_bootstrap.sh:/etc/localstack/init/ready.d/localstack_bootstrap.sh standardnotes-web: image: $(getScriptImageByContainerName standardnotes-web) container_name: standardnotes-web hostname: standardnotes-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-standardnotes-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro standardnotes-redis: image: $(getScriptImageByContainerName standardnotes-redis) container_name: standardnotes-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $STANDARDNOTES_REDIS_PASSWORD --appendonly no networks: - int-standardnotes-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-standardnotes-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/standardnotes/db networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-standardnotes-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/standardnotes.env TZ=\${PORTAINER_TZ} DB_HOST=standardnotes-db DB_PORT=3306 DB_USERNAME=$STANDARDNOTES_DATABASE_USER DB_PASSWORD=$STANDARDNOTES_DATABASE_USER_PASSWORD DB_DATABASE=$STANDARDNOTES_DATABASE_NAME DB_TYPE=mysql MYSQL_DATABASE=$STANDARDNOTES_DATABASE_NAME MYSQL_ROOT_PASSWORD=$STANDARDNOTES_DATABASE_ROOT_PASSWORD MYSQL_USER=$STANDARDNOTES_DATABASE_USER MYSQL_PASSWORD=$STANDARDNOTES_DATABASE_USER_PASSWORD REDIS_PORT=6379 REDIS_HOST=standardnotes-redis CACHE_TYPE=redis REDIS_URL=redis://:$STANDARDNOTES_REDIS_PASSWORD@standardnotes-redis:6379/0 AUTH_JWT_SECRET=$(openssl rand -hex 32) AUTH_SERVER_ENCRYPTION_SERVER_KEY=$(openssl rand -hex 32) VALET_TOKEN_SECRET=$(openssl rand -hex 32) PUBLIC_FILES_SERVER_URL=https://$SUB_STANDARDNOTES_FILES.$HOMESERVER_DOMAIN COOKIE_DOMAIN=$HOMESERVER_DOMAIN DEFAULT_SYNC_SERVER=https://$SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN SF_DEFAULT_SERVER=https://$SUB_STANDARDNOTES_SERVER.$HOMESERVER_DOMAIN SERVICES=sns,sqs HOSTNAME_EXTERNAL=localstack LS_LOG=warn EOFMT cat < $HSHQ_STACKS_DIR/standardnotes/localstack/localstack_bootstrap.sh #!/usr/bin/env bash set -euo pipefail echo "configuring sns/sqs" echo "===================" LOCALSTACK_HOST=localhost AWS_REGION=us-east-1 LOCALSTACK_DUMMY_ID=000000000000 get_all_queues() { awslocal --endpoint-url=http://\${LOCALSTACK_HOST}:4566 sqs list-queues } create_queue() { local QUEUE_NAME_TO_CREATE=\$1 awslocal --endpoint-url=http://\${LOCALSTACK_HOST}:4566 sqs create-queue --queue-name \${QUEUE_NAME_TO_CREATE} } get_all_topics() { awslocal --endpoint-url=http://\${LOCALSTACK_HOST}:4566 sns list-topics } create_topic() { local TOPIC_NAME_TO_CREATE=\$1 awslocal --endpoint-url=http://\${LOCALSTACK_HOST}:4566 sns create-topic --name \${TOPIC_NAME_TO_CREATE} } link_queue_and_topic() { local TOPIC_ARN_TO_LINK=\$1 local QUEUE_ARN_TO_LINK=\$2 awslocal --endpoint-url=http://\${LOCALSTACK_HOST}:4566 sns subscribe --topic-arn \${TOPIC_ARN_TO_LINK} --protocol sqs --notification-endpoint \${QUEUE_ARN_TO_LINK} } get_queue_arn_from_name() { local QUEUE_NAME=\$1 echo "arn:aws:sns:\${AWS_REGION}:\${LOCALSTACK_DUMMY_ID}:\$QUEUE_NAME" } get_topic_arn_from_name() { local TOPIC_NAME=\$1 echo "arn:aws:sns:\${AWS_REGION}:\${LOCALSTACK_DUMMY_ID}:\$TOPIC_NAME" } PAYMENTS_TOPIC_NAME="payments-local-topic" echo "creating topic \$PAYMENTS_TOPIC_NAME" TOPIC_CREATED_RESULT=\$(create_topic \${PAYMENTS_TOPIC_NAME}) echo "created topic: \$TOPIC_CREATED_RESULT" PAYMENTS_TOPIC_ARN=\$(get_topic_arn_from_name \$PAYMENTS_TOPIC_NAME) SYNCING_SERVER_TOPIC_NAME="syncing-server-local-topic" echo "creating topic \$SYNCING_SERVER_TOPIC_NAME" TOPIC_CREATED_RESULT=\$(create_topic \${SYNCING_SERVER_TOPIC_NAME}) echo "created topic: \$TOPIC_CREATED_RESULT" SYNCING_SERVER_TOPIC_ARN=\$(get_topic_arn_from_name \$SYNCING_SERVER_TOPIC_NAME) AUTH_TOPIC_NAME="auth-local-topic" echo "creating topic \$AUTH_TOPIC_NAME" TOPIC_CREATED_RESULT=\$(create_topic \${AUTH_TOPIC_NAME}) echo "created topic: \$TOPIC_CREATED_RESULT" AUTH_TOPIC_ARN=\$(get_topic_arn_from_name \$AUTH_TOPIC_NAME) FILES_TOPIC_NAME="files-local-topic" echo "creating topic \$FILES_TOPIC_NAME" TOPIC_CREATED_RESULT=\$(create_topic \${FILES_TOPIC_NAME}) echo "created topic: \$TOPIC_CREATED_RESULT" FILES_TOPIC_ARN=\$(get_topic_arn_from_name \$FILES_TOPIC_NAME) ANALYTICS_TOPIC_NAME="analytics-local-topic" echo "creating topic \$ANALYTICS_TOPIC_NAME" TOPIC_CREATED_RESULT=\$(create_topic \${ANALYTICS_TOPIC_NAME}) echo "created topic: \$TOPIC_CREATED_RESULT" ANALYTICS_TOPIC_ARN=\$(get_topic_arn_from_name \$ANALYTICS_TOPIC_NAME) REVISIONS_TOPIC_NAME="revisions-server-local-topic" echo "creating topic \$REVISIONS_TOPIC_NAME" TOPIC_CREATED_RESULT=\$(create_topic \${REVISIONS_TOPIC_NAME}) echo "created topic: \$TOPIC_CREATED_RESULT" REVISIONS_TOPIC_ARN=\$(get_topic_arn_from_name \$REVISIONS_TOPIC_NAME) SCHEDULER_TOPIC_NAME="scheduler-local-topic" echo "creating topic \$SCHEDULER_TOPIC_NAME" TOPIC_CREATED_RESULT=\$(create_topic \${SCHEDULER_TOPIC_NAME}) echo "created topic: \$TOPIC_CREATED_RESULT" SCHEDULER_TOPIC_ARN=\$(get_topic_arn_from_name \$SCHEDULER_TOPIC_NAME) QUEUE_NAME="analytics-local-queue" echo "creating queue \$QUEUE_NAME" QUEUE_URL=\$(create_queue \${QUEUE_NAME}) echo "created queue: \$QUEUE_URL" ANALYTICS_QUEUE_ARN=\$(get_queue_arn_from_name \$QUEUE_NAME) echo "linking topic \$PAYMENTS_TOPIC_ARN to queue \$ANALYTICS_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$PAYMENTS_TOPIC_ARN \$ANALYTICS_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" QUEUE_NAME="auth-local-queue" echo "creating queue \$QUEUE_NAME" QUEUE_URL=\$(create_queue \${QUEUE_NAME}) echo "created queue: \$QUEUE_URL" AUTH_QUEUE_ARN=\$(get_queue_arn_from_name \$QUEUE_NAME) echo "linking topic \$PAYMENTS_TOPIC_ARN to queue \$AUTH_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$PAYMENTS_TOPIC_ARN \$AUTH_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$AUTH_TOPIC_ARN to queue \$AUTH_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$AUTH_TOPIC_ARN \$AUTH_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$FILES_TOPIC_ARN to queue \$AUTH_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$FILES_TOPIC_ARN \$AUTH_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$REVISIONS_TOPIC_ARN to queue \$AUTH_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$REVISIONS_TOPIC_ARN \$AUTH_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" QUEUE_NAME="files-local-queue" echo "creating queue \$QUEUE_NAME" QUEUE_URL=\$(create_queue \${QUEUE_NAME}) echo "created queue: \$QUEUE_URL" FILES_QUEUE_ARN=\$(get_queue_arn_from_name \$QUEUE_NAME) echo "linking topic \$AUTH_TOPIC_ARN to queue \$FILES_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$AUTH_TOPIC_ARN \$FILES_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$SYNCING_SERVER_TOPIC_ARN to queue \$FILES_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$SYNCING_SERVER_TOPIC_ARN \$FILES_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" QUEUE_NAME="syncing-server-local-queue" echo "creating queue \$QUEUE_NAME" QUEUE_URL=\$(create_queue \${QUEUE_NAME}) echo "created queue: \$QUEUE_URL" SYNCING_SERVER_QUEUE_ARN=\$(get_queue_arn_from_name \$QUEUE_NAME) echo "linking topic \$SYNCING_SERVER_TOPIC_ARN to queue \$SYNCING_SERVER_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$SYNCING_SERVER_TOPIC_ARN \$SYNCING_SERVER_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$FILES_TOPIC_ARN to queue \$SYNCING_SERVER_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$FILES_TOPIC_ARN \$SYNCING_SERVER_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$SYNCING_SERVER_TOPIC_ARN to queue \$AUTH_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$SYNCING_SERVER_TOPIC_ARN \$AUTH_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$AUTH_TOPIC_ARN to queue \$SYNCING_SERVER_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$AUTH_TOPIC_ARN \$SYNCING_SERVER_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" QUEUE_NAME="revisions-server-local-queue" echo "creating queue \$QUEUE_NAME" QUEUE_URL=\$(create_queue \${QUEUE_NAME}) echo "created queue: \$QUEUE_URL" REVISIONS_QUEUE_ARN=\$(get_queue_arn_from_name \$QUEUE_NAME) echo "linking topic \$SYNCING_SERVER_TOPIC_ARN to queue \$REVISIONS_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$SYNCING_SERVER_TOPIC_ARN \$REVISIONS_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" echo "linking topic \$REVISIONS_TOPIC_ARN to queue \$REVISIONS_QUEUE_ARN" LINKING_RESULT=\$(link_queue_and_topic \$REVISIONS_TOPIC_ARN \$REVISIONS_QUEUE_ARN) echo "linking done:" echo "\$LINKING_RESULT" QUEUE_NAME="scheduler-local-queue" echo "creating queue \$QUEUE_NAME" QUEUE_URL=\$(create_queue \${QUEUE_NAME}) echo "created queue: \$QUEUE_URL" SCHEDULER_QUEUE_ARN=\$(get_queue_arn_from_name \$QUEUE_NAME) echo "all topics are:" echo "\$(get_all_topics)" echo "all queues are:" echo "\$(get_all_queues)" EOFLB chmod 700 $HSHQ_STACKS_DIR/standardnotes/localstack/localstack_bootstrap.sh } function performUpdateStandardNotes() { perform_stack_name=standardnotes prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/mariadb:10.7.3,standardnotes/server:0d82819cba9694bc9fb5a3fa53e2dbeda05d1242,localstack/localstack:4.7.0,standardnotes/web:b9d7f368846e9399b27e50697b6e59263befe431,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="standardnotes/server:0d82819cba9694bc9fb5a3fa53e2dbeda05d1242,standardnotes/server:0d82819cba9694bc9fb5a3fa53e2dbeda05d1242" image_update_map[2]="localstack/localstack:4.7.0,mirror.gcr.io/localstack/localstack:4.11.1" image_update_map[3]="standardnotes/web:b9d7f368846e9399b27e50697b6e59263befe431,mirror.gcr.io/standardnotes/web:3190a278314fe788f9136528971c0a9cd0dffbf4" image_update_map[4]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 2) newVer=v2 curImageList=mirror.gcr.io/mariadb:10.7.3,standardnotes/server:0d82819cba9694bc9fb5a3fa53e2dbeda05d1242,mirror.gcr.io/localstack/localstack:4.11.1,mirror.gcr.io/standardnotes/web:3190a278314fe788f9136528971c0a9cd0dffbf4,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="standardnotes/server:0d82819cba9694bc9fb5a3fa53e2dbeda05d1242,standardnotes/server:0d82819cba9694bc9fb5a3fa53e2dbeda05d1242" image_update_map[2]="mirror.gcr.io/localstack/localstack:4.11.1,mirror.gcr.io/localstack/localstack:4.11.1" image_update_map[3]="mirror.gcr.io/standardnotes/web:3190a278314fe788f9136528971c0a9cd0dffbf4,mirror.gcr.io/standardnotes/web:3190a278314fe788f9136528971c0a9cd0dffbf4" image_update_map[4]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Metabase function installMetabase() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory metabase "Metabase" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName metabase-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName metabase-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/metabase mkdir $HSHQ_STACKS_DIR/metabase/config mkdir $HSHQ_STACKS_DIR/metabase/db mkdir $HSHQ_STACKS_DIR/metabase/dbimport mkdir $HSHQ_STACKS_DIR/metabase/dbexport chmod 777 $HSHQ_STACKS_DIR/metabase/dbexport initServicesCredentials set +e addUserMailu alias $METABASE_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigMetabase installStack metabase metabase-app "Starting listener" $HOME/metabase.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$METABASE_INIT_ENV" = "true" ]; then METABASE_INIT_ENV=true updateConfigVar METABASE_INIT_ENV $METABASE_INIT_ENV fi wizard_notes=$(cat <" sleep 3 addReadOnlyUserToDatabase Metabase postgres metabase-db $METABASE_DATABASE_NAME $METABASE_DATABASE_USER $METABASE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/metabase/dbexport $METABASE_DATABASE_READONLYUSER $METABASE_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_METABASE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://metabase-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_METABASE $MANAGETLS_METABASE "$is_integrate_hshq" $NETDEFAULT_METABASE "$inner_block" insertSubAuthelia $SUB_METABASE.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll metabase "$FMLNAME_METABASE" $USERTYPE_METABASE "https://$SUB_METABASE.$HOMESERVER_DOMAIN" "metabase.png" "$(getHeimdallOrderFromSub $SUB_METABASE $USERTYPE_METABASE)" restartAllCaddyContainers checkAddDBConnection true metabase "$FMLNAME_METABASE" postgres metabase-db $METABASE_DATABASE_NAME $METABASE_DATABASE_USER $METABASE_DATABASE_USER_PASSWORD fi sleep 3 set +e setup_token=$(curl -s -X GET -H "Content-Type: application/json" https://$SUB_METABASE.$HOMESERVER_DOMAIN/api/session/properties | jq -r '.["setup-token"]') curl -s -X POST -H "Content-type: application/json" \ https://$SUB_METABASE.$HOMESERVER_DOMAIN/api/setup -d '{ "token": "'$setup_token'", "user": { "email": "'$METABASE_ADMIN_EMAIL_ADDRESS'", "first_name": "'"$(getAdminEmailName)"'", "last_name": "Metabase", "password": "'$METABASE_ADMIN_PASSWORD'" }, "prefs": { "allow_tracking": false, "site_name": "'"$HOMESERVER_NAME"'" } }' > /dev/null 2>&1 sleep 3 echo "Restarting metabase stack..." startStopStack metabase stop sleep 1 startStopStack metabase start waitForContainerLogString metabase-app 5 120 "Starting listener" echo "Metabase configured!" docker container restart openwebui-mcpo > /dev/null 2>&1 } function outputConfigMetabase() { cat < $HOME/metabase-compose.yml $STACK_VERSION_PREFIX metabase $(getScriptStackVersion metabase) services: metabase-db: image: $(getScriptImageByContainerName metabase-db) container_name: metabase-db hostname: metabase-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-metabase-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/metabase/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/metabase/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.metabase-hourly-db.schedule=@every 1h" - "ofelia.job-exec.metabase-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.metabase-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.metabase-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.metabase-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.metabase-hourly-db.email-from=Metabase Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.metabase-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.metabase-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.metabase-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.metabase-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.metabase-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.metabase-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.metabase-monthly-db.email-from=Metabase Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.metabase-monthly-db.mail-only-on-error=false" metabase-app: image: $(getScriptImageByContainerName metabase-app) container_name: metabase-app hostname: metabase-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - metabase-db networks: - int-metabase-net - dock-ext-net - dock-dbs-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /dev/urandom:/dev/random:ro networks: dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-internalmail-net: name: dock-internalmail external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-metabase-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/metabase.env TZ=\${PORTAINER_TZ} MB_DB_TYPE=postgres MB_DB_HOST=metabase-db MB_DB_PORT=5432 MB_DB_DBNAME=$METABASE_DATABASE_NAME MB_DB_USER=$METABASE_DATABASE_USER MB_DB_PASS=$METABASE_DATABASE_USER_PASSWORD POSTGRES_DB=$METABASE_DATABASE_NAME POSTGRES_USER=$METABASE_DATABASE_USER POSTGRES_PASSWORD=$METABASE_DATABASE_USER_PASSWORD MB_CONFIG_FILE=/metabase/config/config.yml MB_EMAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS MB_EMAIL_FROM_NAME=Metabase $(getAdminEmailName) MB_EMAIL_SMTP_HOST=$SMTP_HOSTNAME MB_EMAIL_SMTP_PORT=$SMTP_HOSTPORT MB_EMAIL_SMTP_SECURITY=none MB_EMAIL_SMTP_USERNAME= MB_EMAIL_SMTP_PASSWORD= MB_EMAIL_REPLY_TO=true MB_ANON_TRACKING_ENABLED=false MB_DB_CONNECTION_TIMEOUT_MS=3000 MB_API_KEY=$METABASE_API_KEY MB_LDAP_ENABLED=true MB_LDAP_HOST=ldapserver MB_LDAP_PORT=389 MB_LDAP_SECURITY=starttls MB_LDAP_BIND_DN=$LDAP_READONLY_USER_BIND_DN MB_LDAP_PASSWORD=$LDAP_READONLY_USER_PASSWORD MB_LDAP_USER_BASE=ou=people,$LDAP_BASE_DN MB_LDAP_USER_FILTER=(&(uid={login})(memberOf=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)) MB_LDAP_ATTRIBUTE_EMAIL=mail MB_LDAP_ATTRIBUTE_FIRSTNAME=givenName MB_LDAP_ATTRIBUTE_LASTNAME=sn MB_LDAP_GROUP_BASE=ou=groups,$LDAP_BASE_DN MB_LDAP_GROUP_SYNC=false MB_SEND_EMAIL_ON_FIRST_LOGIN_FROM_NEW_DEVICE=false EOFMT outputDBsList $HSHQ_STACKS_DIR/metabase/dbimport/$ALLDBS_IMPORT_FILENAME cat < $HSHQ_STACKS_DIR/metabase/dbimport/importConnections.sh #!/bin/bash METABASE_ADMIN_EMAIL_ADDRESS=$METABASE_ADMIN_EMAIL_ADDRESS METABASE_ADMIN_PASSWORD=$METABASE_ADMIN_PASSWORD METABASE_URL=https://$SUB_METABASE.$HOMESERVER_DOMAIN IMPORT_FILE=$HSHQ_STACKS_DIR/metabase/dbimport/$ALLDBS_IMPORT_FILENAME function main() { if ! [ -f \$IMPORT_FILE ]; then echo "ERROR: Missing import file, returning..." return fi echo "Adding all databases to Metabase..." SESSION_ID=\$(curl -s -X POST -H "Content-Type: application/json" -d "{\"username\": \"\${METABASE_ADMIN_EMAIL_ADDRESS}\", \"password\": \"\${METABASE_ADMIN_PASSWORD}\"}" \${METABASE_URL}/api/session | jq -r '.id') if [ \$? -ne 0 ] || [ -z "\$SESSION_ID" ]; then echo "ERROR: There was a problem authenticating with the API. Ensure you have added the admin user via the initial onboarding wizard before running this function, returning..." return fi curList=(\$(curl -s -X GET -H "Content-Type: application/json" -H "X-Metabase-Session: \$SESSION_ID" \${METABASE_URL}/api/database | jq -r '.data[].name')) while read curLine; do if [ -z "\$curLine" ] || [ "\${curLine:0:1}" = "#" ]; then continue fi connName=\$(echo "\$curLine" | cut -d" " -f1 | sed 's/"//g') connType=\$(echo "\$curLine" | cut -d" " -f2) connHost=\$(echo "\$curLine" | cut -d" " -f3) connDatabase=\$(echo "\$curLine" | cut -d" " -f4) connUser=\$(echo "\$curLine" | cut -d" " -f5) connPassword=\$(echo "\$curLine" | cut -d" " -f6) connTypeName=\$connType isDup=false for curDB in "\${curList[@]}" do if [ "\$curDB" = "\$connName" ]; then echo "DB has already been added (\$connName), skipping..." isDup=true break fi done if [ "\$isDup" = "true" ]; then continue fi case "\$connType" in "postgres") jsonbody="{\"name\": \"\$connName\", \"engine\": \"\$connTypeName\", \"details\": { \"host\": \"\$connHost\", \"port\": \"5432\", \"dbname\": \"\$connDatabase\", \"user\": \"\$connUser\", \"password\": \"\$connPassword\"}}" ;; "mysql") jsonbody="{\"name\": \"\$connName\", \"engine\": \"\$connTypeName\", \"details\": { \"host\": \"\$connHost\", \"port\": \"3306\", \"dbname\": \"\$connDatabase\", \"user\": \"\$connUser\", \"password\": \"\$connPassword\"}}" ;; "mongodb") connTypeName=mongo jsonbody="{\"name\": \"\$connName\", \"engine\": \"\$connTypeName\", \"details\": { \"host\": \"\$connHost\", \"port\": \"27017\", \"dbname\": \"\$connDatabase\", \"user\": \"\$connUser\", \"pass\": \"\$connPassword\", \"authdb\": \"admin\"}}" ;; *) continue ;; esac #echo "curl -s -X POST -H \"Content-Type: application/json\" -H \"X-Metabase-Session: \$SESSION_ID\" -d '\$jsonbody' \${METABASE_URL}/api/database" #echo "Adding \${connName}..." res=\$(curl -s -X POST -H "Content-Type: application/json" -H "X-Metabase-Session: \$SESSION_ID" -d "\$jsonbody" \${METABASE_URL}/api/database) addid=\$(echo "\$res" | jq -r '.id') if [ -z "\$addid" ] || [ "\$addid" = "null" ]; then errMsg=\$(echo "\$res" | jq -r '.message') echo "ERROR - there was an issue adding this DB (\${connName}): \$errMsg" fi done <\$IMPORT_FILE echo "Import process complete!" } main EOFIM chmod 755 $HSHQ_STACKS_DIR/metabase/dbimport/importConnections.sh } function performUpdateMetabase() { perform_stack_name=metabase prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=mirror.gcr.io/postgres:15.0-bullseye,metabase/metabase:v0.56.2.4 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="metabase/metabase:v0.56.2.4,mirror.gcr.io/metabase/metabase:v0.58.5.4" ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/metabase/metabase:v0.57.5.3 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/metabase/metabase:v0.57.5.3,mirror.gcr.io/metabase/metabase:v0.58.5.4" ;; 3) newVer=v4 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/metabase/metabase:v0.58.5.4 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/metabase/metabase:v0.58.5.4,mirror.gcr.io/metabase/metabase:v0.63.2" ;; 4) newVer=v4 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/metabase/metabase:v0.63.2 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/metabase/metabase:v0.63.2,mirror.gcr.io/metabase/metabase:v0.63.2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Kanboard function installKanboard() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory kanboard "Kanboard" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName kanboard-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName kanboard-db) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/kanboard mkdir $HSHQ_STACKS_DIR/kanboard/db mkdir $HSHQ_STACKS_DIR/kanboard/dbexport mkdir $HSHQ_STACKS_DIR/kanboard/data mkdir $HSHQ_STACKS_DIR/kanboard/plugins mkdir $HSHQ_STACKS_DIR/kanboard/certs chmod 777 $HSHQ_STACKS_DIR/kanboard/dbexport initServicesCredentials set +e addUserMailu alias $KANBOARD_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS KANBOARD_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 12 "" $KANBOARD_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$/\\$/g') outputConfigKanboard installStack kanboard kanboard-app "" $HOME/kanboard.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$KANBOARD_INIT_ENV" = "true" ]; then sendEmail -s "Kanboard Admin Login Info" -b "Kanboard Admin Username: $KANBOARD_ADMIN_USERNAME\nKanboard Admin Password: $KANBOARD_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" KANBOARD_INIT_ENV=true updateConfigVar KANBOARD_INIT_ENV $KANBOARD_INIT_ENV fi sleep 3 docker exec kanboard-db /dbexport/setupDBSettings.sh rm -f $HSHQ_STACKS_DIR/kanboard/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase Kanboard postgres kanboard-db $KANBOARD_DATABASE_NAME $KANBOARD_DATABASE_USER $KANBOARD_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/kanboard/dbexport $KANBOARD_DATABASE_READONLYUSER $KANBOARD_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_KANBOARD.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://kanboard-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KANBOARD $MANAGETLS_KANBOARD "$is_integrate_hshq" $NETDEFAULT_KANBOARD "$inner_block" insertSubAuthelia $SUB_KANBOARD.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll kanboard "$FMLNAME_KANBOARD" $USERTYPE_KANBOARD "https://$SUB_KANBOARD.$HOMESERVER_DOMAIN" "kanboard.png" "$(getHeimdallOrderFromSub $SUB_KANBOARD $USERTYPE_KANBOARD)" restartAllCaddyContainers checkAddDBConnection true kanboard "$FMLNAME_KANBOARD" postgres kanboard-db $KANBOARD_DATABASE_NAME $KANBOARD_DATABASE_USER $KANBOARD_DATABASE_USER_PASSWORD fi } function outputConfigKanboard() { cat < $HOME/kanboard-compose.yml $STACK_VERSION_PREFIX kanboard $(getScriptStackVersion kanboard) services: kanboard-db: image: $(getScriptImageByContainerName kanboard-db) container_name: kanboard-db hostname: kanboard-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-kanboard-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/kanboard/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/kanboard/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.kanboard-hourly-db.schedule=@every 1h" - "ofelia.job-exec.kanboard-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.kanboard-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.kanboard-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.kanboard-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.kanboard-hourly-db.email-from=Kanboard Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.kanboard-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.kanboard-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.kanboard-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.kanboard-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.kanboard-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.kanboard-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.kanboard-monthly-db.email-from=Kanboard Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.kanboard-monthly-db.mail-only-on-error=false" kanboard-app: image: $(getScriptImageByContainerName kanboard-app) container_name: kanboard-app hostname: kanboard-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - kanboard-db networks: - dock-ldap-net - dock-internalmail-net - dock-ext-net - int-kanboard-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-kanboard-data:/var/www/app/data - v-kanboard-plugins:/var/www/app/plugins - v-kanboard-certs:/etc/nginx/ssl volumes: v-kanboard-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/kanboard/data v-kanboard-plugins: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/kanboard/plugins v-kanboard-certs: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/kanboard/certs networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ldap-net: name: dock-ldap external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-kanboard-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/kanboard.env TZ=\${PORTAINER_TZ} DATABASE_URL=postgres://$KANBOARD_DATABASE_USER:$KANBOARD_DATABASE_USER_PASSWORD@kanboard-db/$KANBOARD_DATABASE_NAME POSTGRES_DB=$KANBOARD_DATABASE_NAME POSTGRES_USER=$KANBOARD_DATABASE_USER POSTGRES_PASSWORD=$KANBOARD_DATABASE_USER_PASSWORD LDAP_AUTH=true LDAP_SERVER=ldapserver LDAP_PORT=389 LDAP_SSL_VERIFY=true LDAP_START_TLS=true LDAP_BIND_TYPE=proxy LDAP_USERNAME=$LDAP_READONLY_USER_BIND_DN LDAP_PASSWORD=$LDAP_READONLY_USER_PASSWORD LDAP_USER_BASE_DN=ou=people,$LDAP_BASE_DN LDAP_USER_FILTER=(&(uid=%s)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)) LDAP_USER_ATTRIBUTE_USERNAME=uid LDAP_USER_ATTRIBUTE_FULLNAME=cn LDAP_USER_ATTRIBUTE_EMAIL=mail LDAP_USER_ATTRIBUTE_GROUPS=memberOf LDAP_USER_ATTRIBUTE_PHOTO=jpegPhoto LDAP_USER_CREATION=true LDAP_USER_DEFAULT_ROLE_MANAGER=false LDAP_GROUP_ADMIN_DN=cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN LDAP_GROUP_MANAGER_DN=cn=managers,ou=groups,$LDAP_BASE_DN LDAP_GROUP_PROVIDER=true LDAP_GROUP_BASE_DN=ou=groups,$LDAP_BASE_DN LDAP_GROUP_FILTER=(&(objectclass=groupOfUniqueNames)(cn=%s*)) LDAP_GROUP_ATTRIBUTE_NAME=cn MAIL_TRANSPORT=smtp MAIL_SMTP_HOSTNAME=$SMTP_HOSTNAME MAIL_SMTP_PORT=$SMTP_HOSTPORT MAIL_SMTP_ENCRYPTION=tls MAIL_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS EOFMT cat < $HSHQ_STACKS_DIR/kanboard/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$KANBOARD_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do admUser=\$(echo "select username from users where username='admin';" | psql -t -A -U $KANBOARD_DATABASE_USER $KANBOARD_DATABASE_NAME 2> /dev/null) if ! [ -z "\$admUser" ] && [ "\$admUser" = "admin" ]; then break fi echo "Database not ready, sleeping 3 seconds, total wait=\$curSeconds seconds..." sleep 3 curSeconds=\$((curSeconds+3)) done echo "update users set username='$KANBOARD_ADMIN_USERNAME', email='$KANBOARD_ADMIN_EMAIL_ADDRESS', password='$KANBOARD_ADMIN_PASSWORD_HASH', name='$HOMESERVER_NAME Kanboard Admin' where username='admin';" | psql -U $KANBOARD_DATABASE_USER $KANBOARD_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/kanboard/dbexport/setupDBSettings.sh } function performUpdateKanboard() { perform_stack_name=kanboard prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/kanboard/kanboard:v1.2.47 image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/kanboard/kanboard:v1.2.47,mirror.gcr.io/kanboard/kanboard:v1.2.48" ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/kanboard/kanboard:v1.2.48 image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/kanboard/kanboard:v1.2.48,mirror.gcr.io/kanboard/kanboard:v1.2.48" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Wekan function installWekan() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory wekan "Wekan" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wekan-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName wekan-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/wekan mkdir $HSHQ_STACKS_DIR/wekan/db mkdir $HSHQ_STACKS_DIR/wekan/dbexport mkdir $HSHQ_STACKS_DIR/wekan/files initServicesCredentials set +e WEKAN_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $WEKAN_OIDC_CLIENT_SECRET | tr -d ':\n') outputConfigWekan oidcBlock=$(cat $HOME/wekan.oidc) rm -f $HOME/wekan.oidc insertOIDCClientAuthelia wekan "$oidcBlock" installStack wekan wekan-app "Universal file server initialized successfully" $HOME/wekan.env 3 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 3 docker exec -it wekan-db bash -c "mongosh -u $WEKAN_DATABASE_USER -p $WEKAN_DATABASE_USER_PASSWORD --eval \"use $WEKAN_DATABASE_NAME\" --eval \"db.settings.updateOne( {}, { \\\$set: { disableRegistration: true } } )\"" > /dev/null 2>&1 addReadOnlyUserToDatabase Wekan mongodb wekan-db $WEKAN_DATABASE_NAME $WEKAN_DATABASE_USER $WEKAN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/wekan/dbexport $WEKAN_DATABASE_READONLYUSER $WEKAN_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_WEKAN.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://wekan-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_WEKAN $MANAGETLS_WEKAN "$is_integrate_hshq" $NETDEFAULT_WEKAN "$inner_block" insertSubAuthelia $SUB_WEKAN.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll wekan "$FMLNAME_WEKAN" $USERTYPE_WEKAN "https://$SUB_WEKAN.$HOMESERVER_DOMAIN" "wekan.png" "$(getHeimdallOrderFromSub $SUB_WEKAN $USERTYPE_WEKAN)" checkAddDBConnection true wekan "$FMLNAME_WEKAN" mongo wekan-db $WEKAN_DATABASE_NAME $WEKAN_DATABASE_USER $WEKAN_DATABASE_USER_PASSWORD restartAllCaddyContainers fi } function outputConfigWekan() { cat < $HOME/wekan-compose.yml $STACK_VERSION_PREFIX wekan $(getScriptStackVersion wekan) services: wekan-db: image: $(getScriptImageByContainerName wekan-db) container_name: wekan-db hostname: wekan-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mongod --oplogSize 128 --quiet --auth networks: - int-wekan-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-wekan-db:/data/db - v-wekan-dbexport:/dump wekan-app: image: $(getScriptImageByContainerName wekan-app) container_name: wekan-app hostname: wekan-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - wekan-db networks: - dock-ldap-net - dock-internalmail-net - dock-ext-net - int-wekan-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-wekan-files:/data:rw volumes: v-wekan-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wekan/db v-wekan-dbexport: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wekan/dbexport v-wekan-files: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/wekan/files networks: dock-proxy-net: name: dock-proxy external: true dock-ldap-net: name: dock-ldap external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-wekan-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/wekan.env TZ=\${PORTAINER_TZ} ROOT_URL=https://$SUB_WEKAN.$HOMESERVER_DOMAIN WRITABLE_PATH=/data WITH_API=true MONGO_INITDB_ROOT_USERNAME=$WEKAN_DATABASE_USER MONGO_INITDB_ROOT_PASSWORD=$WEKAN_DATABASE_USER_PASSWORD MONGO_URL=mongodb://$WEKAN_DATABASE_USER:$WEKAN_DATABASE_USER_PASSWORD@wekan-db:27017/${WEKAN_DATABASE_NAME}?authSource=admin MAIL_URL=smtp://$SMTP_HOSTNAME:$SMTP_HOSTPORT/?ignoreTLS=false&tls={rejectUnauthorized:true} MAIL_FROM=WeKan $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt PASSWORD_LOGIN_ENABLED=false OAUTH2_ENABLED=true OAUTH2_LOGIN_STYLE=redirect OAUTH2_CLIENT_ID=wekan OAUTH2_SECRET=$WEKAN_OIDC_CLIENT_SECRET OAUTH2_SERVER_URL=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN OAUTH2_AUTH_ENDPOINT=/api/oidc/authorization OAUTH2_TOKEN_ENDPOINT=/api/oidc/token OAUTH2_USERINFO_ENDPOINT=/api/oidc/userinfo OAUTH2_ID_MAP=sub OAUTH2_USERNAME_MAP=email OAUTH2_FULLNAME_MAP=name OAUTH2_EMAIL_MAP=email EOFMT cat < $HOME/wekan.oidc # Authelia OIDC Client wekan BEGIN - client_id: wekan client_name: WeKan client_secret: $WEKAN_OIDC_CLIENT_SECRET_HASH public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth require_pkce: false pkce_challenge_method: '' redirect_uris: - https://$SUB_WEKAN.$HOMESERVER_DOMAIN/_oauth/oidc scopes: - openid - profile - groups - email response_types: - code grant_types: - authorization_code access_token_signed_response_alg: none userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_post # Authelia OIDC Client wekan END EOFIM } function performUpdateWekan() { perform_stack_name=wekan prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/mongo:8.0.13,ghcr.io/wekan/wekan:v7.94 image_update_map[0]="mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/mongo:8.0.13" image_update_map[1]="ghcr.io/wekan/wekan:v7.94,ghcr.io/wekan/wekan:v8.17" ;; 2) newVer=v2 curImageList=mirror.gcr.io/mongo:8.0.13,ghcr.io/wekan/wekan:v8.17 image_update_map[0]="mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/mongo:8.0.13" image_update_map[1]="ghcr.io/wekan/wekan:v8.17,ghcr.io/wekan/wekan:v8.17" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Revolt function installRevolt() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory revolt "Revolt" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-rabbitmq) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-minio) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-caddy) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-api) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-events) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-autumn) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-january) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-crond) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName revolt-pushd) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/revolt mkdir $HSHQ_STACKS_DIR/revolt/config mkdir $HSHQ_STACKS_DIR/revolt/db mkdir $HSHQ_STACKS_DIR/revolt/dbexport mkdir $HSHQ_STACKS_DIR/revolt/rabbitmq mkdir $HSHQ_STACKS_DIR/revolt/minio mkdir $HSHQ_STACKS_DIR/revolt/caddy mkdir $HSHQ_STACKS_DIR/revolt/caddy/data mkdir $HSHQ_STACKS_DIR/revolt/caddy/config initServicesCredentials set +e outputConfigRevolt installStack revolt revolt-api "Rocket has launched from" $HOME/revolt.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 3 addReadOnlyUserToDatabase Revolt mongodb revolt-db $REVOLT_DATABASE_NAME $REVOLT_DATABASE_USER $REVOLT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/revolt/dbexport $REVOLT_DATABASE_READONLYUSER $REVOLT_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_REVOLT.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://revolt-caddy {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_REVOLT $MANAGETLS_REVOLT "$is_integrate_hshq" $NETDEFAULT_REVOLT "$inner_block" insertSubAuthelia $SUB_REVOLT.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll revolt "$FMLNAME_REVOLT" $USERTYPE_REVOLT "https://$SUB_REVOLT.$HOMESERVER_DOMAIN" "revolt.png" "$(getHeimdallOrderFromSub $SUB_REVOLT $USERTYPE_REVOLT)" checkAddDBConnection true revolt "$FMLNAME_REVOLT" mongo revolt-db $REVOLT_DATABASE_NAME $REVOLT_DATABASE_USER $REVOLT_DATABASE_USER_PASSWORD restartAllCaddyContainers fi } function outputConfigRevolt() { cat < $HOME/revolt-compose.yml $STACK_VERSION_PREFIX revolt $(getScriptStackVersion revolt) services: revolt-db: image: $(getScriptImageByContainerName revolt-db) container_name: revolt-db hostname: revolt-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mongod --oplogSize 128 --quiet --auth networks: - int-revolt-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-revolt-db:/data/db - v-revolt-dbexport:/dump revolt-redis: image: $(getScriptImageByContainerName revolt-redis) container_name: revolt-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $REVOLT_REDIS_PASSWORD --appendonly no networks: - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro revolt-rabbitmq: image: $(getScriptImageByContainerName revolt-rabbitmq) container_name: revolt-rabbitmq hostname: revolt-rabbitmq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/rabbitmq:/var/lib/rabbitmq minio: image: $(getScriptImageByContainerName revolt-minio) container_name: revolt-minio hostname: revolt-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data networks: dock-proxy-net: int-revolt-net: aliases: - revolt-uploads.minio - attachments.minio - avatars.minio - backgrounds.minio - icons.minio - banners.minio - emojis.minio entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 /usr/bin/mc alias set minio http://revolt-minio:9000 ${REVOLT_MINIO_KEY} ${REVOLT_MINIO_SECRET} echo 'Creating buckets revolt' /usr/bin/mc mb minio/attachments /usr/bin/mc mb minio/avatars /usr/bin/mc mb minio/backgrounds /usr/bin/mc mb minio/icons /usr/bin/mc mb minio/banners /usr/bin/mc mb minio/emojis /usr/bin/mc mb minio/revolt-uploads wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/minio:/data revolt-caddy: image: $(getScriptImageByContainerName revolt-caddy) container_name: revolt-caddy hostname: revolt-caddy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/caddy/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/caddy/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/caddy/Caddyfile:/etc/caddy/Caddyfile revolt-api: image: $(getScriptImageByContainerName revolt-api) container_name: revolt-api hostname: revolt-api restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - revolt-redis - revolt-rabbitmq networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-events: image: $(getScriptImageByContainerName revolt-events) container_name: revolt-events hostname: revolt-events restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - revolt-redis networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-web: image: $(getScriptImageByContainerName revolt-web) container_name: revolt-web hostname: revolt-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro revolt-autumn: image: $(getScriptImageByContainerName revolt-autumn) container_name: revolt-autumn hostname: revolt-autumn restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-january: image: $(getScriptImageByContainerName revolt-january) container_name: revolt-january hostname: revolt-january restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-crond: image: $(getScriptImageByContainerName revolt-crond) container_name: revolt-crond hostname: revolt-crond restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - minio networks: - int-revolt-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-pushd: image: $(getScriptImageByContainerName revolt-pushd) container_name: revolt-pushd hostname: revolt-pushd restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - revolt-redis - revolt-rabbitmq networks: - int-revolt-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml volumes: v-revolt-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/revolt/db v-revolt-dbexport: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/revolt/dbexport networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-revolt-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/revolt.env TZ=\${PORTAINER_TZ} HOSTNAME=:80 REVOLT_PUBLIC_URL=https://$SUB_REVOLT.$HOMESERVER_DOMAIN/api REVOLT_APP_URL=https://$SUB_REVOLT.$HOMESERVER_DOMAIN VITE_API_URL=https://$SUB_REVOLT.$HOMESERVER_DOMAIN/api REVOLT_EXTERNAL_WS_URL=wss://$SUB_REVOLT.$HOMESERVER_DOMAIN/ws AUTUMN_PUBLIC_URL=https://$SUB_REVOLT.$HOMESERVER_DOMAIN/autumn JANUARY_PUBLIC_URL=https://$SUB_REVOLT.$HOMESERVER_DOMAIN/january AUTUMN_MONGO_URI=mongodb://$REVOLT_DATABASE_USER:$REVOLT_DATABASE_USER_PASSWORD@revolt-db:27017/${REVOLT_DATABASE_NAME}?authSource=admin MINIO_ROOT_USER=$REVOLT_MINIO_KEY MINIO_ROOT_PASSWORD=$REVOLT_MINIO_SECRET MINIO_DOMAIN=minio MINIO_CONSOLE_ADDRESS=:9090 RABBITMQ_DEFAULT_USER=$REVOLT_RABBITMQ_USERNAME RABBITMQ_DEFAULT_PASS=$REVOLT_RABBITMQ_PASSWORD MONGO_INITDB_ROOT_USERNAME=$REVOLT_DATABASE_USER MONGO_INITDB_ROOT_PASSWORD=$REVOLT_DATABASE_USER_PASSWORD NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt EOFMT openssl ecparam -name prime256v1 -genkey -noout -out $HOME/vapid_private.pem cat < $HSHQ_STACKS_DIR/revolt/config/Revolt.toml production = true [database] # MongoDB connection URL # Defaults to the container name specified in self-hosted mongodb = "mongodb://$REVOLT_DATABASE_USER:$REVOLT_DATABASE_USER_PASSWORD@revolt-db:27017/${REVOLT_DATABASE_NAME}?authSource=admin" # mongodb = "mongodb://revolt-db:27017/${REVOLT_DATABASE_NAME}" # Redis connection URL # Defaults to the container name specified in self-hosted redis = "redis://:$REVOLT_REDIS_PASSWORD@revolt-redis:6379" [hosts] # Web locations of various services # Defaults assume all services are reverse-proxied # See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile # # Remember to change these to https/wss where appropriate in production! app = "https://$SUB_REVOLT.$HOMESERVER_DOMAIN" api = "https://$SUB_REVOLT.$HOMESERVER_DOMAIN/api" events = "wss://$SUB_REVOLT.$HOMESERVER_DOMAIN/ws" autumn = "https://$SUB_REVOLT.$HOMESERVER_DOMAIN/autumn" january = "https://$SUB_REVOLT.$HOMESERVER_DOMAIN/january" voso_legacy = "" voso_legacy_ws = "" [rabbit] host = "revolt-rabbitmq" port = 5672 username = "$REVOLT_RABBITMQ_USERNAME" password = "$REVOLT_RABBITMQ_PASSWORD" [api] [api.registration] # Whether an invite should be required for registration # See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only invite_only = false [api.smtp] # Email server configuration for verification # Defaults to no email verification (host field is empty) host = "$SMTP_HOSTNAME" username = "$EMAIL_SMTP_EMAIL_ADDRESS" password = "$EMAIL_SMTP_PASSWORD" from_address = "Revolt $(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" port = 587 use_starttls = true [api.security] # Authifier Shield API key authifier_shield_key = "" # Legacy voice server management token voso_legacy_token = "" # Whether services are behind the Cloudflare network trust_cloudflare = false # easypwned endpoint easypwned = "" [api.security.captcha] # hCaptcha configuration hcaptcha_key = "" hcaptcha_sitekey = "" [api.workers] # Maximum concurrent connections (to proxy server) max_concurrent_connections = 50 [api.users] [pushd] # this changes the names of the queues to not overlap # prod/beta if they happen to be on the same exchange/instance. # Usually they have to be, so that messages sent from one or the other get sent to everyone production = true # Changes how many users are processed in each chunk when resolving role/everyone mentions. # Increasing this will resolve mentions faster, but will consume more memory while resolving. mass_mention_chunk_size = 200 # none of these should need changing exchange = "revolt.notifications" message_queue = "notifications.origin.message" mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted fr_received_queue = "notifications.ingest.fr_received" # friend request received generic_queue = "notifications.ingest.generic" # generic messages (title + body) ack_queue = "notifications.process.ack" # updates badges for apple devices [pushd.vapid] queue = "notifications.outbound.vapid" private_key = "$(base64 -i $HOME/vapid_private.pem | tr -d '\n' | tr -d '=')" public_key = "$(openssl ec -in $HOME/vapid_private.pem -outform DER|tail --bytes 65|base64|tr '/+' '_-'|tr -d '\n'|tr -d '=')" [pushd.fcm] queue = "notifications.outbound.fcm" key_type = "" project_id = "" private_key_id = "" private_key = "" client_email = "" client_id = "" auth_uri = "" token_uri = "" auth_provider_x509_cert_url = "" client_x509_cert_url = "" [pushd.apn] sandbox = false queue = "notifications.outbound.apn" pkcs8 = "" key_id = "" team_id = "" [files] # Encryption key for stored files # Generate your own key using openssl rand -base64 32 encryption_key = "$(openssl rand -base64 32)" # Quality used for lossy WebP previews (set to 100 for lossless) webp_quality = 80.0 # Mime types that cannot be uploaded or served # # Example for Windows executables and Android installation files: # ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"] blocked_mime_types = [] # ClamAV service # hostname:port $(if [ -z "$is_antivirus_commented_out" ]; then echo "clamd_host = \"mailu-antivirus:3310\""; else echo "clamd_host = \"\""; fi) # Mime types that should be virus scanned # # Leave empty to scan all file types #scan_mime_types = [ # "application/vnd.microsoft.portable-executable", # "application/vnd.android.package-archive", # "application/zip", #] [files.limit] # Minimum file size (in bytes) min_file_size = 1 # Minimum image resolution min_resolution = [1, 1] # Maximum MP of images max_mega_pixels = 40 # Maximum pixel side of an image max_pixel_side = 10_000 [files.preview] # Maximum image resolution attachments = [1280, 1280] avatars = [128, 128] backgrounds = [1280, 720] icons = [128, 128] banners = [480, 480] emojis = [128, 128] [files.s3] # Configuration for S3 # Defaults included for MinIO + self-hosted setup # # Backblaze B2: # - endpoint is listed on the "Buckets" page # - path_style_buckets is set to true # - region is eu-central-003 string from endpoint URL # - access_key_id is keyID generated on the "Application Keys" page # - secret_access_key is token generated on the "Application Keys" page # - default_bucket matches the name of the bucket you've created # S3 protocol endpoint endpoint = "http://minio:9000" # Whether to use path-style buckets # Generally true, except for MinIO path_style_buckets = false # S3 region name region = "minio" # S3 protocol key ID access_key_id = "$REVOLT_MINIO_KEY" # S3 protocol access key secret_access_key = "$REVOLT_MINIO_SECRET" default_bucket = "revolt-uploads" [features] # Feature gate options webhooks_enabled = false # Enable push notifications for mass pings (everyone, online, roles) # When false this will still ping in-client but will not send notifications from pushd mass_mentions_send_notifications = true # Can role/everyone pings be used at all mass_mentions_enabled = true [features.limits] [features.limits.global] group_size = 100 message_embeds = 5 message_replies = 5 message_reactions = 20 server_emoji = 100 server_roles = 200 server_channels = 200 # How many hours since creation a user is considered new new_user_hours = 72 # Maximum permissible body size in bytes for uploads # (should be greater than any one file upload limit) body_limit_size = 20_000_000 [features.limits.new_user] # Limits imposed on new users # Number of outgoing friend requests permitted at any time outgoing_friend_requests = 5 # Maximum number of owned bots bots = 2 # Message content length message_length = 2000 # Number of attachments that can be included message_attachments = 5 # Maximum number of servers the user can create/join servers = 50 [features.limits.new_user.file_upload_size_limit] # Maximum file size limits (in bytes) attachments = 20_000_000 avatars = 4_000_000 backgrounds = 6_000_000 icons = 2_500_000 banners = 6_000_000 emojis = 500_000 [features.limits.default] # Limits imposed on users by default # Number of outgoing friend requests permitted at any time outgoing_friend_requests = 10 # Maximum number of owned bots bots = 5 # Message content length message_length = 2000 # Number of attachments that can be included message_attachments = 5 # Maximum number of servers the user can create/join servers = 100 [features.limits.default.file_upload_size_limit] # Maximum file size limits (in bytes) attachments = 20_000_000 avatars = 4_000_000 backgrounds = 6_000_000 icons = 2_500_000 banners = 6_000_000 emojis = 500_000 [features.advanced] # The max amount of messages the rabbitmq provider/db mention adder job will delay for before forcing handling of a channel. # default: 5 process_message_delay_limit = 5 [sentry] # Configuration for Sentry error reporting api = "" events = "" files = "" proxy = "" pushd = "" crond = "" EOFMT rm -f $HOME/vapid_private.pem cat < $HSHQ_STACKS_DIR/revolt/caddy/Caddyfile :80 { route /api* { uri strip_prefix /api reverse_proxy http://revolt-api:14702 { header_down Location "^/" "/api/" } } route /ws { uri strip_prefix /ws reverse_proxy http://revolt-events:14703 { header_down Location "^/" "/ws/" } } route /autumn* { uri strip_prefix /autumn reverse_proxy http://revolt-autumn:14704 { header_down Location "^/" "/autumn/" } } route /january* { uri strip_prefix /january reverse_proxy http://revolt-january:14705 { header_down Location "^/" "/january/" } } reverse_proxy http://revolt-web:5000 } EOFMT } function performUpdateRevolt() { perform_stack_name=revolt prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/rabbitmq:4.1.4,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/caddy:2.10.0,ghcr.io/revoltchat/server:20250807-1,ghcr.io/revoltchat/bonfire:20250807-1,ghcr.io/revoltchat/client:master,ghcr.io/revoltchat/autumn:20250807-1,ghcr.io/revoltchat/january:20250807-1,ghcr.io/revoltchat/crond:20250807-1,ghcr.io/revoltchat/pushd:20250807-1,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/mongo:8.0.13" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[2]="mirror.gcr.io/rabbitmq:4.1.4,mirror.gcr.io/rabbitmq:4.1.4" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z" image_update_map[4]="mirror.gcr.io/caddy:2.10.0,mirror.gcr.io/caddy:2.10.0" image_update_map[5]="ghcr.io/revoltchat/server:20250807-1,ghcr.io/revoltchat/server:20250807-1" image_update_map[6]="ghcr.io/revoltchat/bonfire:20250807-1,ghcr.io/revoltchat/bonfire:20250807-1" image_update_map[7]="ghcr.io/revoltchat/client:master,ghcr.io/revoltchat/client:master" image_update_map[8]="ghcr.io/revoltchat/autumn:20250807-1,ghcr.io/revoltchat/autumn:20250807-1" image_update_map[9]="ghcr.io/revoltchat/january:20250807-1,ghcr.io/revoltchat/january:20250807-1" image_update_map[10]="ghcr.io/revoltchat/crond:20250807-1,ghcr.io/revoltchat/crond:20250807-1" image_update_map[11]="ghcr.io/revoltchat/pushd:20250807-1,ghcr.io/revoltchat/pushd:20250807-1" image_update_map[12]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true fixV2ComposeRevolt perform_update_report="${perform_update_report}$stack_upgrade_report" startStopStack revolt stop sleep 2 startStopStack revolt start return ;; 2) newVer=v3 curImageList=mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/rabbitmq:4.1.4,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/caddy:2.10.0,ghcr.io/revoltchat/server:20250807-1,ghcr.io/revoltchat/bonfire:20250807-1,ghcr.io/revoltchat/client:master,ghcr.io/revoltchat/autumn:20250807-1,ghcr.io/revoltchat/january:20250807-1,ghcr.io/revoltchat/crond:20250807-1,ghcr.io/revoltchat/pushd:20250807-1 image_update_map[0]="mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/mongo:8.0.13" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[2]="mirror.gcr.io/rabbitmq:4.1.4,mirror.gcr.io/rabbitmq:4.1.4" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="mirror.gcr.io/caddy:2.10.0,mirror.gcr.io/caddy:2.10.0" image_update_map[5]="ghcr.io/revoltchat/server:20250807-1,ghcr.io/revoltchat/server:20250807-1" image_update_map[6]="ghcr.io/revoltchat/bonfire:20250807-1,ghcr.io/revoltchat/bonfire:20250807-1" image_update_map[7]="ghcr.io/revoltchat/client:master,ghcr.io/revoltchat/client:master" image_update_map[8]="ghcr.io/revoltchat/autumn:20250807-1,ghcr.io/revoltchat/autumn:20250807-1" image_update_map[9]="ghcr.io/revoltchat/january:20250807-1,ghcr.io/revoltchat/january:20250807-1" image_update_map[10]="ghcr.io/revoltchat/crond:20250807-1,ghcr.io/revoltchat/crond:20250807-1" image_update_map[11]="ghcr.io/revoltchat/pushd:20250807-1,ghcr.io/revoltchat/pushd:20250807-1" ;; 3) newVer=v3 curImageList=mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/rabbitmq:4.1.4,ghcr.io/homeserverhq/minio:v1.0,mirror.gcr.io/caddy:2.10.0,ghcr.io/revoltchat/server:20250807-1,ghcr.io/revoltchat/bonfire:20250807-1,ghcr.io/revoltchat/client:master,ghcr.io/revoltchat/autumn:20250807-1,ghcr.io/revoltchat/january:20250807-1,ghcr.io/revoltchat/crond:20250807-1,ghcr.io/revoltchat/pushd:20250807-1 image_update_map[0]="mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/mongo:8.0.13" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" image_update_map[2]="mirror.gcr.io/rabbitmq:4.1.4,mirror.gcr.io/rabbitmq:4.1.4" image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="mirror.gcr.io/caddy:2.10.0,mirror.gcr.io/caddy:2.10.0" image_update_map[5]="ghcr.io/revoltchat/server:20250807-1,ghcr.io/revoltchat/server:20250807-1" image_update_map[6]="ghcr.io/revoltchat/bonfire:20250807-1,ghcr.io/revoltchat/bonfire:20250807-1" image_update_map[7]="ghcr.io/revoltchat/client:master,ghcr.io/revoltchat/client:master" image_update_map[8]="ghcr.io/revoltchat/autumn:20250807-1,ghcr.io/revoltchat/autumn:20250807-1" image_update_map[9]="ghcr.io/revoltchat/january:20250807-1,ghcr.io/revoltchat/january:20250807-1" image_update_map[10]="ghcr.io/revoltchat/crond:20250807-1,ghcr.io/revoltchat/crond:20250807-1" image_update_map[11]="ghcr.io/revoltchat/pushd:20250807-1,ghcr.io/revoltchat/pushd:20250807-1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function fixV2ComposeRevolt() { rm -f $HOME/revolt-compose.yml cat < $HOME/revolt-compose.yml $STACK_VERSION_PREFIX revolt v3 services: revolt-db: image: mirror.gcr.io/mongo:8.0.13 container_name: revolt-db hostname: revolt-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mongod --oplogSize 128 --quiet --auth networks: - int-revolt-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-revolt-db:/data/db - v-revolt-dbexport:/dump revolt-redis: image: mirror.gcr.io/redis:8.2.0-bookworm container_name: revolt-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $REVOLT_REDIS_PASSWORD --appendonly no networks: - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro revolt-rabbitmq: image: mirror.gcr.io/rabbitmq:4.1.4 container_name: revolt-rabbitmq hostname: revolt-rabbitmq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/rabbitmq:/var/lib/rabbitmq minio: image: mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z container_name: revolt-minio hostname: revolt-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data networks: dock-proxy-net: int-revolt-net: aliases: - revolt-uploads.minio - attachments.minio - avatars.minio - backgrounds.minio - icons.minio - banners.minio - emojis.minio entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 /usr/bin/mc alias set minio http://revolt-minio:9000 ${REVOLT_MINIO_KEY} ${REVOLT_MINIO_SECRET} echo 'Creating buckets revolt' /usr/bin/mc mb minio/attachments /usr/bin/mc mb minio/avatars /usr/bin/mc mb minio/backgrounds /usr/bin/mc mb minio/icons /usr/bin/mc mb minio/banners /usr/bin/mc mb minio/emojis /usr/bin/mc mb minio/revolt-uploads wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/minio:/data revolt-caddy: image: mirror.gcr.io/caddy:2.10.0 container_name: revolt-caddy hostname: revolt-caddy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/caddy/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/caddy/config:/config - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/caddy/Caddyfile:/etc/caddy/Caddyfile revolt-api: image: ghcr.io/revoltchat/server:20250807-1 container_name: revolt-api hostname: revolt-api restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - revolt-redis - revolt-rabbitmq networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-events: image: ghcr.io/revoltchat/bonfire:20250807-1 container_name: revolt-events hostname: revolt-events restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - revolt-redis networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-web: image: ghcr.io/revoltchat/client:master container_name: revolt-web hostname: revolt-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro revolt-autumn: image: ghcr.io/revoltchat/autumn:20250807-1 container_name: revolt-autumn hostname: revolt-autumn restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-january: image: ghcr.io/revoltchat/january:20250807-1 container_name: revolt-january hostname: revolt-january restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-internalmail-net - int-revolt-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-crond: image: ghcr.io/revoltchat/crond:20250807-1 container_name: revolt-crond hostname: revolt-crond restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - minio networks: - int-revolt-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml revolt-pushd: image: ghcr.io/revoltchat/pushd:20250807-1 container_name: revolt-pushd hostname: revolt-pushd restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - revolt-db - revolt-redis - revolt-rabbitmq networks: - int-revolt-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/revolt/config/Revolt.toml:/Revolt.toml volumes: v-revolt-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/revolt/db v-revolt-dbexport: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/revolt/dbexport networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-revolt-net: driver: bridge internal: true ipam: driver: default EOFMT } # FrappeHR function installFrappeHR() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory frappe-hr "FrappeHR" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName frappe-hr-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName frappe-hr-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName frappe-hr-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/frappe-hr mkdir $HSHQ_STACKS_DIR/frappe-hr/db mkdir $HSHQ_STACKS_DIR/frappe-hr/dbexport mkdir $HSHQ_STACKS_DIR/frappe-hr/workspace chmod 777 $HSHQ_STACKS_DIR/frappe-hr/dbexport initServicesCredentials set +e addUserMailu alias $FRAPPE_HR_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS FRAPPE_HR_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $FRAPPE_HR_ADMIN_PASSWORD | tr -d ':\n') outputConfigFrappeHR installStack frappe-hr frappe-hr-app "" $HOME/frappe-hr.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$FRAPPE_HR_INIT_ENV" = "true" ]; then sendEmail -s "FrappeHR Admin Login Info" -b "FrappeHR Admin Username: $FRAPPE_HR_ADMIN_USERNAME\nFrappeHR Admin Password: $FRAPPE_HR_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" FRAPPE_HR_INIT_ENV=true updateConfigVar FRAPPE_HR_INIT_ENV $FRAPPE_HR_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase FrappeHR mysql frappe-hr-db $FRAPPE_HR_DATABASE_NAME root $FRAPPE_HR_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/frappe-hr/dbexport $FRAPPE_HR_DATABASE_READONLYUSER $FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_FRAPPE_HR.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>@socketio {\n" inner_block=$inner_block">>>>>>>>path /socket.io*\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>handle @socketio {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://frappe-hr-app:9000\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://frappe-hr-app:8000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FRAPPE_HR $MANAGETLS_FRAPPE_HR "$is_integrate_hshq" $NETDEFAULT_FRAPPE_HR "$inner_block" insertSubAuthelia $SUB_FRAPPE_HR.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll frappe-hr "$FMLNAME_FRAPPE_HR" $USERTYPE_FRAPPE_HR "https://$SUB_FRAPPE_HR.$HOMESERVER_DOMAIN" "frappe-hr.png" "$(getHeimdallOrderFromSub $SUB_FRAPPE_HR $USERTYPE_FRAPPE_HR)" restartAllCaddyContainers checkAddDBConnection true frappe-hr "$FMLNAME_FRAPPE_HR" mysql frappe-hr-db $FRAPPE_HR_DATABASE_NAME $FRAPPE_HR_DATABASE_USER $FRAPPE_HR_DATABASE_USER_PASSWORD fi } function outputConfigFrappeHR() { cat < $HOME/frappe-hr-compose.yml $STACK_VERSION_PREFIX frappe-hr $(getScriptStackVersion frappe-hr) services: mariadb: image: $(getScriptImageByContainerName frappe-hr-db) container_name: frappe-hr-db hostname: frappe-hr-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --skip-character-set-client-handshake --skip-innodb-read-only-compressed networks: - int-frappe-hr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-frappe-hr-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/frappe-hr/dbexport:/dbexport environment: - MYSQL_ROOT_PASSWORD=123 # - MYSQL_DATABASE=$FRAPPE_HR_DATABASE_NAME # - MYSQL_ROOT_PASSWORD=$FRAPPE_HR_DATABASE_ROOT_PASSWORD # - MYSQL_USER=$FRAPPE_HR_DATABASE_USER # - MYSQL_PASSWORD=$FRAPPE_HR_DATABASE_USER_PASSWORD labels: - "ofelia.enabled=true" - "ofelia.job-exec.frappe-hr-hourly-db.schedule=@every 1h" - "ofelia.job-exec.frappe-hr-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.frappe-hr-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.frappe-hr-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.frappe-hr-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.frappe-hr-hourly-db.email-from=FrappeHR Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.frappe-hr-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.frappe-hr-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.frappe-hr-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.frappe-hr-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.frappe-hr-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.frappe-hr-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.frappe-hr-monthly-db.email-from=FrappeHR Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.frappe-hr-monthly-db.mail-only-on-error=false" frappe-hr-app: image: $(getScriptImageByContainerName frappe-hr-app) container_name: frappe-hr-app hostname: frappe-hr-app restart: unless-stopped env_file: stack.env command: bash /workspace/init.sh working_dir: /home/frappe depends_on: - mariadb networks: - int-frappe-hr-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/frappe-hr/workspace:/workspace environment: - SHELL=/bin/bash redis: image: $(getScriptImageByContainerName frappe-hr-redis) container_name: frappe-hr-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --appendonly no networks: - int-frappe-hr-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-frappe-hr-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/frappe-hr/db networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-frappe-hr-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/frappe-hr.env TZ=\${PORTAINER_TZ} EOFMT cat < $HSHQ_STACKS_DIR/frappe-hr/workspace/init.sh #!bin/bash if [ -d "/home/frappe/frappe-bench/apps/frappe" ]; then echo "Bench already exists, skipping init" cd frappe-bench bench start else echo "Creating new bench..." fi export PATH="\${NVM_DIR}/versions/node/v\${NODE_VERSION_DEVELOP}/bin/:\${PATH}" bench init --skip-redis-config-generation frappe-bench cd frappe-bench # Use containers instead of localhost bench set-mariadb-host mariadb bench set-redis-cache-host redis://redis:6379 bench set-redis-queue-host redis://redis:6379 bench set-redis-socketio-host redis://redis:6379 # Remove redis, watch from Procfile sed -i '/redis/d' ./Procfile sed -i '/watch/d' ./Procfile bench get-app erpnext bench get-app hrms bench new-site hrms.localhost --force --mariadb-root-password 123 --admin-password admin --no-mariadb-socket bench --site hrms.localhost install-app hrms bench --site hrms.localhost set-config developer_mode 1 bench --site hrms.localhost enable-scheduler bench --site hrms.localhost clear-cache bench use hrms.localhost bench start EOFMT } function performUpdateFrappeHR() { perform_stack_name=frappe-hr prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/mariadb:10.8.8,mirror.gcr.io/frappe/bench:v5.25.9,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/mariadb:10.8.8,mirror.gcr.io/mariadb:10.8.8" image_update_map[1]="mirror.gcr.io/frappe/bench:v5.25.9,mirror.gcr.io/frappe/bench:v5.25.9" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # MintHCM function installMintHCM() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory minthcm "MintHCM" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName minthcm-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName minthcm-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName minthcm-es) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/minthcm mkdir $HSHQ_STACKS_DIR/minthcm/db mkdir $HSHQ_STACKS_DIR/minthcm/dbexport mkdir $HSHQ_STACKS_DIR/minthcm/es mkdir $HSHQ_STACKS_DIR/minthcm/www mkdir $HSHQ_STACKS_DIR/minthcm/cron chmod 777 $HSHQ_STACKS_DIR/minthcm/dbexport initServicesCredentials set +e addUserMailu alias $MINTHCM_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS MINTHCM_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $MINTHCM_ADMIN_PASSWORD | tr -d ':\n') outputConfigMintHCM echo "Please be patient, this will take a few minutes..." installStack minthcm minthcm-web "Starting periodic command scheduler cron" $HOME/minthcm.env 30 900 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$MINTHCM_INIT_ENV" = "true" ]; then sendEmail -s "MintHCM Admin Login Info" -b "MintHCM Admin Username: $MINTHCM_ADMIN_USERNAME\nMintHCM Admin Password: $MINTHCM_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" MINTHCM_INIT_ENV=true updateConfigVar MINTHCM_INIT_ENV $MINTHCM_INIT_ENV fi sleep 3 echo "Setting up integrations..." docker exec minthcm-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/minthcm/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase MintHCM mysql minthcm-db $MINTHCM_DATABASE_NAME root $MINTHCM_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/minthcm/dbexport $MINTHCM_DATABASE_READONLYUSER $MINTHCM_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_MINTHCM.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://minthcm-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_MINTHCM $MANAGETLS_MINTHCM "$is_integrate_hshq" $NETDEFAULT_MINTHCM "$inner_block" insertSubAuthelia $SUB_MINTHCM.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll minthcm "$FMLNAME_MINTHCM" $USERTYPE_MINTHCM "https://$SUB_MINTHCM.$HOMESERVER_DOMAIN" "minthcm.png" "$(getHeimdallOrderFromSub $SUB_MINTHCM $USERTYPE_MINTHCM)" restartAllCaddyContainers checkAddDBConnection true minthcm "$FMLNAME_MINTHCM" mysql minthcm-db $MINTHCM_DATABASE_NAME $MINTHCM_DATABASE_USER $MINTHCM_DATABASE_USER_PASSWORD fi } function outputConfigMintHCM() { cat < $HOME/minthcm-compose.yml $STACK_VERSION_PREFIX minthcm $(getScriptStackVersion minthcm) services: minthcm-db: image: $(getScriptImageByContainerName minthcm-db) container_name: minthcm-db hostname: minthcm-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-minthcm-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-minthcm-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/minthcm/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.minthcm-hourly-db.schedule=@every 1h" - "ofelia.job-exec.minthcm-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.minthcm-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.minthcm-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.minthcm-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.minthcm-hourly-db.email-from=MintHCM Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.minthcm-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.minthcm-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.minthcm-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.minthcm-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.minthcm-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.minthcm-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.minthcm-monthly-db.email-from=MintHCM Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.minthcm-monthly-db.mail-only-on-error=false" minthcm-web: image: $(getScriptImageByContainerName minthcm-web) container_name: minthcm-web hostname: minthcm-www restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true tty: true stdin_open: true depends_on: - minthcm-db - minthcm-es networks: - int-minthcm-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-minthcm-www:/var/www/html - v-minthcm-cron:/var/spool/cron/crontabs minthcm-es: image: $(getScriptImageByContainerName minthcm-es) container_name: minthcm-es hostname: minthcm-es restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true mem_limit: 4g networks: - int-minthcm-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-minthcm-es:/usr/share/elasticsearch/data ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: v-minthcm-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/minthcm/db v-minthcm-es: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/minthcm/es v-minthcm-www: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/minthcm/www v-minthcm-cron: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/minthcm/cron networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-minthcm-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/minthcm.env TZ=\${PORTAINER_TZ} MYSQL_DATABASE=$MINTHCM_DATABASE_NAME MYSQL_ROOT_PASSWORD=$MINTHCM_DATABASE_ROOT_PASSWORD MYSQL_USER=$MINTHCM_DATABASE_USER MYSQL_PASSWORD=$MINTHCM_DATABASE_USER_PASSWORD SYS_NICE=CAP_SYS_NICE MYSQL_DEFAULT_AUTHENTICATION_PLUGIN=caching_sha2_password node.name=es1 cluster.name=minthcm discovery.type=single-node xpack.security.enabled=false ES_JAVA_OPTS=-Xms512m -Xmx512m WEB_PORT=80 DB_HOST=minthcm-db DB_PORT=3306 DB_NAME=$MINTHCM_DATABASE_NAME DB_USER=$MINTHCM_DATABASE_USER DB_PASS=$MINTHCM_DATABASE_USER_PASSWORD MINT_URL=https://$SUB_MINTHCM.$HOMESERVER_DOMAIN MINT_USER=$MINTHCM_ADMIN_USERNAME MINT_PASS=$MINTHCM_ADMIN_PASSWORD ELASTICSEARCH_HOST=minthcm-es ELASTICSEARCH_PORT=9200 ELASTICSEARCH_USERNAME=$MINTHCM_ES_USER ELASTICSEARCH_PASSWORD=$MINTHCM_ES_USER_PASSWORD INSTALL_DEMO_DATA=$(if [ "$IS_HSHQ_DEV_TEST" = "true" ]; then echo "yes"; else echo "no"; fi) SSL=no REBUILD_FRONTEND=no EOFMT cat < $HSHQ_STACKS_DIR/minthcm/dbexport/setupDBSettings.sh #!/bin/bash mysql -N -u $MINTHCM_DATABASE_USER -p$MINTHCM_DATABASE_USER_PASSWORD -e "use $MINTHCM_DATABASE_NAME; update outbound_email set smtp_from_name='MintCRM $(getAdminEmailName)', smtp_from_addr='$EMAIL_SMTP_EMAIL_ADDRESS', mail_smtpserver='mailu-front', mail_smtpauth_req=0 where user_id=1;" mysql -N -u $MINTHCM_DATABASE_USER -p$MINTHCM_DATABASE_USER_PASSWORD -e "use $MINTHCM_DATABASE_NAME; update users set first_name='$(getAdminEmailName)', last_name='$HOMESERVER_NAME' where id=1;" EOFDS chmod +x $HSHQ_STACKS_DIR/minthcm/dbexport/setupDBSettings.sh } function performUpdateMintHCM() { perform_stack_name=minthcm prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/percona:8.0.43-34,mirror.gcr.io/minthcm/minthcm:latest,docker.elastic.co/elasticsearch/elasticsearch:7.9.3 image_update_map[0]="mirror.gcr.io/percona:8.0.43-34,mirror.gcr.io/percona:8.0.43-34" image_update_map[1]="mirror.gcr.io/minthcm/minthcm:latest,mirror.gcr.io/minthcm/minthcm:latest" image_update_map[2]="docker.elastic.co/elasticsearch/elasticsearch:7.9.3,docker.elastic.co/elasticsearch/elasticsearch:7.9.3" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # CloudBeaver function installCloudBeaver() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory cloudbeaver "CloudBeaver" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName cloudbeaver-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/cloudbeaver mkdir $HSHQ_STACKS_DIR/cloudbeaver/workspace initServicesCredentials set +e outputConfigCloudBeaver installStack cloudbeaver cloudbeaver-app "" $HOME/cloudbeaver.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$CLOUDBEAVER_INIT_ENV" = "true" ]; then sendEmail -s "CloudBeaver Admin Login Info" -b "CloudBeaver Admin Username: $CLOUDBEAVER_ADMIN_USERNAME\nCloudBeaver Admin Password: $CLOUDBEAVER_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" CLOUDBEAVER_INIT_ENV=true updateConfigVar CLOUDBEAVER_INIT_ENV $CLOUDBEAVER_INIT_ENV fi sleep 3 set -e inner_block="" inner_block=$inner_block">>https://$SUB_CLOUDBEAVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://cloudbeaver-app:8978 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CLOUDBEAVER $MANAGETLS_CLOUDBEAVER "$is_integrate_hshq" $NETDEFAULT_CLOUDBEAVER "$inner_block" insertSubAuthelia $SUB_CLOUDBEAVER.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll cloudbeaver "$FMLNAME_CLOUDBEAVER" $USERTYPE_CLOUDBEAVER "https://$SUB_CLOUDBEAVER.$HOMESERVER_DOMAIN" "cloudbeaver.png" "$(getHeimdallOrderFromSub $SUB_CLOUDBEAVER $USERTYPE_CLOUDBEAVER)" restartAllCaddyContainers fi } function outputConfigCloudBeaver() { cat < $HOME/cloudbeaver-compose.yml $STACK_VERSION_PREFIX cloudbeaver $(getScriptStackVersion cloudbeaver) services: cloudbeaver-app: image: $(getScriptImageByContainerName cloudbeaver-app) container_name: cloudbeaver-app hostname: cloudbeaver-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/cloudbeaver/workspace:/opt/cloudbeaver/workspace networks: dock-proxy-net: name: dock-proxy external: true dock-dbs-net: name: dock-dbs external: true EOFMT cat < $HOME/cloudbeaver.env TZ=\${PORTAINER_TZ} CB_SERVER_NAME=$HOMESERVER_NAME - CloudBeaver CB_SERVER_URL=https://$SUB_CLOUDBEAVER.$HOMESERVER_DOMAIN CB_ADMIN_NAME=$CLOUDBEAVER_ADMIN_USERNAME CB_ADMIN_PASSWORD=$CLOUDBEAVER_ADMIN_PASSWORD EOFMT } function performUpdateCloudBeaver() { perform_stack_name=cloudbeaver prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/dbeaver/cloudbeaver:25.2.0 image_update_map[0]="mirror.gcr.io/dbeaver/cloudbeaver:25.2.0,mirror.gcr.io/dbeaver/cloudbeaver:25.3.0" ;; 2) newVer=v2 curImageList=mirror.gcr.io/dbeaver/cloudbeaver:25.3.0 image_update_map[0]="mirror.gcr.io/dbeaver/cloudbeaver:25.3.0,mirror.gcr.io/dbeaver/cloudbeaver:25.3.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Twenty function installTwenty() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory twenty "Twenty" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName twenty-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName twenty-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName twenty-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName twenty-minio) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName twenty-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/twenty mkdir $HSHQ_STACKS_DIR/twenty/data mkdir $HSHQ_STACKS_DIR/twenty/minio mkdir $HSHQ_STACKS_DIR/twenty/db mkdir $HSHQ_STACKS_DIR/twenty/dbexport chmod 777 $HSHQ_STACKS_DIR/twenty/dbexport initServicesCredentials set +e addUserMailu alias $TWENTY_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS TWENTY_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $TWENTY_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2b/' | sed 's/\$/\\\$/g') outputConfigTwenty installStack twenty twenty-app "Nest application successfully started" $HOME/twenty.env 15 900 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi #sendEmail -s "$FMLNAME_TWENTY Admin Login Info" -b "Below are some generated credentials that you can use to configure $FMLNAME_TWENTY. You will still need to go through the initial onboarding wizard the first time you access the site, and enter the information manually.\n\n$FMLNAME_TWENTY Admin Username: $TWENTY_ADMIN_USERNAME\n$FMLNAME_TWENTY Admin Email: $TWENTY_ADMIN_EMAIL_ADDRESS\n$FMLNAME_TWENTY Admin Password: $TWENTY_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" TWENTY_INIT_ENV=true updateConfigVar TWENTY_INIT_ENV $TWENTY_INIT_ENV sleep 3 performOnboardingTwenty addReadOnlyUserToDatabase Twenty postgres twenty-db $TWENTY_DATABASE_NAME $TWENTY_DATABASE_USER $TWENTY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/twenty/dbexport $TWENTY_DATABASE_READONLYUSER $TWENTY_DATABASE_READONLYUSER_PASSWORD addMCPServerLiteLLM "twenty" "twt" "http://twenty-mcp:80/mcp" http none "" set -e inner_block="" inner_block=$inner_block">>https://$SUB_TWENTY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://twenty-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_TWENTY $MANAGETLS_TWENTY "$is_integrate_hshq" $NETDEFAULT_TWENTY "$inner_block" insertSubAuthelia $SUB_TWENTY.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll twenty "$FMLNAME_TWENTY" $USERTYPE_TWENTY "https://$SUB_TWENTY.$HOMESERVER_DOMAIN" "twenty.png" "$(getHeimdallOrderFromSub $SUB_TWENTY $USERTYPE_TWENTY)" restartAllCaddyContainers checkAddDBConnection true twenty "$FMLNAME_TWENTY" postgres twenty-db $TWENTY_DATABASE_NAME $TWENTY_DATABASE_USER $TWENTY_DATABASE_USER_PASSWORD fi } function outputConfigTwenty() { cat < $HOME/twenty-compose.yml $STACK_VERSION_PREFIX twenty $(getScriptStackVersion twenty) services: twenty-db: image: $(getScriptImageByContainerName twenty-db) container_name: twenty-db hostname: twenty-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-twenty-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.twenty-hourly-db.schedule=@every 1h" - "ofelia.job-exec.twenty-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.twenty-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.twenty-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.twenty-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.twenty-hourly-db.email-from=Twenty Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.twenty-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.twenty-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.twenty-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.twenty-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.twenty-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.twenty-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.twenty-monthly-db.email-from=Twenty Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.twenty-monthly-db.mail-only-on-error=false" twenty-app: image: $(getScriptImageByContainerName twenty-app) container_name: twenty-app hostname: twenty-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - twenty-db networks: - int-twenty-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net environment: - DISABLE_DB_MIGRATIONS=false - DISABLE_CRON_JOBS_REGISTRATION=false volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-twenty-data:/app/packages/twenty-server/.local-storage twenty-worker: image: $(getScriptImageByContainerName twenty-worker) container_name: twenty-worker hostname: twenty-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: ["yarn", "worker:prod"] depends_on: - twenty-db networks: - int-twenty-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net environment: - DISABLE_DB_MIGRATIONS=true - DISABLE_CRON_JOBS_REGISTRATION=true volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-twenty-data:/app/packages/twenty-server/.local-storage twenty-minio: image: $(getScriptImageByContainerName twenty-minio) container_name: twenty-minio hostname: twenty-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data networks: - dock-proxy-net - int-twenty-net entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 /usr/bin/mc alias set minio http://twenty-minio:9000 ${TWENTY_MINIO_KEY} ${TWENTY_MINIO_SECRET} echo 'Creating bucket twenty' /usr/bin/mc mb minio/bucket-twenty wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/minio:/data twenty-mcp: image: $(getScriptImageByContainerName twenty-mcp) container_name: twenty-mcp hostname: twenty-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-twenty-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro twenty-redis: image: $(getScriptImageByContainerName twenty-redis) container_name: twenty-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $TWENTY_REDIS_PASSWORD --appendonly no --maxmemory-policy noeviction networks: - int-twenty-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-twenty-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/twenty/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-twenty-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/twenty.env TZ=\${PORTAINER_TZ} NODE_PORT=3000 NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt PG_DATABASE_URL=postgres://$TWENTY_DATABASE_USER:$TWENTY_DATABASE_USER_PASSWORD@twenty-db:5432/$TWENTY_DATABASE_NAME PG_DATABASE_HOST=twenty-db PG_DATABASE_PORT=5432 PG_DATABASE_USER=$TWENTY_DATABASE_USER PG_DATABASE_PASSWORD=$TWENTY_DATABASE_USER_PASSWORD POSTGRES_DB=$TWENTY_DATABASE_NAME POSTGRES_USER=$TWENTY_DATABASE_USER POSTGRES_PASSWORD=$TWENTY_DATABASE_USER_PASSWORD SERVER_URL=https://$SUB_TWENTY.$HOMESERVER_DOMAIN REDIS_URL=redis://:$TWENTY_REDIS_PASSWORD@twenty-redis:6379 STORAGE_TYPE=S3 STORAGE_S3_REGION=region-twenty STORAGE_S3_NAME=bucket-twenty STORAGE_S3_ENDPOINT=http://twenty-minio:9000 STORAGE_S3_ACCESS_KEY_ID=$TWENTY_MINIO_KEY STORAGE_S3_SECRET_ACCESS_KEY=$TWENTY_MINIO_SECRET MINIO_ROOT_USER=$TWENTY_MINIO_KEY MINIO_ROOT_PASSWORD=$TWENTY_MINIO_SECRET MINIO_DOMAIN=region-twenty MINIO_CONSOLE_ADDRESS=:9090 APP_SECRET=$TWENTY_APP_SECRET ENCRYPTION_KEY=$TWENTY_ENCRYPTION_KEY EMAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS EMAIL_FROM_NAME=Twenty $(getAdminEmailName) EMAIL_SYSTEM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS EMAIL_DRIVER=smtp EMAIL_SMTP_HOST=$SMTP_HOSTNAME EMAIL_SMTP_PORT=$SMTP_HOSTPORT OUTBOUND_HTTP_SAFE_MODE_ENABLED=false IS_IMAP_SMTP_CALDAV_ENABLED=true CALENDAR_PROVIDER_CALENDAR_ENABLED=true MESSAGING_PROVIDER_IMAP_ENABLED=true CALENDAR_PROVIDER_GOOGLE_ENABLED=false AI_PROVIDERS='{"litellm-proxy":{"npm":"@ai-sdk/openai-compatible","label":"LiteLLM Proxy","baseUrl":"http://litellm-proxy:4000/v1","apiKey":"$LITELLM_AUTH_KEY","models":[{"name":"LongContext","label":"LongContext"}]}}' AI_MODELS_DEFAULT_RECOMMENDED='["litellm-proxy/LongContext"]' TWENTY_BASE_URL=http://twenty-app:3000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFMT } function performOnboardingTwenty() { set +e local admin_email="$TWENTY_ADMIN_EMAIL_ADDRESS" local admin_password="$TWENTY_ADMIN_PASSWORD" local workspace_name="$HOMESERVER_NAME" local subdomain=$(echo "$HOMESERVER_DOMAIN" | cut -d"." -f1) local origin="https://$SUB_TWENTY.$HOMESERVER_DOMAIN" local admin_role_uid="20202020-02c2-43f2-b94d-cab1f2b532eb" local expires_at expires_at="$(date -u -d "+88 years" +%Y-%m-%dT%H:%M:%S.000Z)" local first_name="Twenty" local last_name="$(getAdminEmailName)" local position="Administrator" local t_admin_imap_host="$SMTP_HOSTNAME" local t_admin_imap_port="993" local t_admin_imap_user="$EMAIL_ADMIN_EMAIL_ADDRESS" local t_admin_imap_pass="$EMAIL_ADMIN_PASSWORD" local t_admin_imap_sec="SSL_TLS" local t_admin_smtp_host="$SMTP_HOSTNAME" local t_admin_smtp_port="587" local t_admin_smtp_user="$EMAIL_ADMIN_EMAIL_ADDRESS" local t_admin_smtp_pass="$EMAIL_ADMIN_PASSWORD" local t_admin_smtp_sec="STARTTLS" local t_admin_caldav_host="" local t_admin_caldav_port="443" local t_admin_caldav_user="" local t_admin_caldav_pass="" local t_admin_caldav_sec="" local is_nextcloud_installed=false docker ps | grep -q nextcloud-app > /dev/null 2>&1 if [ $? -eq 0 ]; then is_nextcloud_installed=true t_admin_caldav_host="https://$SUB_NEXTCLOUD.$HOMESERVER_DOMAIN/remote.php/dav" t_admin_caldav_port="443" t_admin_caldav_user="$NEXTCLOUD_ADMIN_USERNAME" t_admin_caldav_pass="$NEXTCLOUD_ADMIN_PASSWORD" t_admin_caldav_sec="SSL_TLS" fi log(){ echo "$*" >&2; } jwt_claim() { local b64="${1#*.}"; b64="${b64%.*}" local pad=$(( (4 - ${#b64} % 4) % 4 )) i for ((i=0;i/dev/null | jq -r "$2" } app_curl_exec(){ docker exec twenty-app curl -sS --max-time 30 "$@"; } gql(){ local query="$1" vars="$2" bearer="$3" local hdr=(-H 'Content-Type: application/json' -H "Origin: $origin") if [[ -n "$bearer" ]]; then hdr+=(-H "Authorization: Bearer $bearer"); fi app_curl_exec -X POST "${hdr[@]}" http://localhost:3000/metadata \ --data "$(jq -cn --arg q "$query" --argjson v "$vars" '{query:$q,variables:$v}')" } local access_token="" local email_exists email_exists="$(gql \ 'query($email:String!){ checkUserExists(email:$email){ exists } }' \ "{\"email\":\"$admin_email\"}" "" \ | jq -r '.data.checkUserExists.exists // "false"' 2>/dev/null || true)" if [[ "$email_exists" == "true" ]]; then log "$(date -u +%T) admin '$admin_email' already exists -> logging in" local login_token login_token="$(gql \ 'mutation($email:String!,$password:String!,$origin:String!){ getLoginTokenFromCredentials(email:$email,password:$password,origin:$origin){ loginToken{ token } } }' \ "{\"email\":\"$admin_email\",\"password\":\"$admin_password\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getLoginTokenFromCredentials.loginToken.token')" if [[ -z "$login_token" || "$login_token" == "null" ]]; then log "ERROR: password login failed for '$admin_email'. Check admin_password." >&2 return 1 fi access_token="$(gql \ 'mutation($loginToken:String!,$origin:String!){ getAuthTokensFromLoginToken(loginToken:$loginToken,origin:$origin){ tokens{ accessOrWorkspaceAgnosticToken{ token } } } }' \ "{\"loginToken\":\"$login_token\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token')" else log "$(date -u +%T) admin '$admin_email' does not exist -> onboarding" local agnostic_token agnostic_token="$(gql \ 'mutation($email:String!,$password:String!,$locale:String!){ signUp(email:$email,password:$password,locale:$locale){ tokens{ accessOrWorkspaceAgnosticToken{ token } } } }' \ "{\"email\":\"$admin_email\",\"password\":\"$admin_password\",\"locale\":\"en\"}" "" \ | jq -r '.data.signUp.tokens.accessOrWorkspaceAgnosticToken.token')" if [[ -z "$agnostic_token" || "$agnostic_token" == "null" ]]; then log "ERROR: signUp failed. Is onboarding already completed for '$admin_email'?" >&2 return 1 fi local login_token login_token="$(gql \ 'mutation($input:SignUpInNewWorkspaceInput!){ signUpInNewWorkspace(input:$input){ loginToken{ token } workspace{ id } } }' \ "{\"input\":{\"displayName\":\"$workspace_name\",\"subdomain\":\"$subdomain\"}}" "$agnostic_token" \ | jq -r '.data.signUpInNewWorkspace.loginToken.token')" if [[ -z "$login_token" || "$login_token" == "null" ]]; then log "ERROR: signUpInNewWorkspace failed." >&2 return 1 fi access_token="$(gql \ 'mutation($loginToken:String!,$origin:String!){ getAuthTokensFromLoginToken(loginToken:$loginToken,origin:$origin){ tokens{ accessOrWorkspaceAgnosticToken{ token } } } }' \ "{\"loginToken\":\"$login_token\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token')" fi if [[ -z "$access_token" || "$access_token" == "null" ]]; then log "ERROR: could not obtain an ACCESS token." >&2 return 1 fi log "$(date -u +%T) ensuring workspace is active (activateWorkspace)" local activated activated="$(gql \ 'mutation ActivateWorkspace($input: ActivateWorkspaceInput!){ activateWorkspace(data:$input){ id } }' \ '{"input":{}}' "$access_token" \ | jq -r '.data.activateWorkspace.id' 2>/dev/null || true)" if [[ -z "$activated" || "$activated" == "null" ]]; then log "ERROR: workspace activation failed. Check twenty-app/twenty-worker logs." >&2 return 1 fi local lt2 lt2="$(gql \ 'mutation($email:String!,$password:String!,$origin:String!){ getLoginTokenFromCredentials(email:$email,password:$password,origin:$origin){ loginToken{ token } } }' \ "{\"email\":\"$admin_email\",\"password\":\"$admin_password\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getLoginTokenFromCredentials.loginToken.token')" access_token="$(gql \ 'mutation($loginToken:String!,$origin:String!){ getAuthTokensFromLoginToken(loginToken:$loginToken,origin:$origin){ tokens{ accessOrWorkspaceAgnosticToken{ token } } } }' \ "{\"loginToken\":\"$lt2\",\"origin\":\"$origin\"}" "" \ | jq -r '.data.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token')" local user_id workspace_id wm_id pset user_id="$( jwt_claim "$access_token" '.userId // .sub')" workspace_id="$(jwt_claim "$access_token" '.workspaceId')" wm_id="$( jwt_claim "$access_token" '.workspaceMemberId')" pset="$(gql \ 'mutation($input: UpdateWorkspaceMemberSettingsInput!){ updateWorkspaceMemberSettings(input:$input) }' \ "{\"input\":{\"workspaceMemberId\":\"$wm_id\",\"update\":{\"name\":{\"firstName\":\"$first_name\",\"lastName\":\"$last_name\"},\"jobTitle\":\"$position\"}}}" \ "$access_token")" if [[ "$(printf '%s' "$pset" | jq -r '.data.updateWorkspaceMemberSettings // false' 2>/dev/null)" != "true" ]]; then log "WARN: could not set admin profile: $pset" >&2 fi local admin_handle admin_conn_params admin_save admin_handle="$(printf '%s' "$admin_email" | tr '[:upper:]' '[:lower:]')" admin_conn_params="$(jq -cn \ --arg n "$admin_handle" \ --arg ih "$t_admin_imap_host" --argjson ip "$t_admin_imap_port" \ --arg iu "$t_admin_imap_user" --arg ipa "$t_admin_imap_pass" --arg is "$t_admin_imap_sec" \ --arg sh "$t_admin_smtp_host" --argjson sp "$t_admin_smtp_port" \ --arg su "$t_admin_smtp_user" --arg spa "$t_admin_smtp_pass" --arg ss "$t_admin_smtp_sec" \ --arg ch "$t_admin_caldav_host" --argjson cp "$t_admin_caldav_port" \ --arg cu "$t_admin_caldav_user" --arg cpa "$t_admin_caldav_pass" --arg cs "$t_admin_caldav_sec" \ '{name:$n, IMAP:{host:$ih,port:$ip,username:$iu,password:$ipa,connectionSecurity:$is}, SMTP:{host:$sh,port:$sp,username:$su,password:$spa,connectionSecurity:$ss}} + (if ($ch != "") then {CALDAV:{host:$ch,port:$cp,username:$cu,password:$cpa,connectionSecurity:$cs}} else {} end)')" admin_save="$(gql \ 'mutation($handle:String!,$connectionParameters:EmailAccountConnectionParameters!){ saveImapSmtpCaldavAccount(handle:$handle,connectionParameters:$connectionParameters){ success connectedAccountId } }' \ "$(jq -cn --arg h "$admin_handle" --argjson cp "$admin_conn_params" '{handle:$h,connectionParameters:$cp}')" \ "$access_token")" if [[ "$(printf '%s' "$admin_save" | jq -r '.data.saveImapSmtpCaldavAccount.success // false' 2>/dev/null)" != "true" ]]; then log "ERROR: saveImapSmtpCaldavAccount failed for admin: $admin_save" >&2 return 1 fi local acct_id mc_id acct_id="$(printf '%s' "$admin_save" | jq -r '.data.saveImapSmtpCaldavAccount.connectedAccountId // empty')" if [[ -z "$acct_id" ]]; then log "ERROR: could not resolve connectedAccountId for admin mail sync." >&2 return 1 fi mc_id="$(docker exec -i -e AID="$acct_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT id FROM core.\"messageChannel\" WHERE \"connectedAccountId\" = '\''$AID'\''" ' 2>/dev/null | tr -d '[:space:]')" if [[ -n "$mc_id" ]]; then docker exec -i -e MCID="$mc_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "UPDATE core.\"messageChannel\" SET \"messageFolderImportPolicy\" = '\''SELECTED_FOLDERS'\'' WHERE id = '\''$MCID'\''; UPDATE core.\"messageFolder\" SET \"isSynced\" = false WHERE \"messageChannelId\" = '\''$MCID'\''; UPDATE core.\"messageFolder\" f SET \"isSynced\" = true FROM core.\"messageFolder\" p WHERE f.\"messageChannelId\" = '\''$MCID'\'' AND f.name = '\''Work'\'' AND p.\"externalId\" = f.\"parentFolderId\" AND p.name = '\''Processed'\'' AND p.\"messageChannelId\" = '\''$MCID'\'';" ' >/dev/null 2>&1 local synced_work unsynced_count synced_work="$(docker exec -i -e MCID="$mc_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT count(*) FROM core.\"messageFolder\" f JOIN core.\"messageFolder\" p ON p.\"externalId\" = f.\"parentFolderId\" WHERE f.\"messageChannelId\" = '\''$MCID'\'' AND f.name = '\''Work'\'' AND p.name = '\''Processed'\'' AND f.\"isSynced\" = true" ' 2>/dev/null | tr -d '[:space:]')" unsynced_count="$(docker exec -i -e MCID="$mc_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT count(*) FROM core.\"messageFolder\" WHERE \"messageChannelId\" = '\''$MCID'\'' AND NOT \"isSynced\"" ' 2>/dev/null | tr -d '[:space:]')" log "$(date -u +%T) email folder policy = SELECTED_FOLDERS (Processed -> Work synced=$synced_work, others unsynced=$unsynced_count)" if [[ "$synced_work" != "1" ]]; then log "WARN: Processed -> Work folder was not set as the only synced folder (synced_work=$synced_work)." >&2 fi fi docker exec -i -e AID="$acct_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "UPDATE core.\"calendarChannel\" SET visibility = '\''SHARE_EVERYTHING'\'' WHERE \"connectedAccountId\" = '\''$AID'\''" ' >/dev/null 2>&1 gql \ 'mutation($connectedAccountId: UUID!){ startChannelSync(connectedAccountId: $connectedAccountId){ success } }' \ "{\"connectedAccountId\":\"$acct_id\"}" "$access_token" >/dev/null log "$(date -u +%T) channel sync started -> account setup autocompleted" docker exec -i -e ADMIN_WSID="$workspace_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "DELETE FROM core.\"keyValuePair\" WHERE \"type\"='"'"'USER_VARIABLE'"'"' AND \"workspaceId\"='"'"'$ADMIN_WSID'"'"' AND \"key\" IN ('"'"'ONBOARDING_CONNECT_ACCOUNT_PENDING'"'"','"'"'ONBOARDING_INSTALL_APPS_PENDING'"'"','"'"'ONBOARDING_INVITE_TEAM_PENDING'"'"')" ' >/dev/null 2>&1 local ost ost="$(gql '{ currentUser{ onboardingStatus } }' '{}' "$access_token" \ | jq -r '.data.currentUser.onboardingStatus // "ERROR"' 2>/dev/null || true)" if [[ "$ost" != "COMPLETED" ]]; then log "ERROR: onboardingStatus='$ost' (expected COMPLETED)." >&2 return 1 fi local role_id role_id="$(docker exec -i -e ADMIN_ROLE_UID="$admin_role_uid" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT id FROM core.role WHERE \"universalIdentifier\" = '\''$ADMIN_ROLE_UID'\''" ' 2>/dev/null | tr -d '[:space:]')" if [[ -z "$role_id" ]]; then log "ERROR: admin role (universalIdentifier '$admin_role_uid') not found — did workspace activation complete?" >&2 return 1 fi local member_role_id member_role_id="$(docker exec -i -e FIRST_WSID="$workspace_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "SELECT \"defaultRoleId\" FROM core.workspace WHERE id = '\''$FIRST_WSID'\''" ' 2>/dev/null | tr -d '[:space:]')" if [[ -n "$member_role_id" ]]; then docker exec -i -e MEMBER_ROLE_ID="$member_role_id" twenty-db sh -c ' PGPASSWORD="$POSTGRES_PASSWORD" psql -qtA -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -c "UPDATE core.role SET \"canBeAssignedToApiKeys\" = true WHERE id = '\''$MEMBER_ROLE_ID'\'' AND NOT \"canBeAssignedToApiKeys\"" ' >/dev/null 2>&1 log "$(date -u +%T) ensured Member role ($member_role_id) is API-key-assignable" else log "WARN: could not resolve default (Member) role — skipping API-key assignability setup" >&2 fi local key_name="MCP-HSHQAdmin" local key_id key_id="$(app_curl_exec -sS -X GET -H "Authorization: Bearer $access_token" \ -H 'Content-Type: application/json' \ --max-time 30 "http://localhost:3000/rest/apiKeys" 2>/dev/null \ | jq -r --arg n "$key_name" '.[]? | select(.name==$n and .revokedAt==null) | .id' 2>/dev/null \ | head -n1)" if [[ -z "$key_id" ]]; then log "$(date -u +%T) no non-revoked key named '$key_name' -> minting one (88y)" key_id="$(app_curl_exec -sS -X POST -H "Authorization: Bearer $access_token" \ -H 'Content-Type: application/json' \ --data "$(jq -cn --arg n "$key_name" --arg e "$expires_at" --arg r "$role_id" \ '{name:$n,expiresAt:$e,roleId:$r}')" \ "http://localhost:3000/rest/apiKeys" 2>/dev/null \ | jq -r '.id' 2>/dev/null)" if [[ -z "$key_id" || "$key_id" == "null" ]]; then log "ERROR: could not create API key." >&2 return 1 fi else log "$(date -u +%T) reusing existing key '$key_name' ($key_id)" fi local token token="$(gql \ 'mutation($apiKeyId:UUID!,$expiresAt:String!){ generateApiKeyToken(apiKeyId:$apiKeyId,expiresAt:$expiresAt){ token } }' \ "{\"apiKeyId\":\"$key_id\",\"expiresAt\":\"$expires_at\"}" "$access_token" \ | jq -r '.data.generateApiKeyToken.token')" if [[ -z "$token" || "$token" == "null" ]]; then log "ERROR: could not generate API key token." >&2 return 1 fi TWENTY_ADMIN_API_KEY=$(printf '%s\n' "$token") updateConfigVar TWENTY_ADMIN_API_KEY "$TWENTY_ADMIN_API_KEY" key_name="AutoKB" key_id="$(app_curl_exec -sS -X GET -H "Authorization: Bearer $access_token" \ -H 'Content-Type: application/json' \ --max-time 30 "http://localhost:3000/rest/apiKeys" 2>/dev/null \ | jq -r --arg n "$key_name" '.[]? | select(.name==$n and .revokedAt==null) | .id' 2>/dev/null \ | head -n1)" if [[ -z "$key_id" ]]; then log "$(date -u +%T) no non-revoked key named '$key_name' -> minting one (88y)" key_id="$(app_curl_exec -sS -X POST -H "Authorization: Bearer $access_token" \ -H 'Content-Type: application/json' \ --data "$(jq -cn --arg n "$key_name" --arg e "$expires_at" --arg r "$role_id" \ '{name:$n,expiresAt:$e,roleId:$r}')" \ "http://localhost:3000/rest/apiKeys" 2>/dev/null \ | jq -r '.id' 2>/dev/null)" if [[ -z "$key_id" || "$key_id" == "null" ]]; then log "ERROR: could not create API key." >&2 return 1 fi else log "$(date -u +%T) reusing existing key '$key_name' ($key_id)" fi token="$(gql \ 'mutation($apiKeyId:UUID!,$expiresAt:String!){ generateApiKeyToken(apiKeyId:$apiKeyId,expiresAt:$expiresAt){ token } }' \ "{\"apiKeyId\":\"$key_id\",\"expiresAt\":\"$expires_at\"}" "$access_token" \ | jq -r '.data.generateApiKeyToken.token')" if [[ -z "$token" || "$token" == "null" ]]; then log "ERROR: could not generate API key token." >&2 return 1 fi TWENTY_AKB_API_KEY=$(printf '%s\n' "$token") updateConfigVar TWENTY_AKB_API_KEY "$TWENTY_AKB_API_KEY" } function performUpdateTwenty() { perform_stack_name=twenty prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/twentycrm/twenty:v1.5.3,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/twentycrm/twenty:v1.5.3,mirror.gcr.io/twentycrm/twenty:v1.12.0" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[4]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/twentycrm/twenty:v1.12.0,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[2]="mirror.gcr.io/twentycrm/twenty:v1.12.0,mirror.gcr.io/twentycrm/twenty:v1.12.0" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[4]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true fixV2ComposeTwenty perform_update_report="${perform_update_report}$stack_upgrade_report" startStopStack twenty stop sleep 2 startStopStack twenty start return ;; 3) newVer=v4 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/twentycrm/twenty:v1.12.0,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[2]="mirror.gcr.io/twentycrm/twenty:v1.12.0,mirror.gcr.io/twentycrm/twenty:v2.16.0" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" ;; 4) newVer=v5 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/twentycrm/twenty:v2.16.0,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[2]="mirror.gcr.io/twentycrm/twenty:v2.16.0,mirror.gcr.io/twentycrm/twenty:v2.21.0" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfTwentyUpdateV5 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v6 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/twentycrm/twenty:v2.21.0,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/twenty-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[2]="mirror.gcr.io/twentycrm/twenty:v2.21.0,mirror.gcr.io/twentycrm/twenty:v2.21.0" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="ghcr.io/homeserverhq/twenty-mcp:v2,ghcr.io/homeserverhq/twenty-mcp:v2" ;; 6) newVer=v6 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/twentycrm/twenty:v2.21.0,ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/twenty-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[2]="mirror.gcr.io/twentycrm/twenty:v2.21.0,mirror.gcr.io/twentycrm/twenty:v2.21.0" image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="ghcr.io/homeserverhq/twenty-mcp:v2,ghcr.io/homeserverhq/twenty-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function fixV2ComposeTwenty() { rm -f $HOME/twenty-compose.yml cat < $HOME/twenty-compose.yml $STACK_VERSION_PREFIX twenty v3 services: twenty-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: twenty-db hostname: twenty-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-twenty-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.twenty-hourly-db.schedule=@every 1h" - "ofelia.job-exec.twenty-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.twenty-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.twenty-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.twenty-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.twenty-hourly-db.email-from=Twenty Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.twenty-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.twenty-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.twenty-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.twenty-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.twenty-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.twenty-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.twenty-monthly-db.email-from=Twenty Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.twenty-monthly-db.mail-only-on-error=false" twenty-app: image: mirror.gcr.io/twentycrm/twenty:v1.12.0 container_name: twenty-app hostname: twenty-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - twenty-db networks: - int-twenty-net - dock-ext-net - dock-internalmail-net environment: - DISABLE_DB_MIGRATIONS=false - DISABLE_CRON_JOBS_REGISTRATION=false volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-twenty-data:/app/packages/twenty-server/.local-storage twenty-worker: image: mirror.gcr.io/twentycrm/twenty:v1.12.0 container_name: twenty-worker hostname: twenty-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: ["yarn", "worker:prod"] depends_on: - twenty-db networks: - int-twenty-net - dock-ext-net - dock-internalmail-net environment: - DISABLE_DB_MIGRATIONS=true - DISABLE_CRON_JOBS_REGISTRATION=true volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-twenty-data:/app/packages/twenty-server/.local-storage twenty-redis: image: mirror.gcr.io/redis:8.4.0-bookworm container_name: twenty-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $TWENTY_REDIS_PASSWORD --appendonly no --maxmemory-policy noeviction networks: - int-twenty-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro twenty-minio: image: mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z container_name: twenty-minio hostname: twenty-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data networks: - dock-proxy-net - int-twenty-net entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 /usr/bin/mc alias set minio http://twenty-minio:9000 ${TWENTY_MINIO_KEY} ${TWENTY_MINIO_SECRET} echo 'Creating bucket twenty' /usr/bin/mc mb minio/bucket-twenty wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/minio:/data volumes: v-twenty-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/twenty/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-twenty-net: driver: bridge internal: true ipam: driver: default EOFMT } function mfTwentyUpdateV5() { buildOrPullImage $(getScriptImageByContainerName twenty-mcp) if [ $? -ne 0 ]; then return 1 fi cat < $HOME/twenty-compose.yml $STACK_VERSION_PREFIX twenty v5 services: twenty-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: twenty-db hostname: twenty-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-twenty-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.twenty-hourly-db.schedule=@every 1h" - "ofelia.job-exec.twenty-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.twenty-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.twenty-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.twenty-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.twenty-hourly-db.email-from=Twenty Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.twenty-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.twenty-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.twenty-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.twenty-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.twenty-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.twenty-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.twenty-monthly-db.email-from=Twenty Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.twenty-monthly-db.mail-only-on-error=false" twenty-app: image: mirror.gcr.io/twentycrm/twenty:v2.21.0 container_name: twenty-app hostname: twenty-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - twenty-db networks: - int-twenty-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net environment: - DISABLE_DB_MIGRATIONS=false - DISABLE_CRON_JOBS_REGISTRATION=false volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-twenty-data:/app/packages/twenty-server/.local-storage twenty-worker: image: mirror.gcr.io/twentycrm/twenty:v2.21.0 container_name: twenty-worker hostname: twenty-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: ["yarn", "worker:prod"] depends_on: - twenty-db networks: - int-twenty-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net environment: - DISABLE_DB_MIGRATIONS=true - DISABLE_CRON_JOBS_REGISTRATION=true volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-twenty-data:/app/packages/twenty-server/.local-storage twenty-minio: image: mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z container_name: twenty-minio hostname: twenty-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data networks: - dock-proxy-net - int-twenty-net entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 /usr/bin/mc alias set minio http://twenty-minio:9000 ${TWENTY_MINIO_KEY} ${TWENTY_MINIO_SECRET} echo 'Creating bucket twenty' /usr/bin/mc mb minio/bucket-twenty wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/twenty/minio:/data twenty-mcp: image: ghcr.io/homeserverhq/twenty-mcp:v2 container_name: twenty-mcp hostname: twenty-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-twenty-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro twenty-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: twenty-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $TWENTY_REDIS_PASSWORD --appendonly no --maxmemory-policy noeviction networks: - int-twenty-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-twenty-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/twenty/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-twenty-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/twenty.env TZ=\${PORTAINER_TZ} NODE_PORT=3000 NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt PG_DATABASE_URL=postgres://$TWENTY_DATABASE_USER:$TWENTY_DATABASE_USER_PASSWORD@twenty-db:5432/$TWENTY_DATABASE_NAME PG_DATABASE_HOST=twenty-db PG_DATABASE_PORT=5432 PG_DATABASE_USER=$TWENTY_DATABASE_USER PG_DATABASE_PASSWORD=$TWENTY_DATABASE_USER_PASSWORD POSTGRES_DB=$TWENTY_DATABASE_NAME POSTGRES_USER=$TWENTY_DATABASE_USER POSTGRES_PASSWORD=$TWENTY_DATABASE_USER_PASSWORD SERVER_URL=https://$SUB_TWENTY.$HOMESERVER_DOMAIN REDIS_URL=redis://:$TWENTY_REDIS_PASSWORD@twenty-redis:6379 STORAGE_TYPE=S3 STORAGE_S3_REGION=region-twenty STORAGE_S3_NAME=bucket-twenty STORAGE_S3_ENDPOINT=http://twenty-minio:9000 STORAGE_S3_ACCESS_KEY_ID=$TWENTY_MINIO_KEY STORAGE_S3_SECRET_ACCESS_KEY=$TWENTY_MINIO_SECRET MINIO_ROOT_USER=$TWENTY_MINIO_KEY MINIO_ROOT_PASSWORD=$TWENTY_MINIO_SECRET MINIO_DOMAIN=region-twenty MINIO_CONSOLE_ADDRESS=:9090 APP_SECRET=$TWENTY_APP_SECRET ENCRYPTION_KEY=$TWENTY_ENCRYPTION_KEY EMAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS EMAIL_FROM_NAME=Twenty $(getAdminEmailName) EMAIL_SYSTEM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS EMAIL_DRIVER=smtp EMAIL_SMTP_HOST=$SMTP_HOSTNAME EMAIL_SMTP_PORT=$SMTP_HOSTPORT OUTBOUND_HTTP_SAFE_MODE_ENABLED=false IS_IMAP_SMTP_CALDAV_ENABLED=true CALENDAR_PROVIDER_CALENDAR_ENABLED=true MESSAGING_PROVIDER_IMAP_ENABLED=true CALENDAR_PROVIDER_GOOGLE_ENABLED=false AI_PROVIDERS='{"litellm-proxy":{"npm":"@ai-sdk/openai-compatible","label":"LiteLLM Proxy","baseUrl":"http://litellm-proxy:4000/v1","apiKey":"$LITELLM_AUTH_KEY","models":[{"name":"LongContext","label":"LongContext"}]}}' TWENTY_BASE_URL=http://twenty-app:3000 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFMT addMCPServerLiteLLM "twenty" "twt" "http://twenty-mcp:80/mcp" http none "" } # Odoo function installOdoo() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory odoo "Odoo" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName odoo-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName odoo-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/odoo mkdir $HSHQ_STACKS_DIR/odoo/addons mkdir $HSHQ_STACKS_DIR/odoo/config mkdir $HSHQ_STACKS_DIR/odoo/db mkdir $HSHQ_STACKS_DIR/odoo/dbexport mkdir $HSHQ_STACKS_DIR/odoo/web chmod 777 $HSHQ_STACKS_DIR/odoo/dbexport initServicesCredentials set +e addUserMailu alias $ODOO_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS ODOO_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $ODOO_ADMIN_PASSWORD | tr -d ':\n') odbname=$ODOO_DATABASE_NAME ODOO_DATABASE_NAME=postgres outputConfigOdoo ODOO_DATABASE_NAME=$odbname installStack odoo odoo-app "running on odoo-app:8069" $HOME/odoo.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$ODOO_INIT_ENV" = "true" ]; then sendEmail -s "Odoo Admin Login Info" -b "Odoo Admin Username: $ODOO_ADMIN_USERNAME\nOdoo Admin Password: $ODOO_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" ODOO_INIT_ENV=true updateConfigVar ODOO_INIT_ENV $ODOO_INIT_ENV fi inner_block="" inner_block=$inner_block">>https://$SUB_ODOO.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://odoo-app:8069 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ODOO $MANAGETLS_ODOO "$is_integrate_hshq" $NETDEFAULT_ODOO "$inner_block" insertSubAuthelia $SUB_ODOO.$HOMESERVER_DOMAIN bypass restartAllCaddyContainers sleep 5 set +e echo "Initializing Odoo database..." num_tries=0 total_tries=5 while [ $num_tries -lt $total_tries ] do curl "https://$SUB_ODOO.$HOMESERVER_DOMAIN/" > /dev/null 2>&1 if [ $? -eq 0 ]; then break fi echo "Could not reach Odoo subdomain, retrying in 5 seconds..." sleep 5 ((num_tries++)) done curl "https://$SUB_ODOO.$HOMESERVER_DOMAIN/web/database/create" --data "master_pwd=$ODOO_ADMIN_PASSWORD&name=$ODOO_DATABASE_NAME&login=$ODOO_ADMIN_USERNAME&password=$ODOO_ADMIN_PASSWORD&phone=5551212&lang=en_US&country_code=us" --compressed > /dev/null 2>&1 echo "Modifying initial settings..." docker exec -it odoo-app bash -c "odoo shell -p 8070 --db_host odoo-db -r $ODOO_DATABASE_USER -w $ODOO_DATABASE_USER_PASSWORD -d $ODOO_DATABASE_NAME < /etc/odoo/initconfig.py" > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/odoo/config/initconfig.py echo "Adding modules..." docker exec -it odoo-app bash -c "odoo -p 8070 --db_host odoo-db -r $ODOO_DATABASE_USER -w $ODOO_DATABASE_USER_PASSWORD -d $ODOO_DATABASE_NAME -i crm,mail,account,account_invoicing,hr,hr_expense,planning --stop-after-init --without-demo=True" > /dev/null 2>&1 dtnow=$(date '+%Y-%m-%d %H:%M:%S.%3N') cat < $HSHQ_STACKS_DIR/odoo/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$ODOO_DATABASE_USER_PASSWORD echo "insert into ir_mail_server(id, smtp_port, sequence, create_uid, write_uid, name, smtp_host, smtp_authentication, smtp_encryption, smtp_debug, active, create_date, write_date, max_email_size) values(1,$SMTP_HOSTPORT,10,2,2,'HSHQ','$SMTP_HOSTNAME','login','starttls_strict',false,true,'$dtnow', '$dtnow',0);" | psql -U $ODOO_DATABASE_USER $ODOO_DATABASE_NAME echo "update ir_module_module set state='to remove' where name in ('google_gmail', 'microsoft_outlook');" | psql -U $ODOO_DATABASE_USER $ODOO_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/odoo/dbexport/setupDBSettings.sh docker exec odoo-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/odoo/dbexport/setupDBSettings.sh docker exec -it odoo-app bash -c "odoo -p 8070 --db_host odoo-db -r $ODOO_DATABASE_USER -w $ODOO_DATABASE_USER_PASSWORD -d $ODOO_DATABASE_NAME -u google_gmail,microsoft_outlook --stop-after-init --without-demo=True" > /dev/null 2>&1 addReadOnlyUserToDatabase Odoo postgres odoo-db $ODOO_DATABASE_NAME $ODOO_DATABASE_USER $ODOO_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/odoo/dbexport $ODOO_DATABASE_READONLYUSER $ODOO_DATABASE_READONLYUSER_PASSWORD echo "Odoo Initialization complete!" echo "Restarting stack..." updateStackEnv odoo outputEnvOdoo > /dev/null 2>&1 set -e if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll odoo "$FMLNAME_ODOO" $USERTYPE_ODOO "https://$SUB_ODOO.$HOMESERVER_DOMAIN" "odoo.png" "$(getHeimdallOrderFromSub $SUB_ODOO $USERTYPE_ODOO)" restartAllCaddyContainers checkAddDBConnection true odoo "$FMLNAME_ODOO" postgres odoo-db $ODOO_DATABASE_NAME $ODOO_DATABASE_USER $ODOO_DATABASE_USER_PASSWORD fi } function outputConfigOdoo() { cat < $HOME/odoo-compose.yml $STACK_VERSION_PREFIX odoo $(getScriptStackVersion odoo) services: odoo-db: image: $(getScriptImageByContainerName odoo-db) container_name: odoo-db hostname: odoo-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-odoo-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/odoo/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/odoo/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.odoo-hourly-db.schedule=@every 1h" - "ofelia.job-exec.odoo-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.odoo-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.odoo-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.odoo-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.odoo-hourly-db.email-from=Odoo Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.odoo-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.odoo-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.odoo-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.odoo-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.odoo-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.odoo-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.odoo-monthly-db.email-from=Odoo Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.odoo-monthly-db.mail-only-on-error=false" odoo-app: image: $(getScriptImageByContainerName odoo-app) container_name: odoo-app hostname: odoo-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - odoo-db networks: - int-odoo-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-odoo-web:/var/lib/odoo - \${PORTAINER_HSHQ_STACKS_DIR}/odoo/config:/etc/odoo - \${PORTAINER_HSHQ_STACKS_DIR}/odoo/addons:/mnt/extra-addons volumes: v-odoo-web: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/odoo/web networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-odoo-net: driver: bridge internal: true ipam: driver: default EOFMT outputEnvOdoo cat < $HSHQ_STACKS_DIR/odoo/config/initconfig.py env['res.company'].search([], limit=1).write({'name': '$HOMESERVER_NAME'}) user = env['res.users'].search([('login','=','$ODOO_ADMIN_USERNAME')]) user.name = 'Odoo $(getAdminEmailName)' user.complete_name = '$HOMESERVER_NAME Odoo $(getAdminEmailName)' user.tz = '$TZ' user.email = '$ODOO_ADMIN_EMAIL_ADDRESS' user.signature = '

$HOMESERVER_NAME Odoo $(getAdminEmailName)

' self.env.cr.commit() EOFMT } function outputEnvOdoo() { rm -f $HOME/odoo.env cat < $HOME/odoo.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$ODOO_DATABASE_NAME POSTGRES_USER=$ODOO_DATABASE_USER POSTGRES_PASSWORD=$ODOO_DATABASE_USER_PASSWORD HOST=odoo-db USER=$ODOO_DATABASE_USER PASSWORD=$ODOO_DATABASE_USER_PASSWORD EOFMT } function performUpdateOdoo() { perform_stack_name=odoo prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/odoo:19.0-20250918 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/odoo:19.0-20250918,mirror.gcr.io/odoo:19.0-20251121" ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/odoo:19.0-20251121 image_update_map[0]="mirror.gcr.io/postgres:15.0-bullseye,mirror.gcr.io/postgres:15.0-bullseye" image_update_map[1]="mirror.gcr.io/odoo:19.0-20251121,mirror.gcr.io/odoo:19.0-20251121" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Calcom function installCalcom() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory calcom "Calcom" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName calcom-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName calcom-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/calcom mkdir $HSHQ_STACKS_DIR/calcom/db mkdir $HSHQ_STACKS_DIR/calcom/dbexport chmod 777 $HSHQ_STACKS_DIR/calcom/dbexport initServicesCredentials set +e addUserMailu alias $CALCOM_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS CALCOM_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 12 "" $CALCOM_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2a/' | sed 's/\$/\\\$/g') outputConfigCalcom installStack calcom calcom-app "Ready in" $HOME/calcom.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$CALCOM_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_CALCOM_APP Admin Login Info" -b "$FMLNAME_CALCOM_APP Admin Email: $CALCOM_ADMIN_EMAIL_ADDRESS\n$FMLNAME_CALCOM_APP Admin Password: $CALCOM_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" CALCOM_INIT_ENV=true updateConfigVar CALCOM_INIT_ENV $CALCOM_INIT_ENV fi sleep 3 docker exec calcom-db /dbexport/setupDBSettings.sh rm -f $HSHQ_STACKS_DIR/calcom/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase Calcom postgres calcom-db $CALCOM_DATABASE_NAME $CALCOM_DATABASE_USER $CALCOM_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/calcom/dbexport $CALCOM_DATABASE_READONLYUSER $CALCOM_DATABASE_READONLYUSER_PASSWORD startStopStack calcom stop startStopStack calcom start set -e inner_block="" inner_block=$inner_block">>https://$SUB_CALCOM_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://calcom-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CALCOM_APP $MANAGETLS_CALCOM_APP "$is_integrate_hshq" $NETDEFAULT_CALCOM_APP "$inner_block" insertSubAuthelia $SUB_CALCOM_APP.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_CALCOM_STUDIO.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://calcom-studio:5555 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CALCOM_STUDIO $MANAGETLS_CALCOM_STUDIO "$is_integrate_hshq" $NETDEFAULT_CALCOM_STUDIO "$inner_block" insertSubAuthelia $SUB_CALCOM_STUDIO.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll calcom "$FMLNAME_CALCOM_APP" $USERTYPE_CALCOM_APP "https://$SUB_CALCOM_APP.$HOMESERVER_DOMAIN" "calcom.png" "$(getHeimdallOrderFromSub $SUB_CALCOM_APP $USERTYPE_CALCOM_APP)" restartAllCaddyContainers checkAddDBConnection true calcom "$FMLNAME_CALCOM_APP" postgres calcom-db $CALCOM_DATABASE_NAME $CALCOM_DATABASE_USER $CALCOM_DATABASE_USER_PASSWORD fi } function outputConfigCalcom() { cat < $HOME/calcom-compose.yml $STACK_VERSION_PREFIX calcom $(getScriptStackVersion calcom) services: calcom-db: image: $(getScriptImageByContainerName calcom-db) container_name: calcom-db hostname: calcom-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-calcom-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/calcom/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/calcom/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.calcom-hourly-db.schedule=@every 1h" - "ofelia.job-exec.calcom-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.calcom-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.calcom-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.calcom-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.calcom-hourly-db.email-from=Calcom Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.calcom-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.calcom-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.calcom-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.calcom-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.calcom-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.calcom-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.calcom-monthly-db.email-from=Calcom Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.calcom-monthly-db.mail-only-on-error=false" calcom-app: image: $(getScriptImageByContainerName calcom-app) container_name: calcom-app hostname: calcom-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - calcom-db networks: - int-calcom-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # calcom-studio: # image: $(getScriptImageByContainerName calcom-studio) # container_name: calcom-studio # hostname: calcom-studio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # depends_on: # - calcom-db # command: # - npx # - prisma # - studio # networks: # - int-calcom-net # - dock-ext-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-calcom-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/calcom.env TZ=\${PORTAINER_TZ} NEXT_PUBLIC_LICENSE_CONSENT= LICENSE= ALLOWED_HOSTNAMES='"$HOMESERVER_DOMAIN","$SUB_CALCOM_APP.$HOMESERVER_DOMAIN","$SUB_CALCOM_STUDIO.$HOMESERVER_DOMAIN","calcom-app:3000","localhost:3000","calcom-studio:5555","localhost:5555"' NEXT_PUBLIC_WEBAPP_URL=https://$SUB_CALCOM_APP.$HOMESERVER_DOMAIN NEXT_PUBLIC_API_V2_URL=https://$SUB_CALCOM_APP.$HOMESERVER_DOMAIN/api/v2 NEXTAUTH_URL=https://$SUB_CALCOM_APP.$HOMESERVER_DOMAIN/api/auth NEXTAUTH_SECRET=$(pwgen -n 32 1) CALENDSO_ENCRYPTION_KEY=$(pwgen -n 32 1) POSTGRES_USER=$CALCOM_DATABASE_USER POSTGRES_PASSWORD=$CALCOM_DATABASE_USER_PASSWORD POSTGRES_DB=$CALCOM_DATABASE_NAME DATABASE_HOST=calcom-db:5432 DATABASE_URL=postgresql://$CALCOM_DATABASE_USER:$CALCOM_DATABASE_USER_PASSWORD@calcom-db/$CALCOM_DATABASE_NAME DATABASE_DIRECT_URL=\${DATABASE_URL} CALCOM_TELEMETRY_DISABLED=1 EMAIL_FROM_NAME=Cal.com $(getAdminEmailName) EMAIL_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS EMAIL_SERVER="smtp://$SMTP_HOSTNAME:$SMTP_HOSTPORT/?ignoreTLS=true" NODE_ENV=production NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt TURBO_TELEMETRY_DISABLED=1 DO_NOT_TRACK=1 CSP_POLICY= NEXT_PUBLIC_WEBSITE_TERMS_URL= NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL= NEXT_PUBLIC_SINGLE_ORG_SLUG= ORGANIZATIONS_ENABLED= EOFMT dtnow=$(date '+%Y-%m-%d %H:%M:%S.%3N') randuid=$(uuidgen) cat < $HSHQ_STACKS_DIR/calcom/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$CALCOM_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do vapp=\$(echo "select slug from \"App\" limit 1;" | psql -t -A -U $CALCOM_DATABASE_USER $CALCOM_DATABASE_NAME 2> /dev/null) if ! [ -z "\$vapp" ]; then break fi echo "Database not ready, sleeping 3 seconds, total wait=\$curSeconds seconds..." sleep 3 curSeconds=\$((curSeconds+3)) done echo "insert into users(id,username,name,email,\"timeZone\",\"weekStart\",\"startTime\",\"endTime\",created,\"bufferTime\",\"emailVerified\",\"hideBranding\",\"completedOnboarding\",\"twoFactorEnabled\",locale,\"identityProvider\",verified,\"timeFormat\",\"defaultScheduleId\",\"allowDynamicBooking\",role,\"disableImpersonation\",\"allowSEOIndexing\",\"receiveMonthlyDigestEmail\",locked,\"isPlatformManaged\",\"smsLockState\",\"smsLockReviewedByAdmin\",\"creationSource\",\"whitelistWorkflows\",uuid) values(1,'${ADMIN_USERNAME_BASE}-calcom','Cal.com $(getAdminEmailName)','$CALCOM_ADMIN_EMAIL_ADDRESS','$TZ','Sunday',0,1440,'$dtnow',0,'$dtnow',false,true,false,'en','CAL',false,12,1,true,'ADMIN',false,true,true,false,false,'UNLOCKED',false,'webapp',false,'$randuid');" | psql -U $CALCOM_DATABASE_USER $CALCOM_DATABASE_NAME > /dev/null 2>&1 echo "insert into \"UserPassword\"(\"userId\",hash) values(1,'$CALCOM_ADMIN_PASSWORD_HASH');" | psql -U $CALCOM_DATABASE_USER $CALCOM_DATABASE_NAME > /dev/null 2>&1 echo "insert into \"Schedule\"(id,\"userId\",name,\"timeZone\") values(1,1,'Working Hours','$TZ');" | psql -U $CALCOM_DATABASE_USER $CALCOM_DATABASE_NAME > /dev/null 2>&1 echo "insert into \"Availability\"(id,days,\"startTime\",\"endTime\",\"scheduleId\") values(1, ARRAY [1, 2, 3, 4, 5],'09:00:00','17:00:00',1);" | psql -U $CALCOM_DATABASE_USER $CALCOM_DATABASE_NAME > /dev/null 2>&1 EOFDS chmod +x $HSHQ_STACKS_DIR/calcom/dbexport/setupDBSettings.sh } function performUpdateCalcom() { perform_stack_name=calcom prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/calcom/cal.com:v5.7.1 image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/calcom/cal.com:v5.7.1,mirror.gcr.io/calcom/cal.com:v5.9.8" ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/calcom/cal.com:v5.9.8 image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/calcom/cal.com:v5.9.8,mirror.gcr.io/calcom/cal.com:v5.9.8" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Rallly function installRallly() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory rallly "Rallly" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName rallly-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName rallly-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName rallly-minio) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/rallly mkdir $HSHQ_STACKS_DIR/rallly/db mkdir $HSHQ_STACKS_DIR/rallly/dbexport mkdir $HSHQ_STACKS_DIR/rallly/minio chmod 777 $HSHQ_STACKS_DIR/rallly/dbexport initServicesCredentials set +e addUserMailu alias $RALLLY_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS RALLLY_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $RALLLY_ADMIN_PASSWORD | tr -d ':\n') outputConfigRallly installStack rallly rallly-app "Ready in" $HOME/rallly.env 3 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$RALLLY_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_RALLLY Admin Login Info" -b "This service only requires an email address, as a confirmation token is automatically sent via email during login. The following address has been registered as the administrator, and the admin section can be reached by going to https://$SUB_RALLLY.$HOMESERVER_DOMAIN/control-panel \n\n$FMLNAME_RALLLY Admin Email: $RALLLY_ADMIN_EMAIL_ADDRESS\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" RALLLY_INIT_ENV=true updateConfigVar RALLLY_INIT_ENV $RALLLY_INIT_ENV fi sleep 3 docker exec rallly-db /dbexport/setupDBSettings.sh rm -f $HSHQ_STACKS_DIR/rallly/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase Rallly postgres rallly-db $RALLLY_DATABASE_NAME $RALLLY_DATABASE_USER $RALLLY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/rallly/dbexport $RALLLY_DATABASE_READONLYUSER $RALLLY_DATABASE_READONLYUSER_PASSWORD startStopStack rallly stop startStopStack rallly start set -e inner_block="" inner_block=$inner_block">>https://$SUB_RALLLY.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://rallly-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_RALLLY $MANAGETLS_RALLLY "$is_integrate_hshq" $NETDEFAULT_RALLLY "$inner_block" insertSubAuthelia $SUB_RALLLY.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll rallly "$FMLNAME_RALLLY" $USERTYPE_RALLLY "https://$SUB_RALLLY.$HOMESERVER_DOMAIN" "rallly.png" "$(getHeimdallOrderFromSub $SUB_RALLLY $USERTYPE_RALLLY)" restartAllCaddyContainers checkAddDBConnection true rallly "$FMLNAME_RALLLY" postgres rallly-db $RALLLY_DATABASE_NAME $RALLLY_DATABASE_USER $RALLLY_DATABASE_USER_PASSWORD fi } function outputConfigRallly() { cat < $HOME/rallly-compose.yml $STACK_VERSION_PREFIX rallly $(getScriptStackVersion rallly) services: rallly-db: image: $(getScriptImageByContainerName rallly-db) container_name: rallly-db hostname: rallly-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-rallly-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/rallly/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/rallly/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.rallly-hourly-db.schedule=@every 1h" - "ofelia.job-exec.rallly-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.rallly-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.rallly-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.rallly-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.rallly-hourly-db.email-from=Rallly Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.rallly-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.rallly-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.rallly-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.rallly-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.rallly-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.rallly-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.rallly-monthly-db.email-from=Rallly Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.rallly-monthly-db.mail-only-on-error=false" rallly-app: image: $(getScriptImageByContainerName rallly-app) container_name: rallly-app hostname: rallly-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - rallly-db networks: - int-rallly-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro rallly-minio: image: $(getScriptImageByContainerName rallly-minio) container_name: rallly-minio hostname: rallly-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data networks: - dock-proxy-net - int-rallly-net entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 /usr/bin/mc alias set minio http://rallly-minio:9000 ${RALLLY_MINIO_KEY} ${RALLLY_MINIO_SECRET} echo 'Creating buckets rallly' /usr/bin/mc mb minio/rallly /usr/bin/mc anonymous set public minio/rallly/public wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/rallly/minio:/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-rallly-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/rallly.env TZ=\${PORTAINER_TZ} NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt NEXT_PUBLIC_BASE_URL=https://$SUB_RALLLY.$HOMESERVER_DOMAIN AUTH_URL=https://$SUB_RALLLY.$HOMESERVER_DOMAIN SECRET_PASSWORD=$(pwgen -n 32 1) SUPPORT_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS NOREPLY_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS NOREPLY_EMAIL_NAME=Rallly $(getAdminEmailName) INITIAL_ADMIN_EMAIL=$RALLLY_ADMIN_EMAIL_ADDRESS SMTP_HOST=$SMTP_HOSTNAME SMTP_PORT=$SMTP_HOSTPORT SMTP_REJECT_UNAUTHORIZED=true POSTGRES_DB=$RALLLY_DATABASE_NAME POSTGRES_USER=$RALLLY_DATABASE_USER POSTGRES_PASSWORD=$RALLLY_DATABASE_USER_PASSWORD DATABASE_URL=postgres://$RALLLY_DATABASE_USER:$RALLLY_DATABASE_USER_PASSWORD@rallly-db:5432/$RALLLY_DATABASE_NAME DIRECT_DATABASE_URL=\${DATABASE_URL} NEXT_PUBLIC_SELF_HOSTED=true NODE_ENV=production S3_REGION=rallly S3_BUCKET_NAME=rallly S3_ENDPOINT=http://rallly-minio:9000 S3_ACCESS_KEY_ID=$RALLLY_MINIO_KEY S3_SECRET_ACCESS_KEY=$RALLLY_MINIO_SECRET MINIO_ROOT_USER=$RALLLY_MINIO_KEY MINIO_ROOT_PASSWORD=$RALLLY_MINIO_SECRET MINIO_DOMAIN=rallly MINIO_CONSOLE_ADDRESS=:9090 EOFMT dtnow=$(date '+%Y-%m-%d %H:%M:%S.%3N') cat < $HSHQ_STACKS_DIR/rallly/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$RALLLY_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do testdb=\$(echo "select id from instance_settings limit 1;" | psql -t -A -U $RALLLY_DATABASE_USER $RALLLY_DATABASE_NAME 2> /dev/null) if ! [ -z "\$testdb" ]; then break fi echo "Database not ready, sleeping 3 seconds, total wait=\$curSeconds seconds..." sleep 3 curSeconds=\$((curSeconds+3)) done adminID=$(pwgen -n 25 1) spaceID=$(uuidgen) spaceMemberID=$(uuidgen) echo "insert into users(id,name,email,created_at,updated_at,email_verified,locale,time_format,time_zone,week_start,banned,role) values('\$adminID','Rallly $(getAdminEmailName)','$RALLLY_ADMIN_EMAIL_ADDRESS','$dtnow','$dtnow','$dtnow','en','hours12','$TZ',1,false,'admin');" | psql -U $RALLLY_DATABASE_USER $RALLLY_DATABASE_NAME > /dev/null 2>&1 echo "insert into spaces(id,name,owner_id,created_at,updated_at) values('\$spaceID','Personal', '\$adminID', '$dtnow', '$dtnow');" | psql -U $RALLLY_DATABASE_USER $RALLLY_DATABASE_NAME > /dev/null 2>&1 echo "insert into space_members(id,space_id,user_id,created_at,updated_at,role,last_selected_at) values('\$spaceMemberID','\$spaceID','\$adminID','$dtnow','$dtnow','ADMIN','$dtnow');" | psql -U $RALLLY_DATABASE_USER $RALLLY_DATABASE_NAME > /dev/null 2>&1 EOFDS chmod +x $HSHQ_STACKS_DIR/rallly/dbexport/setupDBSettings.sh } function performUpdateRallly() { perform_stack_name=rallly prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/lukevella/rallly:4.4.1,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/lukevella/rallly:4.4.1,mirror.gcr.io/lukevella/rallly:4.4.1" image_update_map[2]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z" image_update_map[3]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/lukevella/rallly:4.5.7,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/lukevella/rallly:4.5.7,mirror.gcr.io/lukevella/rallly:4.5.7" image_update_map[2]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[3]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true fixV2ComposeRallly perform_update_report="${perform_update_report}$stack_upgrade_report" startStopStack rallly stop sleep 2 startStopStack rallly start return ;; 3) newVer=v4 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/lukevella/rallly:4.5.7,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/lukevella/rallly:4.5.7,mirror.gcr.io/lukevella/rallly:4.5.7" image_update_map[2]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" ;; 4) newVer=v4 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/lukevella/rallly:4.5.7,ghcr.io/homeserverhq/minio:v1.0 image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/lukevella/rallly:4.5.7,mirror.gcr.io/lukevella/rallly:4.5.7" image_update_map[2]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function fixV2ComposeRallly() { rm -f $HOME/rallly-compose.yml cat < $HOME/rallly-compose.yml $STACK_VERSION_PREFIX rallly v3 services: rallly-db: image: mirror.gcr.io/postgres:17.6 container_name: rallly-db hostname: rallly-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-rallly-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/rallly/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/rallly/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.rallly-hourly-db.schedule=@every 1h" - "ofelia.job-exec.rallly-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.rallly-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.rallly-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.rallly-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.rallly-hourly-db.email-from=Rallly Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.rallly-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.rallly-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.rallly-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.rallly-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.rallly-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.rallly-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.rallly-monthly-db.email-from=Rallly Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.rallly-monthly-db.mail-only-on-error=false" rallly-app: image: mirror.gcr.io/lukevella/rallly:4.5.7 container_name: rallly-app hostname: rallly-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - rallly-db networks: - int-rallly-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro rallly-minio: image: mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z container_name: rallly-minio hostname: rallly-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: server /data networks: - dock-proxy-net - int-rallly-net entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 /usr/bin/mc alias set minio http://rallly-minio:9000 ${RALLLY_MINIO_KEY} ${RALLLY_MINIO_SECRET} echo 'Creating buckets rallly' /usr/bin/mc mb minio/rallly /usr/bin/mc anonymous set public minio/rallly/public wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/rallly/minio:/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-rallly-net: driver: bridge internal: true ipam: driver: default EOFMT } # EasyAppointments function installEasyAppointments() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory easyappointments "EasyAppointments" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName easyappointments-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName easyappointments-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/easyappointments mkdir $HSHQ_STACKS_DIR/easyappointments/db mkdir $HSHQ_STACKS_DIR/easyappointments/dbexport chmod 777 $HSHQ_STACKS_DIR/easyappointments/dbexport initServicesCredentials set +e addUserMailu alias $EASYAPPOINTMENTS_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS generateCert easyappointments-app easyappointments-app outputConfigEasyAppointments set +e installStack easyappointments easyappointments-db "Ready for start up" $HOME/easyappointments.env 3 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$EASYAPPOINTMENTS_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_EASYAPPOINTMENTS Admin Login Info" -b "$FMLNAME_EASYAPPOINTMENTS Admin Username: $EASYAPPOINTMENTS_ADMIN_USERNAME\n$FMLNAME_EASYAPPOINTMENTS Admin Password: $EASYAPPOINTMENTS_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" EASYAPPOINTMENTS_INIT_ENV=true updateConfigVar EASYAPPOINTMENTS_INIT_ENV $EASYAPPOINTMENTS_INIT_ENV fi sleep 5 set +e echo "Initializing database..." docker exec easyappointments-db /dbexport/setupDBSettings.sh retVal=$? if [ $retVal -ne 0 ]; then echo "There was a problem initializing the database, please remove EasyAppointments and try again. If the problem persists, then please open an issue on https://github.com/homeserverhq/hshq." return $retVal fi echo "Initialization complete!" rm -f $HSHQ_STACKS_DIR/easyappointments/dbexport/setupDBSettings.sh rm -f $HSHQ_STACKS_DIR/easyappointments/dbexport/initdb.sql addReadOnlyUserToDatabase EasyAppointments mysql easyappointments-db $EASYAPPOINTMENTS_DATABASE_NAME root $EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/easyappointments/dbexport $EASYAPPOINTMENTS_DATABASE_READONLYUSER $EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD startStopStack easyappointments stop startStopStack easyappointments start set -e inner_block="" inner_block=$inner_block">>https://$SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://easyappointments-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_EASYAPPOINTMENTS $MANAGETLS_EASYAPPOINTMENTS "$is_integrate_hshq" $NETDEFAULT_EASYAPPOINTMENTS "$inner_block" insertSubAuthelia $SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll easyappointments "$FMLNAME_EASYAPPOINTMENTS" $USERTYPE_EASYAPPOINTMENTS "https://$SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN" "easyappointments.png" "$(getHeimdallOrderFromSub $SUB_EASYAPPOINTMENTS $USERTYPE_EASYAPPOINTMENTS)" restartAllCaddyContainers checkAddDBConnection true easyappointments "$FMLNAME_EASYAPPOINTMENTS" mysql easyappointments-db $EASYAPPOINTMENTS_DATABASE_NAME $EASYAPPOINTMENTS_DATABASE_USER $EASYAPPOINTMENTS_DATABASE_USER_PASSWORD fi } function outputConfigEasyAppointments() { cat < $HOME/easyappointments-compose.yml $STACK_VERSION_PREFIX easyappointments $(getScriptStackVersion easyappointments) services: easyappointments-db: image: $(getScriptImageByContainerName easyappointments-db) container_name: easyappointments-db hostname: easyappointments-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-easyappointments-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-easyappointments-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/easyappointments/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.easyappointments-hourly-db.schedule=@every 1h" - "ofelia.job-exec.easyappointments-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.easyappointments-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.easyappointments-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.easyappointments-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.easyappointments-hourly-db.email-from=EasyAppointments Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.easyappointments-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.easyappointments-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.easyappointments-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.easyappointments-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.easyappointments-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.easyappointments-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.easyappointments-monthly-db.email-from=EasyAppointments Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.easyappointments-monthly-db.mail-only-on-error=false" easyappointments-app: image: $(getScriptImageByContainerName easyappointments-app) container_name: easyappointments-app hostname: easyappointments-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - easyappointments-db networks: - int-easyappointments-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/easyappointments-app.crt:/ldapcerts/easyappointments-app.crt - \${PORTAINER_HSHQ_SSL_DIR}/easyappointments-app.key:/ldapcerts/easyappointments-app.key - \${PORTAINER_HSHQ_STACKS_DIR}/easyappointments/ldap.conf:/etc/ldap/ldap.conf:ro - \${PORTAINER_HSHQ_STACKS_DIR}/easyappointments/constants.php:/var/www/html/application/config/constants.php:ro volumes: v-easyappointments-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/easyappointments/db networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-easyappointments-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/easyappointments.env TZ=\${PORTAINER_TZ} MYSQL_DATABASE=$EASYAPPOINTMENTS_DATABASE_NAME MYSQL_ROOT_PASSWORD=$EASYAPPOINTMENTS_DATABASE_ROOT_PASSWORD MYSQL_USER=$EASYAPPOINTMENTS_DATABASE_USER MYSQL_PASSWORD=$EASYAPPOINTMENTS_DATABASE_USER_PASSWORD BASE_URL=https://$SUB_EASYAPPOINTMENTS.$HOMESERVER_DOMAIN DEBUG_MODE=TRUE DB_HOST=easyappointments-db DB_NAME=$EASYAPPOINTMENTS_DATABASE_NAME DB_USERNAME=$EASYAPPOINTMENTS_DATABASE_USER DB_PASSWORD=$EASYAPPOINTMENTS_DATABASE_USER_PASSWORD MAIL_PROTOCOL=smtp MAIL_SMTP_DEBUG=0 MAIL_SMTP_AUTH=0 MAIL_SMTP_HOST=$SMTP_HOSTNAME MAIL_SMTP_USER= MAIL_SMTP_PASS= MAIL_SMTP_CRYPTO=tls MAIL_SMTP_PORT=$SMTP_HOSTPORT MAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS MAIL_FROM_NAME=Easy!Appointments $(getAdminEmailName) MAIL_REPLY_TO_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS EOFMT cat < $HOME/pwhashgen.php EOFMT pwsalt=$(pwgen -n 64 1) EASYAPPOINTMENTS_ADMIN_PASSWORD_HASH=$(php $HOME/pwhashgen.php $EASYAPPOINTMENTS_ADMIN_PASSWORD $pwsalt) cat < $HSHQ_STACKS_DIR/easyappointments/dbexport/setupDBSettings.sh #!/bin/bash mysql -N -u $EASYAPPOINTMENTS_DATABASE_USER -p$EASYAPPOINTMENTS_DATABASE_USER_PASSWORD < /dbexport/initdb.sql EOFDS chmod +x $HSHQ_STACKS_DIR/easyappointments/dbexport/setupDBSettings.sh dtnow=$(date -u '+%Y-%m-%d %H:%M:%S') cat < $HSHQ_STACKS_DIR/easyappointments/dbexport/initdb.sql use $EASYAPPOINTMENTS_DATABASE_NAME; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Table structure for table \`ea_appointments\` -- DROP TABLE IF EXISTS \`ea_appointments\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_appointments\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`book_datetime\` datetime DEFAULT NULL, \`start_datetime\` datetime DEFAULT NULL, \`end_datetime\` datetime DEFAULT NULL, \`location\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`notes\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`hash\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`color\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT '#7cbae8', \`status\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT '', \`is_unavailability\` tinyint(4) NOT NULL DEFAULT 0, \`id_users_provider\` int(11) DEFAULT NULL, \`id_users_customer\` int(11) DEFAULT NULL, \`id_services\` int(11) DEFAULT NULL, \`id_google_calendar\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`id_caldav_calendar\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (\`id\`), KEY \`id_users_provider\` (\`id_users_provider\`), KEY \`id_users_customer\` (\`id_users_customer\`), KEY \`id_services\` (\`id_services\`), CONSTRAINT \`appointments_services\` FOREIGN KEY (\`id_services\`) REFERENCES \`ea_services\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT \`appointments_users_customer\` FOREIGN KEY (\`id_users_customer\`) REFERENCES \`ea_users\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT \`appointments_users_provider\` FOREIGN KEY (\`id_users_provider\`) REFERENCES \`ea_users\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table \`ea_blocked_periods\` -- DROP TABLE IF EXISTS \`ea_blocked_periods\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_blocked_periods\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`start_datetime\` datetime DEFAULT NULL, \`end_datetime\` datetime DEFAULT NULL, \`notes\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table \`ea_consents\` -- DROP TABLE IF EXISTS \`ea_consents\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_consents\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`created\` timestamp NULL DEFAULT NULL, \`modified\` timestamp NULL DEFAULT NULL, \`first_name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`last_name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`email\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`ip\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`type\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table \`ea_migrations\` -- DROP TABLE IF EXISTS \`ea_migrations\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_migrations\` ( \`version\` bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table \`ea_migrations\` -- LOCK TABLES \`ea_migrations\` WRITE; /*!40000 ALTER TABLE \`ea_migrations\` DISABLE KEYS */; INSERT INTO \`ea_migrations\` VALUES (60); /*!40000 ALTER TABLE \`ea_migrations\` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table \`ea_roles\` -- DROP TABLE IF EXISTS \`ea_roles\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_roles\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`slug\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`is_admin\` tinyint(4) DEFAULT NULL, \`appointments\` int(11) DEFAULT NULL, \`customers\` int(11) DEFAULT NULL, \`services\` int(11) DEFAULT NULL, \`users\` int(11) DEFAULT NULL, \`system_settings\` int(11) DEFAULT NULL, \`user_settings\` int(11) DEFAULT NULL, \`webhooks\` int(11) DEFAULT NULL, \`blocked_periods\` int(11) DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table \`ea_roles\` -- LOCK TABLES \`ea_roles\` WRITE; /*!40000 ALTER TABLE \`ea_roles\` DISABLE KEYS */; INSERT INTO \`ea_roles\` VALUES (1,NULL,NULL,'Administrator','admin',1,15,15,15,15,15,15,15,15), (2,NULL,NULL,'Provider','provider',0,15,15,0,0,0,15,0,0), (3,NULL,NULL,'Customer','customer',0,0,0,0,0,0,0,0,0), (4,NULL,NULL,'Secretary','secretary',0,15,15,0,0,0,15,0,0); /*!40000 ALTER TABLE \`ea_roles\` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table \`ea_secretaries_providers\` -- DROP TABLE IF EXISTS \`ea_secretaries_providers\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_secretaries_providers\` ( \`id_users_secretary\` int(11) NOT NULL, \`id_users_provider\` int(11) NOT NULL, PRIMARY KEY (\`id_users_secretary\`,\`id_users_provider\`), KEY \`secretaries_users_provider\` (\`id_users_provider\`), CONSTRAINT \`secretaries_users_provider\` FOREIGN KEY (\`id_users_provider\`) REFERENCES \`ea_users\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT \`secretaries_users_secretary\` FOREIGN KEY (\`id_users_secretary\`) REFERENCES \`ea_users\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table \`ea_service_categories\` -- DROP TABLE IF EXISTS \`ea_service_categories\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_service_categories\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`description\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table \`ea_services\` -- DROP TABLE IF EXISTS \`ea_services\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_services\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`duration\` int(11) DEFAULT NULL, \`price\` decimal(10,2) DEFAULT NULL, \`currency\` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`description\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`color\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT '#7cbae8', \`location\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`availabilities_type\` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT 'flexible', \`attendants_number\` int(11) DEFAULT 1, \`is_private\` tinyint(4) DEFAULT 0, \`id_service_categories\` int(11) DEFAULT NULL, PRIMARY KEY (\`id\`), KEY \`id_service_categories\` (\`id_service_categories\`), CONSTRAINT \`services_service_categories\` FOREIGN KEY (\`id_service_categories\`) REFERENCES \`ea_service_categories\` (\`id\`) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table \`ea_services\` -- LOCK TABLES \`ea_services\` WRITE; /*!40000 ALTER TABLE \`ea_services\` DISABLE KEYS */; INSERT INTO \`ea_services\` VALUES (1,'$dtnow','$dtnow','Service',30,0.00,'',NULL,'#7cbae8',NULL,'flexible',1,0,NULL); /*!40000 ALTER TABLE \`ea_services\` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table \`ea_services_providers\` -- DROP TABLE IF EXISTS \`ea_services_providers\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_services_providers\` ( \`id_users\` int(11) NOT NULL, \`id_services\` int(11) NOT NULL, PRIMARY KEY (\`id_users\`,\`id_services\`), KEY \`services_providers_services\` (\`id_services\`), CONSTRAINT \`services_providers_services\` FOREIGN KEY (\`id_services\`) REFERENCES \`ea_services\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT \`services_providers_users_provider\` FOREIGN KEY (\`id_users\`) REFERENCES \`ea_users\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table \`ea_settings\` -- DROP TABLE IF EXISTS \`ea_settings\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_settings\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`name\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`value\` longtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB AUTO_INCREMENT=74 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table \`ea_settings\` -- LOCK TABLES \`ea_settings\` WRITE; /*!40000 ALTER TABLE \`ea_settings\` DISABLE KEYS */; INSERT INTO \`ea_settings\` VALUES (1,NULL,NULL,'company_working_plan','{\"monday\":{\"start\":\"09:00\",\"end\":\"18:00\",\"breaks\":[{\"start\":\"14:30\",\"end\":\"15:00\"}]},\"tuesday\":{\"start\":\"09:00\",\"end\":\"18:00\",\"breaks\":[{\"start\":\"14:30\",\"end\":\"15:00\"}]},\"wednesday\":{\"start\":\"09:00\",\"end\":\"18:00\",\"breaks\":[{\"start\":\"14:30\",\"end\":\"15:00\"}]},\"thursday\":{\"start\":\"09:00\",\"end\":\"18:00\",\"breaks\":[{\"start\":\"14:30\",\"end\":\"15:00\"}]},\"friday\":{\"start\":\"09:00\",\"end\":\"18:00\",\"breaks\":[{\"start\":\"14:30\",\"end\":\"15:00\"}]},\"saturday\":{\"start\":\"09:00\",\"end\":\"18:00\",\"breaks\":[{\"start\":\"14:30\",\"end\":\"15:00\"}]},\"sunday\":{\"start\":\"09:00\",\"end\":\"18:00\",\"breaks\":[{\"start\":\"14:30\",\"end\":\"15:00\"}]}}'), (2,NULL,NULL,'book_advance_timeout','30'), (3,NULL,NULL,'google_analytics_code',''), (4,NULL,NULL,'customer_notifications','1'), (5,NULL,NULL,'date_format','DMY'), (6,NULL,NULL,'require_captcha','0'), (7,NULL,NULL,'time_format','regular'), (8,NULL,NULL,'display_cookie_notice','0'), (9,NULL,NULL,'cookie_notice_content','Cookie notice content.'), (10,NULL,NULL,'display_terms_and_conditions','0'), (11,NULL,NULL,'terms_and_conditions_content','Terms and conditions content.'), (12,NULL,NULL,'display_privacy_policy','0'), (13,NULL,NULL,'privacy_policy_content','Privacy policy content.'), (14,NULL,NULL,'first_weekday','sunday'), (16,NULL,NULL,'api_token',''), (17,NULL,NULL,'display_any_provider','1'), (18,NULL,NULL,'display_first_name','1'), (19,NULL,NULL,'require_first_name','1'), (20,NULL,NULL,'display_last_name','1'), (21,NULL,NULL,'require_last_name','1'), (22,NULL,NULL,'display_email','1'), (23,NULL,NULL,'require_email','1'), (24,NULL,NULL,'display_phone_number','1'), (25,NULL,NULL,'require_phone_number','1'), (26,NULL,NULL,'display_address','1'), (27,NULL,NULL,'require_address','0'), (28,NULL,NULL,'display_city','1'), (29,NULL,NULL,'require_city','0'), (30,NULL,NULL,'display_zip_code','1'), (31,NULL,NULL,'require_zip_code','0'), (32,NULL,NULL,'display_notes','1'), (33,NULL,NULL,'require_notes','0'), (34,NULL,NULL,'matomo_analytics_url',''), (35,NULL,NULL,'display_delete_personal_information','0'), (36,NULL,NULL,'disable_booking','0'), (37,NULL,NULL,'disable_booking_message','

Thanks for stopping by!

We are not accepting new appointments at the moment, please check back again later.

'), (38,NULL,NULL,'company_logo',''), (39,NULL,NULL,'company_color','#ffffff'), (40,NULL,NULL,'display_login_button','1'), (41,NULL,NULL,'theme','default'), (42,'$dtnow','$dtnow','limit_customer_access','0'), (43,NULL,NULL,'future_booking_limit','90'), (44,NULL,NULL,'appointment_status_options','[\"Booked\", \"Confirmed\", \"Rescheduled\", \"Cancelled\", \"Draft\"]'), (45,NULL,NULL,'display_custom_field_1','0'), (46,NULL,NULL,'require_custom_field_1','0'), (47,NULL,NULL,'label_custom_field_1',''), (48,NULL,NULL,'display_custom_field_2','0'), (49,NULL,NULL,'require_custom_field_2','0'), (50,NULL,NULL,'label_custom_field_2',''), (51,NULL,NULL,'display_custom_field_3','0'), (52,NULL,NULL,'require_custom_field_3','0'), (53,NULL,NULL,'label_custom_field_3',''), (54,NULL,NULL,'display_custom_field_4','0'), (55,NULL,NULL,'require_custom_field_4','0'), (56,NULL,NULL,'label_custom_field_4',''), (57,NULL,NULL,'display_custom_field_5','0'), (58,NULL,NULL,'require_custom_field_5','0'), (59,NULL,NULL,'label_custom_field_5',''), (60,NULL,NULL,'matomo_analytics_site_id','1'), (61,NULL,NULL,'default_language','english'), (62,NULL,NULL,'default_timezone','$TZ'), (63,'$dtnow','$dtnow','ldap_is_active','1'), (64,'$dtnow','$dtnow','ldap_host','ldaps://ldapserver'), (65,'$dtnow','$dtnow','ldap_port','389'), (66,'$dtnow','$dtnow','ldap_user_dn','$LDAP_READONLY_USER_BIND_DN'), (67,'$dtnow','$dtnow','ldap_password','$LDAP_READONLY_USER_PASSWORD'), (68,'$dtnow','$dtnow','ldap_base_dn','$LDAP_BASE_DN'), (69,'$dtnow','$dtnow','ldap_filter','(&(objectClass=person)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))'), (70,'$dtnow','$dtnow','ldap_field_mapping','{\n \"first_name\": \"givenname\",\n \"last_name\": \"sn\",\n \"email\": \"mail\",\n \"username\": \"uid\"\n}'), (71,'$dtnow','$dtnow','company_name','$HOMESERVER_NAME'), (72,'$dtnow','$dtnow','company_email','$EMAIL_ADMIN_EMAIL_ADDRESS'), (73,'$dtnow','$dtnow','company_link','www.$HOMESERVER_DOMAIN'); /*!40000 ALTER TABLE \`ea_settings\` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table \`ea_user_settings\` -- DROP TABLE IF EXISTS \`ea_user_settings\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_user_settings\` ( \`id_users\` int(11) NOT NULL, \`username\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`password\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`salt\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`working_plan\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`working_plan_exceptions\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`notifications\` tinyint(4) DEFAULT NULL, \`google_sync\` tinyint(4) DEFAULT NULL, \`google_token\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`google_calendar\` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`caldav_sync\` tinyint(4) DEFAULT 0, \`caldav_url\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`caldav_username\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`caldav_password\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`sync_past_days\` int(11) DEFAULT 30, \`sync_future_days\` int(11) DEFAULT 90, \`calendar_view\` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT 'default', PRIMARY KEY (\`id_users\`), CONSTRAINT \`user_settings_users\` FOREIGN KEY (\`id_users\`) REFERENCES \`ea_users\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table \`ea_user_settings\` -- LOCK TABLES \`ea_user_settings\` WRITE; /*!40000 ALTER TABLE \`ea_user_settings\` DISABLE KEYS */; INSERT INTO \`ea_user_settings\` VALUES (1,'$EASYAPPOINTMENTS_ADMIN_USERNAME','$EASYAPPOINTMENTS_ADMIN_PASSWORD_HASH','$pwsalt',NULL,NULL,1,NULL,NULL,NULL,0,NULL,NULL,NULL,30,90,'default'); /*!40000 ALTER TABLE \`ea_user_settings\` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table \`ea_users\` -- DROP TABLE IF EXISTS \`ea_users\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_users\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`first_name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`last_name\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`email\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`mobile_number\` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`phone_number\` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`address\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`city\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`state\` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`zip_code\` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`notes\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`timezone\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT '$TZ', \`language\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT 'english', \`custom_field_1\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`custom_field_2\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`custom_field_3\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`custom_field_4\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`custom_field_5\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`is_private\` tinyint(4) DEFAULT 0, \`ldap_dn\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`id_roles\` int(11) DEFAULT NULL, PRIMARY KEY (\`id\`), KEY \`id_roles\` (\`id_roles\`), CONSTRAINT \`users_roles\` FOREIGN KEY (\`id_roles\`) REFERENCES \`ea_roles\` (\`id\`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table \`ea_users\` -- LOCK TABLES \`ea_users\` WRITE; /*!40000 ALTER TABLE \`ea_users\` DISABLE KEYS */; INSERT INTO \`ea_users\` VALUES (1,'$dtnow','$dtnow','EasyAppointments','$(getAdminEmailName)','$EASYAPPOINTMENTS_ADMIN_EMAIL_ADDRESS',NULL,'1235551212',NULL,NULL,NULL,NULL,NULL,'$TZ','english',NULL,NULL,NULL,NULL,NULL,0,NULL,1); /*!40000 ALTER TABLE \`ea_users\` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table \`ea_webhooks\` -- DROP TABLE IF EXISTS \`ea_webhooks\`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE \`ea_webhooks\` ( \`id\` int(11) NOT NULL AUTO_INCREMENT, \`create_datetime\` datetime DEFAULT NULL, \`update_datetime\` datetime DEFAULT NULL, \`name\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`url\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`actions\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`secret_header\` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT 'X-Ea-Token', \`secret_token\` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, \`is_ssl_verified\` tinyint(4) NOT NULL DEFAULT 1, \`notes\` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; EOFSQ rm -f $HOME/pwhashgen.php cat < $HSHQ_STACKS_DIR/easyappointments/ldap.conf TLS_CERT /ldapcerts/easyappointments-app.crt TLS_KEY /ldapcerts/easyappointments-app.key TLS_CACERT /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT always TLS_REQSAN always EOFLD cat < $HSHQ_STACKS_DIR/easyappointments/constants.php 'givenname', 'last_name' => 'sn', 'email' => 'mail', 'phone_number' => 'telephonenumber', 'username' => 'cn', ]; /* |-------------------------------------------------------------------------- | Webhook Actions |-------------------------------------------------------------------------- | | External application endpoints can subscribe to these webhook actions. | */ const WEBHOOK_APPOINTMENT_SAVE = 'appointment_save'; const WEBHOOK_APPOINTMENT_DELETE = 'appointment_delete'; const WEBHOOK_UNAVAILABILITY_SAVE = 'unavailability_save'; const WEBHOOK_UNAVAILABILITY_DELETE = 'unavailability_delete'; const WEBHOOK_CUSTOMER_SAVE = 'customer_save'; const WEBHOOK_CUSTOMER_DELETE = 'customer_delete'; const WEBHOOK_SERVICE_SAVE = 'service_save'; const WEBHOOK_SERVICE_DELETE = 'service_delete'; const WEBHOOK_SERVICE_CATEGORY_SAVE = 'service_category_save'; const WEBHOOK_SERVICE_CATEGORY_DELETE = 'service_category_delete'; const WEBHOOK_PROVIDER_SAVE = 'provider_save'; const WEBHOOK_PROVIDER_DELETE = 'provider_delete'; const WEBHOOK_SECRETARY_SAVE = 'secretary_save'; const WEBHOOK_SECRETARY_DELETE = 'secretary_delete'; const WEBHOOK_ADMIN_SAVE = 'admin_save'; const WEBHOOK_ADMIN_DELETE = 'admin_delete'; const WEBHOOK_BLOCKED_PERIOD_SAVE = 'blocked_period_save'; const WEBHOOK_BLOCKED_PERIOD_DELETE = 'blocked_period_delete'; /* End of file constants.php */ /* Location: ./application/config/constants.php */ EOFLD } function performUpdateEasyAppointments() { perform_stack_name=easyappointments prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/mariadb:10.7.3,alextselegidis/easyappointments:1.5.2 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/alextselegidis/easyappointments:1.5.2,mirror.gcr.io/alextselegidis/easyappointments:1.5.2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # OpenProject function installOpenProject() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory openproject "OpenProject" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openproject-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openproject-memcache) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openproject-web) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openproject-minio) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openproject-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/openproject mkdir $HSHQ_STACKS_DIR/openproject/db mkdir $HSHQ_STACKS_DIR/openproject/dbexport mkdir $HSHQ_STACKS_DIR/openproject/data mkdir $HSHQ_STACKS_DIR/openproject/minio chmod 777 $HSHQ_STACKS_DIR/openproject/dbexport initServicesCredentials set +e addUserMailu alias $ADMIN_USERNAME_BASE"_openproject" $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS OPENPROJECT_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $OPENPROJECT_ADMIN_PASSWORD | tr -d ':\n') outputConfigOpenProject installStack openproject openproject-web "Listening on" $HOME/openproject.env 10 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$OPENPROJECT_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_OPENPROJECT_APP Admin Login Info" -b "$FMLNAME_OPENPROJECT_APP Admin Username: $OPENPROJECT_ADMIN_USERNAME\n$FMLNAME_OPENPROJECT_APP Admin Password: $OPENPROJECT_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" OPENPROJECT_INIT_ENV=true updateConfigVar OPENPROJECT_INIT_ENV $OPENPROJECT_INIT_ENV fi sleep 3 docker exec openproject-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/openproject/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase OpenProject postgres openproject-db $OPENPROJECT_DATABASE_NAME $OPENPROJECT_DATABASE_USER $OPENPROJECT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/openproject/dbexport $OPENPROJECT_DATABASE_READONLYUSER $OPENPROJECT_DATABASE_READONLYUSER_PASSWORD addMCPServerLiteLLM "openproject" "op" "http://openproject-mcp:80/mcp" http none "" set -e inner_block="" inner_block=$inner_block">>https://$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://openproject-web:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>header_up X-Forwarded-Proto https\n" inner_block=$inner_block">>>>>>>>header_up X-Forwarded-For {header.X-Forwarded-For}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENPROJECT_APP $MANAGETLS_OPENPROJECT_APP "$is_integrate_hshq" $NETDEFAULT_OPENPROJECT_APP "$inner_block" insertSubAuthelia $SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll openproject "$FMLNAME_OPENPROJECT_APP" $USERTYPE_OPENPROJECT_APP "https://$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN" "openproject.png" "$(getHeimdallOrderFromSub $SUB_OPENPROJECT_APP $USERTYPE_OPENPROJECT_APP)" restartAllCaddyContainers checkAddDBConnection true openproject "$FMLNAME_OPENPROJECT_APP" postgres openproject-db $OPENPROJECT_DATABASE_NAME $OPENPROJECT_DATABASE_USER $OPENPROJECT_DATABASE_USER_PASSWORD fi } function outputConfigOpenProject() { cat < $HOME/openproject-compose.yml $STACK_VERSION_PREFIX openproject $(getScriptStackVersion openproject) services: openproject-db: image: $(getScriptImageByContainerName openproject-db) container_name: openproject-db hostname: openproject-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-openproject-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.openproject-hourly-db.schedule=@every 1h" - "ofelia.job-exec.openproject-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.openproject-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openproject-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openproject-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openproject-hourly-db.email-from=OpenProject Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openproject-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.openproject-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.openproject-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.openproject-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openproject-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openproject-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openproject-monthly-db.email-from=OpenProject Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openproject-monthly-db.mail-only-on-error=false" openproject-memcache: image: $(getScriptImageByContainerName openproject-memcache) container_name: openproject-memcache hostname: openproject-memcache restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openproject-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro openproject-web: image: $(getScriptImageByContainerName openproject-web) container_name: openproject-web hostname: openproject-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/web" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-worker: image: $(getScriptImageByContainerName openproject-worker) container_name: openproject-worker hostname: openproject-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/worker" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-cron: image: $(getScriptImageByContainerName openproject-cron) container_name: openproject-cron hostname: openproject-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/cron" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-seeder: image: $(getScriptImageByContainerName openproject-seeder) container_name: openproject-seeder hostname: openproject-seeder restart: on-failure env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/seeder" depends_on: - openproject-db - openproject-memcache networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets # openproject-minio: # image: $(getScriptImageByContainerName openproject-minio) # container_name: openproject-minio # hostname: openproject-minio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # command: server /data # networks: # - dock-proxy-net # - int-openproject-net # entrypoint: > # /bin/sh -c " # minio server /data --address ':9000' --console-address ':9001' & # MINIO_PID=\\\$! # while ! curl -s http://localhost:9000/minio/health/live; do # echo 'Waiting for MinIO to start...' # sleep 1 # done # sleep 5 # /usr/bin/mc alias set minio http://openproject-minio:9000 ${OPENPROJECT_MINIO_KEY} ${OPENPROJECT_MINIO_SECRET} # echo 'Creating bucket openproject' # /usr/bin/mc mb minio/uploads # wait \\\$MINIO_PID # " # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/minio:/data openproject-mcp: image: $(getScriptImageByContainerName openproject-mcp) container_name: openproject-mcp hostname: openproject-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-openproject-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-openproject-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openproject/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-openproject-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/openproject.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$OPENPROJECT_DATABASE_NAME POSTGRES_USER=$OPENPROJECT_DATABASE_USER POSTGRES_PASSWORD=$OPENPROJECT_DATABASE_USER_PASSWORD HTTPS=false OPENPROJECT_HTTPS=true OPENPROJECT_HSTS=false OPENPROJECT_HOST__NAME=$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN OPENPROJECT_ADDITIONAL__HOST__NAMES=["localhost:8469", "openproject-web:8080"] TAG=16-slim IMAP_ENABLED=false DATABASE_URL=postgres://$OPENPROJECT_DATABASE_USER:$OPENPROJECT_DATABASE_USER_PASSWORD@openproject-db/$OPENPROJECT_DATABASE_NAME?pool=20&encoding=unicode&reconnect=true RAILS_MIN_THREADS=4 RAILS_MAX_THREADS=16 OPENPROJECT_RAILS__CACHE__STORE=memcache OPENPROJECT_CACHE__MEMCACHE__SERVER=openproject-memcache:11211 APP_HOST=openproject-web PGDATA=/var/lib/postgresql/data OPDATA=/var/openproject/assets OPENPROJECT_EMAIL__DELIVERY__METHOD=smtp OPENPROJECT_SMTP__ADDRESS=$SMTP_HOSTNAME OPENPROJECT_SMTP__PORT=$SMTP_HOSTPORT OPENPROJECT_SMTP__AUTHENTICATION=none OPENPROJECT_SMTP__DOMAIN=$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN OPENPROJECT_SMTP__OPENSSL__VERIFY__MODE=peer OPENPROJECT_SMTP__ENABLE__STARTTLS__AUTO=true OPENPROJECT_MAIL_FROM=OpenProject $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> OPENPROJECT_SEED__ADMIN__USER__LOCKED=false OPENPROJECT_SEED__ADMIN__USER__LOGIN=$OPENPROJECT_ADMIN_USERNAME OPENPROJECT_SEED__ADMIN__USER__MAIL=$OPENPROJECT_ADMIN_EMAIL_ADDRESS OPENPROJECT_SEED__ADMIN__USER__NAME=$(getAdminEmailName) OpenProject OPENPROJECT_SEED__ADMIN__USER__PASSWORD=$OPENPROJECT_ADMIN_PASSWORD OPENPROJECT_SEED__ADMIN__USER__PASSWORD__RESET=false OPENPROJECT_USER_DEFAULT_THEME=dark OPENPROJECT_SEED_LDAP_HSHQ_HOST=ldapserver OPENPROJECT_SEED_LDAP_HSHQ_PORT=389 OPENPROJECT_SEED_LDAP_HSHQ_SECURITY=start_tls OPENPROJECT_SEED_LDAP_HSHQ_TLS__VERIFY=true OPENPROJECT_SEED_LDAP_HSHQ_BINDUSER=$LDAP_READONLY_USER_BIND_DN OPENPROJECT_SEED_LDAP_HSHQ_BINDPASSWORD=$LDAP_READONLY_USER_PASSWORD OPENPROJECT_SEED_LDAP_HSHQ_BASEDN=$LDAP_BASE_DN OPENPROJECT_SEED_LDAP_HSHQ_FILTER=(&(objectClass=person)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)) OPENPROJECT_SEED_LDAP_HSHQ_SYNC__USERS=true OPENPROJECT_SEED_LDAP_HSHQ_LOGIN__MAPPING=uid OPENPROJECT_SEED_LDAP_HSHQ_FIRSTNAME__MAPPING=givenName OPENPROJECT_SEED_LDAP_HSHQ_LASTNAME__MAPPING=sn OPENPROJECT_SEED_LDAP_HSHQ_MAIL__MAPPING=mail OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_BASE=ou=groups,$LDAP_BASE_DN OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_FILTER=(objectclass=groupOfUniqueNames) OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_SYNC__USERS=true OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_GROUP__ATTRIBUTE=cn MINIO_ROOT_USER=$OPENPROJECT_MINIO_KEY MINIO_ROOT_PASSWORD=$OPENPROJECT_MINIO_SECRET MINIO_DOMAIN=openproject MINIO_CONSOLE_ADDRESS=:9090 OPENPROJECT_LOG__LEVEL=info OPENPROJECT_ATTACHMENTS__STORAGE=file OPENPROJECT_FOG_CREDENTIALS_AWS__ACCESS__KEY__ID=$OPENPROJECT_MINIO_KEY OPENPROJECT_FOG_CREDENTIALS_AWS__SECRET__ACCESS__KEY=$OPENPROJECT_MINIO_SECRET OPENPROJECT_FOG_CREDENTIALS_ENDPOINT=http://openproject-minio:9000 OPENPROJECT_FOG_CREDENTIALS_PROVIDER=AWS OPENPROJECT_FOG_CREDENTIALS_REGION=openproject OPENPROJECT_FOG_DIRECTORY=uploads SECRET_KEY_BASE=$OPENPROJECT_SECRET_KEY_BASE OPENPROJECT_BASE_URL=http://openproject-web:8080 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFMT cat < $HSHQ_STACKS_DIR/openproject/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$OPENPROJECT_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do admUser=\$(echo "select login from users where login='admin';" | psql -t -A -U $OPENPROJECT_DATABASE_USER $OPENPROJECT_DATABASE_NAME 2> /dev/null) if ! [ -z "\$admUser" ]; then break fi echo "Database not ready, sleeping 5 seconds, total wait=\$curSeconds seconds..." sleep 5 curSeconds=\$((curSeconds+5)) done echo "update users set login='$OPENPROJECT_ADMIN_USERNAME', firstname='$(getAdminEmailName)', lastname='OpenProject' where login='admin';" | psql -U $OPENPROJECT_DATABASE_USER $OPENPROJECT_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/openproject/dbexport/setupDBSettings.sh } function performUpdateOpenProject() { perform_stack_name=openproject prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/openproject/openproject:16.4.1-slim,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/openproject/openproject:16.4.1-slim,mirror.gcr.io/openproject/openproject:16.6.2-slim" image_update_map[2]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[4]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/openproject/openproject:16.6.2-slim,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/openproject/openproject:16.6.2-slim,mirror.gcr.io/openproject/openproject:16.6.2-slim" image_update_map[2]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[4]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true fixV2ComposeOpenProject perform_update_report="${perform_update_report}$stack_upgrade_report" startStopStack openproject stop sleep 2 startStopStack openproject start return ;; 3) newVer=v4 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/openproject/openproject:16.6.2-slim,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/openproject/openproject:16.6.2-slim,mirror.gcr.io/openproject/openproject:17.5.1-slim" image_update_map[2]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" ;; 4) newVer=v5 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/openproject/openproject:17.5.1-slim,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/openproject/openproject:17.5.1-slim,mirror.gcr.io/openproject/openproject:17.6.0-slim" image_update_map[2]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.45-alpine" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfOpenProjectUpdateV5 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 5) newVer=v6 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/openproject/openproject:17.6.0-slim,mirror.gcr.io/memcached:1.6.45-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/openproject-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/openproject/openproject:17.6.0-slim,mirror.gcr.io/openproject/openproject:17.6.0-slim" image_update_map[2]="mirror.gcr.io/memcached:1.6.45-alpine,mirror.gcr.io/memcached:1.6.45-alpine" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="ghcr.io/homeserverhq/openproject-mcp:v2,ghcr.io/homeserverhq/openproject-mcp:v2" ;; 6) newVer=v6 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/openproject/openproject:17.6.0-slim,mirror.gcr.io/memcached:1.6.45-alpine,ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/openproject-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/openproject/openproject:17.6.0-slim,mirror.gcr.io/openproject/openproject:17.6.0-slim" image_update_map[2]="mirror.gcr.io/memcached:1.6.45-alpine,mirror.gcr.io/memcached:1.6.45-alpine" image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="ghcr.io/homeserverhq/openproject-mcp:v2,ghcr.io/homeserverhq/openproject-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function fixV2ComposeOpenProject() { rm -f $HOME/openproject-compose.yml cat < $HOME/openproject-compose.yml $STACK_VERSION_PREFIX openproject v3 services: openproject-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: openproject-db hostname: openproject-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-openproject-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.openproject-hourly-db.schedule=@every 1h" - "ofelia.job-exec.openproject-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.openproject-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openproject-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openproject-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openproject-hourly-db.email-from=OpenProject Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openproject-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.openproject-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.openproject-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.openproject-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openproject-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openproject-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openproject-monthly-db.email-from=OpenProject Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openproject-monthly-db.mail-only-on-error=false" openproject-memcache: image: mirror.gcr.io/memcached:1.6.39-alpine container_name: openproject-memcache hostname: openproject-memcache restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openproject-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro openproject-web: image: mirror.gcr.io/openproject/openproject:16.6.2-slim container_name: openproject-web hostname: openproject-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/web" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-worker: image: mirror.gcr.io/openproject/openproject:16.6.2-slim container_name: openproject-worker hostname: openproject-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/worker" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-cron: image: mirror.gcr.io/openproject/openproject:16.6.2-slim container_name: openproject-cron hostname: openproject-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/cron" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-seeder: image: mirror.gcr.io/openproject/openproject:16.6.2-slim container_name: openproject-seeder hostname: openproject-seeder restart: on-failure env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/seeder" depends_on: - openproject-db - openproject-memcache networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets # openproject-minio: # image: mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z # container_name: openproject-minio # hostname: openproject-minio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # command: server /data # networks: # - dock-proxy-net # - int-openproject-net # entrypoint: > # /bin/sh -c " # minio server /data --address ':9000' --console-address ':9001' & # MINIO_PID=\\\$! # while ! curl -s http://localhost:9000/minio/health/live; do # echo 'Waiting for MinIO to start...' # sleep 1 # done # sleep 5 # /usr/bin/mc alias set minio http://openproject-minio:9000 ${OPENPROJECT_MINIO_KEY} ${OPENPROJECT_MINIO_SECRET} # echo 'Creating bucket openproject' # /usr/bin/mc mb minio/uploads # wait \\\$MINIO_PID # " # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/minio:/data volumes: v-openproject-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openproject/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-openproject-net: driver: bridge internal: true ipam: driver: default EOFMT } function mfOpenProjectUpdateV5() { buildOrPullImage $(getScriptImageByContainerName openproject-mcp) if [ $? -ne 0 ]; then return 1 fi cat < $HOME/openproject-compose.yml $STACK_VERSION_PREFIX openproject v5 services: openproject-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: openproject-db hostname: openproject-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-openproject-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.openproject-hourly-db.schedule=@every 1h" - "ofelia.job-exec.openproject-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.openproject-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openproject-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openproject-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openproject-hourly-db.email-from=OpenProject Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openproject-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.openproject-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.openproject-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.openproject-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openproject-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openproject-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openproject-monthly-db.email-from=OpenProject Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openproject-monthly-db.mail-only-on-error=false" openproject-memcache: image: mirror.gcr.io/memcached:1.6.45-alpine container_name: openproject-memcache hostname: openproject-memcache restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openproject-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro openproject-web: image: mirror.gcr.io/openproject/openproject:17.6.0-slim container_name: openproject-web hostname: openproject-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/web" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-worker: image: mirror.gcr.io/openproject/openproject:17.6.0-slim container_name: openproject-worker hostname: openproject-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/worker" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-cron: image: mirror.gcr.io/openproject/openproject:17.6.0-slim container_name: openproject-cron hostname: openproject-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/cron" depends_on: - openproject-db - openproject-memcache - openproject-seeder networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets openproject-seeder: image: mirror.gcr.io/openproject/openproject:17.6.0-slim container_name: openproject-seeder hostname: openproject-seeder restart: on-failure env_file: stack.env security_opt: - no-new-privileges:true command: "./docker/prod/seeder" depends_on: - openproject-db - openproject-memcache networks: - int-openproject-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openproject-data:/var/openproject/assets # openproject-minio: # image: mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z # container_name: openproject-minio # hostname: openproject-minio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # command: server /data # networks: # - dock-proxy-net # - int-openproject-net # entrypoint: > # /bin/sh -c " # minio server /data --address ':9000' --console-address ':9001' & # MINIO_PID=\\\$! # while ! curl -s http://localhost:9000/minio/health/live; do # echo 'Waiting for MinIO to start...' # sleep 1 # done # sleep 5 # /usr/bin/mc alias set minio http://openproject-minio:9000 ${OPENPROJECT_MINIO_KEY} ${OPENPROJECT_MINIO_SECRET} # echo 'Creating bucket openproject' # /usr/bin/mc mb minio/uploads # wait \\\$MINIO_PID # " # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_STACKS_DIR}/openproject/minio:/data openproject-mcp: image: ghcr.io/homeserverhq/openproject-mcp:v2 container_name: openproject-mcp hostname: openproject-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-openproject-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-openproject-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openproject/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-openproject-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/openproject.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$OPENPROJECT_DATABASE_NAME POSTGRES_USER=$OPENPROJECT_DATABASE_USER POSTGRES_PASSWORD=$OPENPROJECT_DATABASE_USER_PASSWORD HTTPS=false OPENPROJECT_HTTPS=true OPENPROJECT_HSTS=false OPENPROJECT_HOST__NAME=$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN OPENPROJECT_ADDITIONAL__HOST__NAMES=["localhost:8469", "openproject-web:8080"] TAG=16-slim IMAP_ENABLED=false DATABASE_URL=postgres://$OPENPROJECT_DATABASE_USER:$OPENPROJECT_DATABASE_USER_PASSWORD@openproject-db/$OPENPROJECT_DATABASE_NAME?pool=20&encoding=unicode&reconnect=true RAILS_MIN_THREADS=4 RAILS_MAX_THREADS=16 OPENPROJECT_RAILS__CACHE__STORE=memcache OPENPROJECT_CACHE__MEMCACHE__SERVER=openproject-memcache:11211 APP_HOST=openproject-web PGDATA=/var/lib/postgresql/data OPDATA=/var/openproject/assets OPENPROJECT_EMAIL__DELIVERY__METHOD=smtp OPENPROJECT_SMTP__ADDRESS=$SMTP_HOSTNAME OPENPROJECT_SMTP__PORT=$SMTP_HOSTPORT OPENPROJECT_SMTP__AUTHENTICATION=none OPENPROJECT_SMTP__DOMAIN=$SUB_OPENPROJECT_APP.$HOMESERVER_DOMAIN OPENPROJECT_SMTP__OPENSSL__VERIFY__MODE=peer OPENPROJECT_SMTP__ENABLE__STARTTLS__AUTO=true OPENPROJECT_MAIL_FROM=OpenProject $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> OPENPROJECT_SEED__ADMIN__USER__LOCKED=false OPENPROJECT_SEED__ADMIN__USER__LOGIN=$OPENPROJECT_ADMIN_USERNAME OPENPROJECT_SEED__ADMIN__USER__MAIL=$OPENPROJECT_ADMIN_EMAIL_ADDRESS OPENPROJECT_SEED__ADMIN__USER__NAME=$(getAdminEmailName) OpenProject OPENPROJECT_SEED__ADMIN__USER__PASSWORD=$OPENPROJECT_ADMIN_PASSWORD OPENPROJECT_SEED__ADMIN__USER__PASSWORD__RESET=false OPENPROJECT_USER_DEFAULT_THEME=dark OPENPROJECT_SEED_LDAP_HSHQ_HOST=ldapserver OPENPROJECT_SEED_LDAP_HSHQ_PORT=389 OPENPROJECT_SEED_LDAP_HSHQ_SECURITY=start_tls OPENPROJECT_SEED_LDAP_HSHQ_TLS__VERIFY=true OPENPROJECT_SEED_LDAP_HSHQ_BINDUSER=$LDAP_READONLY_USER_BIND_DN OPENPROJECT_SEED_LDAP_HSHQ_BINDPASSWORD=$LDAP_READONLY_USER_PASSWORD OPENPROJECT_SEED_LDAP_HSHQ_BASEDN=$LDAP_BASE_DN OPENPROJECT_SEED_LDAP_HSHQ_FILTER=(&(objectClass=person)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)) OPENPROJECT_SEED_LDAP_HSHQ_SYNC__USERS=true OPENPROJECT_SEED_LDAP_HSHQ_LOGIN__MAPPING=uid OPENPROJECT_SEED_LDAP_HSHQ_FIRSTNAME__MAPPING=givenName OPENPROJECT_SEED_LDAP_HSHQ_LASTNAME__MAPPING=sn OPENPROJECT_SEED_LDAP_HSHQ_MAIL__MAPPING=mail OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_BASE=ou=groups,$LDAP_BASE_DN OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_FILTER=(objectclass=groupOfUniqueNames) OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_SYNC__USERS=true OPENPROJECT_SEED_LDAP_HSHQ_GROUPFILTER_HSHQFILTER_GROUP__ATTRIBUTE=cn MINIO_ROOT_USER=$OPENPROJECT_MINIO_KEY MINIO_ROOT_PASSWORD=$OPENPROJECT_MINIO_SECRET MINIO_DOMAIN=openproject MINIO_CONSOLE_ADDRESS=:9090 OPENPROJECT_LOG__LEVEL=info OPENPROJECT_ATTACHMENTS__STORAGE=file OPENPROJECT_FOG_CREDENTIALS_AWS__ACCESS__KEY__ID=$OPENPROJECT_MINIO_KEY OPENPROJECT_FOG_CREDENTIALS_AWS__SECRET__ACCESS__KEY=$OPENPROJECT_MINIO_SECRET OPENPROJECT_FOG_CREDENTIALS_ENDPOINT=http://openproject-minio:9000 OPENPROJECT_FOG_CREDENTIALS_PROVIDER=AWS OPENPROJECT_FOG_CREDENTIALS_REGION=openproject OPENPROJECT_FOG_DIRECTORY=uploads SECRET_KEY_BASE=$OPENPROJECT_SECRET_KEY_BASE OPENPROJECT_BASE_URL=http://openproject-web:8080 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFMT addMCPServerLiteLLM "openproject" "op" "http://openproject-mcp:80/mcp" http none "" } # Zammad function installZammad() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory zammad "Zammad" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zammad-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zammad-init) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zammad-memcache) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zammad-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zammad-es) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zammad-minio) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/zammad mkdir $HSHQ_STACKS_DIR/zammad/config mkdir $HSHQ_STACKS_DIR/zammad/storage mkdir $HSHQ_STACKS_DIR/zammad/backup mkdir $HSHQ_STACKS_DIR/zammad/es mkdir $HSHQ_STACKS_DIR/zammad/minio mkdir $HSHQ_STACKS_DIR/zammad/db mkdir $HSHQ_STACKS_DIR/zammad/dbexport chmod 777 $HSHQ_STACKS_DIR/zammad/dbexport initServicesCredentials set +e addUserMailu alias $ZAMMAD_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS sleep 3 addUserMailu user $ZAMMAD_DEDICATED_EMAIL_USERNAME $HOMESERVER_DOMAIN $ZAMMAD_DEDICATED_EMAIL_PASSWORD outputConfigZammad installStack zammad zammad-nginx "starting nginx..." $HOME/zammad.env 15 600 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$ZAMMAD_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_ZAMMAD_APP Admin Login Info" -b "$FMLNAME_ZAMMAD_APP Admin Username: $ZAMMAD_ADMIN_EMAIL_ADDRESS\n$FMLNAME_ZAMMAD_APP Admin Password: $ZAMMAD_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" ZAMMAD_INIT_ENV=true updateConfigVar ZAMMAD_INIT_ENV $ZAMMAD_INIT_ENV fi sleep 3 echo "Performing integrations, please wait..." docker exec zammad-db /dbexport/checkDBReady.sh rm -f $HSHQ_STACKS_DIR/zammad/dbexport/checkDBReady.sh docker exec zammad-railsserver bash -c "cp /config/auto_wizard.json /opt/zammad/auto_wizard.json;/docker-entrypoint.sh bundle exec rails runner /config/init.rb" > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/zammad/config/auto_wizard.json rm -f $HSHQ_STACKS_DIR/zammad/config/init.rb addReadOnlyUserToDatabase Zammad postgres zammad-db $ZAMMAD_DATABASE_NAME $ZAMMAD_DATABASE_USER $ZAMMAD_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/zammad/dbexport $ZAMMAD_DATABASE_READONLYUSER $ZAMMAD_DATABASE_READONLYUSER_PASSWORD #echo "Restarting stack..." #startStopStack zammad stop #sleep 3 #startStopStack zammad start set -e inner_block="" inner_block=$inner_block">>https://$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://zammad-nginx:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>header_up X-Forwarded-Proto https\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ZAMMAD_APP $MANAGETLS_ZAMMAD_APP "$is_integrate_hshq" $NETDEFAULT_ZAMMAD_APP "$inner_block" insertSubAuthelia $SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll zammad "$FMLNAME_ZAMMAD_APP" $USERTYPE_ZAMMAD_APP "https://$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN" "zammad.png" "$(getHeimdallOrderFromSub $SUB_ZAMMAD_APP $USERTYPE_ZAMMAD_APP)" restartAllCaddyContainers checkAddDBConnection true zammad "$FMLNAME_ZAMMAD_APP" postgres zammad-db $ZAMMAD_DATABASE_NAME $ZAMMAD_DATABASE_USER $ZAMMAD_DATABASE_USER_PASSWORD fi } function outputConfigZammad() { auto_wizard_token=$(pwgen -n 64 1) cat < $HSHQ_STACKS_DIR/zammad/config/auto_wizard.json { "Token": "$auto_wizard_token", "TextModuleLocale": { "Locale": "en-us" }, "Users": [ { "login": "$ZAMMAD_ADMIN_EMAIL_ADDRESS", "firstname": "Zammad", "lastname": "$(getAdminEmailName)", "email": "$ZAMMAD_ADMIN_EMAIL_ADDRESS", "organization": "$HOMESERVER_NAME", "password": "$ZAMMAD_ADMIN_PASSWORD" } ], "Settings": [ { "name": "developer_mode", "value": false }, { "name": "system_online_service", "value": false }, { "name": "product_name", "value": "$HOMESERVER_NAME Helpdesk" }, { "name": "organization", "value": "$HOMESERVER_NAME" }, { "name": "timezone_default", "value": "$TZ" }, { "name": "notification_sender", "value": "Zammad $(getAdminEmailName) <$ZAMMAD_DEDICATED_EMAIL_ADDRESS>" }, { "name": "es_url", "value": "http://zammad-es:9200" }, { "name": "es_user", "value": "$ZAMMAD_ES_USERNAME" }, { "name": "es_password", "value": "$ZAMMAD_ES_PASSWORD" }, { "name": "ldap_integration", "value": true } ], "EmailAddresses": [ { "id": 1, "channel_id": 3, "name": "Zammad $(getAdminEmailName)", "email": "$ZAMMAD_DEDICATED_EMAIL_ADDRESS" } ], "Organizations": [ { "name": "$HOMESERVER_NAME", "domain": "$HOMESERVER_DOMAIN", "domain_assignment": true } ], "Signatures": [ { "id": 1, "name": "default", "body": "\n #{user.firstname} #{user.lastname}\n\n--\n $HOMESERVER_NAME\n--" } ], "Channels": [ { "id": 1, "area": "Email::Notification", "options": { "outbound": { "adapter": "smtp", "options": { "host": "$SMTP_HOSTNAME", "port": "25", "ssl": true, "user": "", "password": "", "ssl_verify": true } } }, "active": true }, { "id": 2, "area": "Email::Notification", "options": { "outbound": { "adapter": "sendmail" } }, "active": false }, { "id": 3, "area": "Email::Account", "group_id": 1, "options": { "inbound": { "adapter": "imap", "options": { "host": "$SMTP_HOSTNAME", "user": "$ZAMMAD_DEDICATED_EMAIL_ADDRESS", "password": "$ZAMMAD_DEDICATED_EMAIL_PASSWORD", "ssl": "ssl", "ssl_verify": true, "port": "993", "folder": "", "keep_on_server": false } }, "outbound": { "adapter": "smtp", "options": { "host": "$SMTP_HOSTNAME", "user": "$ZAMMAD_DEDICATED_EMAIL_ADDRESS", "password": "$ZAMMAD_DEDICATED_EMAIL_PASSWORD", "port": "587", "ssl_verify": true, "domain": "$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN", "enable_starttls_auto": true }, "email": "$ZAMMAD_DEDICATED_EMAIL_ADDRESS" } }, "active": true } ] } EOFJS dtnow=$(date '+%Y-%m-%d %H:%M:%S.%3N') cat < $HSHQ_STACKS_DIR/zammad/config/init.rb AutoWizard.setup UserInfo.current_user_id = 1 Setting.set('system_init_done', true) userd = User.where(email: "nicole.braun@zammad.org") userd.destroy_all od = Organization.find(1) Ticket.where(organization_id: od.id).update_all(organization_id: nil) od.members.each do |member| member.update!(organization_id: nil) end od.destroy RoleGroup.create(role_id: 2, group_id: 1, access: "full") LdapSource.create(id: 1, name: "HSHQ", preferences: {"host"=>"ldapserver", "ssl"=>"ssl", "ssl_verify"=>true, "options"=>{"$LDAP_BASE_DN"=>"$LDAP_BASE_DN"}, "option"=>"$LDAP_BASE_DN", "base_dn"=>"$LDAP_BASE_DN", "bind_user"=>"$LDAP_READONLY_USER_BIND_DN", "bind_pw"=>"$LDAP_READONLY_USER_PASSWORD", "user_uid"=>"uid", "user_filter"=>"(&(objectClass=person)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))", "group_uid"=>"dn", "group_filter"=>"(objectClass=groupOfUniqueNames)", "user_attributes"=>{"givenname"=>"firstname", "sn"=>"lastname", "mail"=>"email", "uid"=>"login"}, "group_role_map"=>{"cn=$LDAP_ADMIN_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN"=>["1"], "cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN"=>["2"]}, "unassigned_users"=>"skip_sync"}, prio: 0, active: true, updated_by_id: 3, created_by_id: 3, created_at: "${dtnow}000000 +0000", updated_at: "${dtnow}000000 +0000") ImportJob.create(name: 'Import::Ldap').start EOFLD cat < $HOME/zammad-compose.yml $STACK_VERSION_PREFIX zammad $(getScriptStackVersion zammad) services: zammad-db: image: $(getScriptImageByContainerName zammad-db) container_name: zammad-db hostname: zammad-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-zammad-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.zammad-hourly-db.schedule=@every 1h" - "ofelia.job-exec.zammad-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.zammad-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.zammad-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.zammad-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.zammad-hourly-db.email-from=Zammad Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.zammad-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.zammad-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.zammad-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.zammad-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.zammad-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.zammad-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.zammad-monthly-db.email-from=Zammad Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.zammad-monthly-db.mail-only-on-error=false" zammad-init: image: $(getScriptImageByContainerName zammad-init) container_name: zammad-init hostname: zammad-init restart: on-failure env_file: stack.env security_opt: - no-new-privileges:true command: zammad-init depends_on: - zammad-db networks: - int-zammad-net - dock-internalmail-net - dock-ldap-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-backup: image: $(getScriptImageByContainerName zammad-backup) container_name: zammad-backup hostname: zammad-backup restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-backup depends_on: - zammad-db networks: - int-zammad-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage - v-zammad-backup:/var/tmp/zammad zammad-nginx: image: $(getScriptImageByContainerName zammad-nginx) container_name: zammad-nginx hostname: zammad-nginx restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-nginx depends_on: - zammad-railsserver networks: - int-zammad-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-railsserver: image: $(getScriptImageByContainerName zammad-railsserver) container_name: zammad-railsserver hostname: zammad-railsserver restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-railsserver depends_on: - zammad-db networks: - int-zammad-net - dock-proxy-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/config:/config - v-zammad-storage:/opt/zammad/storage zammad-scheduler: image: $(getScriptImageByContainerName zammad-scheduler) container_name: zammad-scheduler hostname: zammad-scheduler restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-scheduler depends_on: - zammad-db networks: - int-zammad-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-websocket: image: $(getScriptImageByContainerName zammad-websocket) container_name: zammad-websocket hostname: zammad-websocket restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-websocket depends_on: - zammad-db networks: - int-zammad-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-memcache: image: $(getScriptImageByContainerName zammad-memcache) container_name: zammad-memcache hostname: zammad-memcache restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: memcached -m 256M networks: - int-zammad-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro zammad-redis: image: $(getScriptImageByContainerName zammad-redis) container_name: zammad-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $ZAMMAD_REDIS_PASSWORD --appendonly no networks: - int-zammad-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro # zammad-es: # image: $(getScriptImageByContainerName zammad-es) # container_name: zammad-es # hostname: zammad-es # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # mem_limit: 2g # networks: # - int-zammad-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - v-zammad-es:/usr/share/elasticsearch/data # If enabling minio, ensure to add this to environment variables: # S3_URL=http://$ZAMMAD_MINIO_KEY:$ZAMMAD_MINIO_SECRET@zammad-minio:9000/zammad?region=zammad&force_path_style=true # # zammad-minio: # image: $(getScriptImageByContainerName zammad-minio) # container_name: zammad-minio # hostname: zammad-minio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # command: server /data # networks: # - dock-proxy-net # - int-zammad-net # entrypoint: > # /bin/sh -c " # minio server /data --address ':9000' --console-address ':9001' & # MINIO_PID=\\\$! # while ! curl -s http://localhost:9000/minio/health/live; do # echo 'Waiting for MinIO to start...' # sleep 1 # done # sleep 5 # /usr/bin/mc alias set minio http://zammad-minio:9000 ${ZAMMAD_MINIO_KEY} ${ZAMMAD_MINIO_SECRET} # echo 'Creating bucket zammad' # /usr/bin/mc mb minio/zammad # wait \\\$MINIO_PID # " # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/minio:/data volumes: v-zammad-storage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zammad/storage v-zammad-backup: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zammad/backup v-zammad-es: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zammad/es networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-zammad-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/zammad.env TZ=\${PORTAINER_TZ} MEMCACHE_SERVERS=zammad-memcache:11211 POSTGRES_DB=$ZAMMAD_DATABASE_NAME POSTGRES_USER=$ZAMMAD_DATABASE_USER POSTGRES_PASSWORD=$ZAMMAD_DATABASE_USER_PASSWORD POSTGRESQL_DB=$ZAMMAD_DATABASE_NAME POSTGRESQL_HOST=zammad-db POSTGRESQL_USER=$ZAMMAD_DATABASE_USER POSTGRESQL_PASS=$ZAMMAD_DATABASE_USER_PASSWORD POSTGRESQL_PORT=5432 POSTGRESQL_OPTIONS=?pool=50 POSTGRESQL_DB_CREATE=true REDIS_URL=redis://:$ZAMMAD_REDIS_PASSWORD@zammad-redis:6379 BACKUP_DIR=/var/tmp/zammad BACKUP_TIME=03:00 HOLD_DAYS=10 ELASTICSEARCH_ENABLED=false ELASTICSEARCH_HOST=zammad-es ELASTICSEARCH_PORT=9200 ELASTICSEARCH_USER=$ZAMMAD_ES_USERNAME ELASTICSEARCH_PASS=$ZAMMAD_ES_PASSWORD ELASTICSEARCH_NAMESPACE=zammad ELASTICSEARCH_USERNAME=$ZAMMAD_ES_USERNAME ELASTICSEARCH_PASSWORD=$ZAMMAD_ES_PASSWORD discovery.type=single-node xpack.security.enabled=false ES_JAVA_OPTS=-Xms1g -Xmx1g NGINX_PORT=8080 NGINX_SERVER_SCHEME=https RAILS_TRUSTED_PROXIES=['127.0.0.0/8','172.16.0.0/12','192.168.0.0/16','10.0.0.0/8'] VIRTUAL_HOST=$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN ZAMMAD_HTTP_TYPE=https ZAMMAD_FQDN=$SUB_ZAMMAD_APP.$HOMESERVER_DOMAIN ZAMMAD_RAILSSERVER_HOST=zammad-railsserver ZAMMAD_RAILSSERVER_PORT=3000 ZAMMAD_WEBSOCKET_HOST=zammad-websocket ZAMMAD_WEBSOCKET_PORT=6042 ZAMMAD_WEB_CONCURRENCY= ZAMMAD_PROCESS_SESSIONS_JOBS_WORKERS= ZAMMAD_PROCESS_SCHEDULED_JOBS_WORKERS= ZAMMAD_PROCESS_DELAYED_JOBS_WORKERS= MINIO_ROOT_USER=$ZAMMAD_MINIO_KEY MINIO_ROOT_PASSWORD=$ZAMMAD_MINIO_SECRET MINIO_DOMAIN=zammad MINIO_CONSOLE_ADDRESS=:9090 EOFMT cat < $HSHQ_STACKS_DIR/zammad/dbexport/checkDBReady.sh #!/bin/bash PGPASSWORD=$ZAMMAD_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do testdb=\$(echo "select state_current from settings where name='application_secret';" | psql -t -A -U $ZAMMAD_DATABASE_USER $ZAMMAD_DATABASE_NAME 2> /dev/null) if ! [ -z "\$testdb" ]; then break fi echo "Database not ready, sleeping 3 seconds, total wait=\$curSeconds seconds..." sleep 3 curSeconds=\$((curSeconds+3)) done EOFDS chmod +x $HSHQ_STACKS_DIR/zammad/dbexport/checkDBReady.sh } function performUpdateZammad() { perform_stack_name=zammad prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/zammad/zammad:6.5.2-2,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/elasticsearch:8.19.4,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/zammad/zammad:6.5.2-2,ghcr.io/zammad/zammad:6.5.2-49" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/elasticsearch:8.19.4,mirror.gcr.io/elasticsearch:9.2.2" image_update_map[4]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[5]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[6]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/zammad/zammad:6.5.2-49,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/zammad/zammad:6.5.2-49,ghcr.io/zammad/zammad:6.5.2-49" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/elasticsearch:9.2.2" image_update_map[4]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[5]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[6]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true fixV2ComposeZammad perform_update_report="${perform_update_report}$stack_upgrade_report" startStopStack zammad stop sleep 2 startStopStack zammad start return ;; 3) newVer=v4 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/zammad/zammad:6.5.2-49,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/zammad/zammad:6.5.2-49,ghcr.io/zammad/zammad:6.5.2-49" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/elasticsearch:9.2.2" image_update_map[4]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[5]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" ;; 4) newVer=v4 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/zammad/zammad:6.5.2-49,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/memcached:1.6.39-alpine,ghcr.io/homeserverhq/minio:v1.0 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/zammad/zammad:6.5.2-49,ghcr.io/zammad/zammad:6.5.2-49" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[3]="mirror.gcr.io/elasticsearch:9.2.2,mirror.gcr.io/elasticsearch:9.2.2" image_update_map[4]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[5]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function fixV2ComposeZammad() { rm -f $HOME/zammad-compose.yml cat < $HOME/zammad-compose.yml $STACK_VERSION_PREFIX zammad v3 services: zammad-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: zammad-db hostname: zammad-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-zammad-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.zammad-hourly-db.schedule=@every 1h" - "ofelia.job-exec.zammad-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.zammad-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.zammad-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.zammad-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.zammad-hourly-db.email-from=Zammad Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.zammad-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.zammad-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.zammad-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.zammad-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.zammad-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.zammad-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.zammad-monthly-db.email-from=Zammad Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.zammad-monthly-db.mail-only-on-error=false" zammad-init: image: ghcr.io/zammad/zammad:6.5.2-49 container_name: zammad-init hostname: zammad-init restart: on-failure env_file: stack.env security_opt: - no-new-privileges:true command: zammad-init depends_on: - zammad-db networks: - int-zammad-net - dock-internalmail-net - dock-ldap-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-backup: image: ghcr.io/zammad/zammad:6.5.2-49 container_name: zammad-backup hostname: zammad-backup restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-backup depends_on: - zammad-db networks: - int-zammad-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage - v-zammad-backup:/var/tmp/zammad zammad-nginx: image: ghcr.io/zammad/zammad:6.5.2-49 container_name: zammad-nginx hostname: zammad-nginx restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-nginx depends_on: - zammad-railsserver networks: - int-zammad-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-railsserver: image: ghcr.io/zammad/zammad:6.5.2-49 container_name: zammad-railsserver hostname: zammad-railsserver restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-railsserver depends_on: - zammad-db networks: - int-zammad-net - dock-proxy-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/config:/config - v-zammad-storage:/opt/zammad/storage zammad-scheduler: image: ghcr.io/zammad/zammad:6.5.2-49 container_name: zammad-scheduler hostname: zammad-scheduler restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-scheduler depends_on: - zammad-db networks: - int-zammad-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-websocket: image: ghcr.io/zammad/zammad:6.5.2-49 container_name: zammad-websocket hostname: zammad-websocket restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: zammad-websocket depends_on: - zammad-db networks: - int-zammad-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-zammad-storage:/opt/zammad/storage zammad-memcache: image: mirror.gcr.io/memcached:1.6.39-alpine container_name: zammad-memcache hostname: zammad-memcache restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: memcached -m 256M networks: - int-zammad-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro zammad-redis: image: mirror.gcr.io/redis:8.4.0-bookworm container_name: zammad-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $ZAMMAD_REDIS_PASSWORD --appendonly no networks: - int-zammad-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro # zammad-es: # image: mirror.gcr.io/elasticsearch:9.2.2 # container_name: zammad-es # hostname: zammad-es # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # mem_limit: 2g # networks: # - int-zammad-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - v-zammad-es:/usr/share/elasticsearch/data # If enabling minio, ensure to add this to environment variables: # S3_URL=http://$ZAMMAD_MINIO_KEY:$ZAMMAD_MINIO_SECRET@zammad-minio:9000/zammad?region=zammad&force_path_style=true # # zammad-minio: # image: mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z # container_name: zammad-minio # hostname: zammad-minio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # command: server /data # networks: # - dock-proxy-net # - int-zammad-net # entrypoint: > # /bin/sh -c " # minio server /data --address ':9000' --console-address ':9001' & # MINIO_PID=\\\$! # while ! curl -s http://localhost:9000/minio/health/live; do # echo 'Waiting for MinIO to start...' # sleep 1 # done # sleep 5 # /usr/bin/mc alias set minio http://zammad-minio:9000 ${ZAMMAD_MINIO_KEY} ${ZAMMAD_MINIO_SECRET} # echo 'Creating bucket zammad' # /usr/bin/mc mb minio/zammad # wait \\\$MINIO_PID # " # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_STACKS_DIR}/zammad/minio:/data volumes: v-zammad-storage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zammad/storage v-zammad-backup: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zammad/backup v-zammad-es: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zammad/es networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-zammad-net: driver: bridge internal: true ipam: driver: default EOFMT } # Zulip function installZulip() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory zulip "Zulip" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zulip-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zulip-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zulip-memcache) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zulip-rabbitmq) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName zulip-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/zulip mkdir $HSHQ_STACKS_DIR/zulip/config mkdir $HSHQ_STACKS_DIR/zulip/data mkdir $HSHQ_STACKS_DIR/zulip/rabbitmq mkdir $HSHQ_STACKS_DIR/zulip/db mkdir $HSHQ_STACKS_DIR/zulip/dbexport chmod 777 $HSHQ_STACKS_DIR/zulip/dbexport initServicesCredentials set +e addUserMailu alias $ZULIP_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS ZULIP_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $ZULIP_ADMIN_PASSWORD | tr -d ':\n') outputConfigZulip installStack zulip zulip-app "nginx entered RUNNING state" $HOME/zulip.env 15 600 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$ZULIP_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_ZULIP_APP Admin Login Info" -b "$FMLNAME_ZULIP_APP Admin Username: $ZULIP_ADMIN_EMAIL_ADDRESS\n$FMLNAME_ZULIP_APP Admin Password: $ZULIP_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" ZULIP_INIT_ENV=true updateConfigVar ZULIP_INIT_ENV $ZULIP_INIT_ENV fi sleep 3 set +e docker exec -u zulip zulip-app bash -c "/data/settings/etc-zulip/initRealm.sh" > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/zulip/config/initRealm.sh addReadOnlyUserToDatabase Zulip postgres zulip-db $ZULIP_DATABASE_NAME $ZULIP_DATABASE_USER $ZULIP_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/zulip/dbexport $ZULIP_DATABASE_READONLYUSER $ZULIP_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://zulip-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ZULIP_APP $MANAGETLS_ZULIP_APP "$is_integrate_hshq" $NETDEFAULT_ZULIP_APP "$inner_block" insertSubAuthelia $SUB_ZULIP_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll zulip "$FMLNAME_ZULIP_APP" $USERTYPE_ZULIP_APP "https://$SUB_ZULIP_APP.$HOMESERVER_DOMAIN" "zulip.png" "$(getHeimdallOrderFromSub $SUB_ZULIP_APP $USERTYPE_ZULIP_APP)" restartAllCaddyContainers checkAddDBConnection true zulip "$FMLNAME_ZULIP_APP" postgres zulip-db $ZULIP_DATABASE_NAME $ZULIP_DATABASE_USER $ZULIP_DATABASE_USER_PASSWORD fi } function outputConfigZulip() { cat < $HOME/zulip-compose.yml $STACK_VERSION_PREFIX zulip $(getScriptStackVersion zulip) services: zulip-db: image: $(getScriptImageByContainerName zulip-db) container_name: zulip-db hostname: zulip-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-zulip-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zulip/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zulip/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.zulip-hourly-db.schedule=@every 1h" - "ofelia.job-exec.zulip-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.zulip-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.zulip-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.zulip-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.zulip-hourly-db.email-from=Zulip Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.zulip-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.zulip-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.zulip-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.zulip-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.zulip-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.zulip-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.zulip-monthly-db.email-from=Zulip Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.zulip-monthly-db.mail-only-on-error=false" zulip-app: image: $(getScriptImageByContainerName zulip-app) container_name: zulip-app hostname: zulip-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true entrypoint: - /newentry.sh command: - app:run depends_on: - zulip-db - zulip-memcache - zulip-rabbitmq - zulip-redis networks: - int-zulip-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/zulip/newentry.sh:/newentry.sh - \${PORTAINER_HSHQ_STACKS_DIR}/zulip/config:/data/settings/etc-zulip - v-zulip-data:/data:rw ulimits: nofile: soft: 1000000 hard: 1048576 zulip-memcache: image: $(getScriptImageByContainerName zulip-memcache) container_name: zulip-memcache hostname: zulip-memcache restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: - "sh" - "-euc" - | echo 'mech_list: plain' > "\$\$SASL_CONF_PATH" echo "zulip@\$\$HOSTNAME:\$\$MEMCACHED_PASSWORD" > "\$\$MEMCACHED_SASL_PWDB" echo "zulip@localhost:\$\$MEMCACHED_PASSWORD" >> "\$\$MEMCACHED_SASL_PWDB" exec memcached -S networks: - int-zulip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro zulip-rabbitmq: image: $(getScriptImageByContainerName zulip-rabbitmq) container_name: zulip-rabbitmq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-zulip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-zulip-rabbitmq:/var/lib/rabbitmq:rw zulip-redis: image: $(getScriptImageByContainerName zulip-redis) container_name: zulip-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $ZULIP_REDIS_PASSWORD --appendonly no networks: - int-zulip-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-zulip-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zulip/data v-zulip-rabbitmq: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/zulip/rabbitmq networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-zulip-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/zulip.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$ZULIP_DATABASE_NAME POSTGRES_USER=$ZULIP_DATABASE_USER POSTGRES_PASSWORD=$ZULIP_DATABASE_USER_PASSWORD SASL_CONF_PATH=/home/memcache/memcached.conf MEMCACHED_SASL_PWDB=/home/memcache/memcached-sasl-db MEMCACHED_PASSWORD=$ZULIP_MEMCACHE_PASSWORD RABBITMQ_DEFAULT_USER=$ZULIP_RABBITMQ_USERNAME RABBITMQ_DEFAULT_PASS=$ZULIP_RABBITMQ_PASSWORD REDIS_PASSWORD=$ZULIP_REDIS_PASSWORD SSL_CERTIFICATE_GENERATION=self-signed DISABLE_HTTPS=True LOADBALANCER_IPS=$DOCKER_NETWORK_RESERVED_RANGE MANUAL_CONFIGURATION=True LINK_SETTINGS_TO_DATA=True SETTING_EXTERNAL_HOST=$SUB_ZULIP_APP.$HOMESERVER_DOMAIN SETTING_ZULIP_ADMINISTRATOR=$ZULIP_ADMIN_EMAIL_ADDRESS DB_HOST=zulip-db DB_HOST_PORT=5432 DB_NAME=$ZULIP_DATABASE_NAME DB_USER=$ZULIP_DATABASE_USER SECRETS_postgres_password=$ZULIP_DATABASE_USER_PASSWORD EOFMT cat < $HSHQ_STACKS_DIR/zulip/config/settings.py from typing import Any from .config import get_secret ################################################################ ## Zulip Server settings. ## ## This file controls settings that affect the whole Zulip server. ## See our documentation at: ## https://zulip.readthedocs.io/en/latest/production/settings.html ## ## For developer documentation on the Zulip settings system, see: ## https://zulip.readthedocs.io/en/latest/subsystems/settings.html ## ## Remember to restart the server after making changes here! ## su zulip -c /home/zulip/deployments/current/scripts/restart-server ################ ## Mandatory settings. ## ## These settings MUST be set in production. In a development environment, ## sensible default values will be used. ## The email address for the person or team who maintains the Zulip ## installation. Note that this is a public-facing email address; it may ## appear on 404 pages, is used as the sender's address for many automated ## emails, and is advertised as a support address. An email address like ## support@example.com is totally reasonable, as is admin@example.com. ## Do not put a display name; e.g. "support@example.com", not ## "Zulip Support ". ZULIP_ADMINISTRATOR = "$ZULIP_ADMIN_EMAIL_ADDRESS" ## The user-accessible Zulip hostname for this installation, e.g. ## zulip.example.com. This should match what users will put in their ## web browser. If you want to allow multiple hostnames, add the rest ## to ALLOWED_HOSTS. ## ## If you need to access the server on a specific port, you should set ## EXTERNAL_HOST to e.g. zulip.example.com:1234 here. EXTERNAL_HOST = "$SUB_ZULIP_APP.$HOMESERVER_DOMAIN" ## Alternative hostnames. A comma-separated list of strings ## representing the host/domain names that your users can enter in ## their browsers to access Zulip. This is a security measure; for ## details, see the Django documentation: ## https://docs.djangoproject.com/en/5.0/ref/settings/#allowed-hosts ## ## Zulip automatically adds to this list "localhost", "127.0.0.1", and ## patterns representing EXTERNAL_HOST and subdomains of it. If you are ## accessing your server by other hostnames, list them here. ## ## Note that these should just be hostnames, without port numbers. # ALLOWED_HOSTS = ["zulip-alias.example.com", "192.0.2.1"] ## If EXTERNAL_HOST is not a valid domain name (e.g. an IP address), ## set FAKE_EMAIL_DOMAIN below to a domain that Zulip can use when ## generating (fake) email addresses for bots, dummy users, etc. # FAKE_EMAIL_DOMAIN = "fake-domain.example.com" ################ ## Outgoing email (SMTP) settings. ## ## Zulip needs to be able to send email (that is, use SMTP) so it can ## confirm new users' email addresses and send notifications. ## ## If you don't already have an SMTP provider, free ones are available. ## ## For more details, including a list of free SMTP providers and ## advice for troubleshooting, see the Zulip documentation: ## https://zulip.readthedocs.io/en/latest/production/email.html ## EMAIL_HOST and EMAIL_HOST_USER are generally required. If your ## SMTP server does not require authentication, leave EMAIL_HOST_USER ## commented out. EMAIL_HOST = "$SMTP_HOSTNAME" EMAIL_HOST_USER = "" ## Passwords and secrets are not stored in this file. The password ## for user EMAIL_HOST_USER goes in /etc/zulip/zulip-secrets.conf. ## In that file, set email_password. For example: # email_password = abcd1234 ## EMAIL_USE_TLS and EMAIL_PORT are required for most SMTP providers. EMAIL_USE_TLS = False EMAIL_USE_SSL = False EMAIL_PORT = $SMTP_HOSTPORT ## The noreply address to be used as the sender for certain generated ## emails. Messages sent to this address could contain sensitive user ## data and should not be delivered anywhere. The default is ## e.g. noreply-{token}@zulip.example.com (if EXTERNAL_HOST is ## zulip.example.com). There are potential security issues if you set ## ADD_TOKENS_TO_NOREPLY_ADDRESS=False to remove the token; see ## https://zulip.readthedocs.io/en/latest/production/email.html for details. # ADD_TOKENS_TO_NOREPLY_ADDRESS = True # TOKENIZED_NOREPLY_EMAIL_ADDRESS = "noreply-{token}@example.com" ## NOREPLY_EMAIL_ADDRESS is the sender for noreply emails that don't ## contain confirmation links (where the security problem fixed by ## ADD_TOKENS_TO_NOREPLY_ADDRESS does not exist), as well as for ## confirmation emails when ADD_TOKENS_TO_NOREPLY_ADDRESS=False. # NOREPLY_EMAIL_ADDRESS = "noreply@example.com" ## Emails sent by the Zulip server will use a sender name starting ## with INSTALLATION_NAME. The default is EXTERNAL_HOST. If INSTALLATION_NAME is ## "zulip.example.com", email senders names will include: ## * "zulip.example.com notifications" (message notification emails). ## * "zulip.example.com account security" (account security emails). INSTALLATION_NAME = "Zulip $(getAdminEmailName)" ## Many countries and bulk mailers require certain types of email to display ## a physical mailing address to comply with anti-spam legislation. ## Non-commercial and non-public-facing installations are unlikely to need ## this setting. ## The address should have no newlines. # PHYSICAL_ADDRESS = "" ################ ## Email gateway integration. ## ## The email gateway integration supports sending messages into Zulip ## by sending an email. ## For details, see the documentation: ## https://zulip.readthedocs.io/en/latest/production/email-gateway.html EMAIL_GATEWAY_PATTERN = "" ## If you are using polling, edit the IMAP settings below: ## ## The IMAP login; username here and password as email_gateway_password in ## zulip-secrets.conf. EMAIL_GATEWAY_LOGIN = "" ## The IMAP server & port to connect to EMAIL_GATEWAY_IMAP_SERVER = "" EMAIL_GATEWAY_IMAP_PORT = 993 ## The IMAP folder name to check for emails. All emails sent to EMAIL_GATEWAY_PATTERN above ## must be delivered to this folder EMAIL_GATEWAY_IMAP_FOLDER = "INBOX" ################ ## Authentication settings. ## ## Enable at least one of the following authentication backends. ## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html ## for documentation on our authentication backends. ## ## The install process requires EmailAuthBackend (the default) to be ## enabled. If you want to disable it, do so after creating the ## initial realm and user. AUTHENTICATION_BACKENDS: tuple[str, ...] = ( "zproject.backends.EmailAuthBackend", # Email and password; just requires SMTP setup # "zproject.backends.GoogleAuthBackend", # Google auth, setup below # "zproject.backends.GitHubAuthBackend", # GitHub auth, setup below # "zproject.backends.GitLabAuthBackend", # GitLab auth, setup below # "zproject.backends.AzureADAuthBackend", # Microsoft Entra ID (AzureAD) auth, setup below # "zproject.backends.AppleAuthBackend", # Apple auth, setup below # "zproject.backends.SAMLAuthBackend", # SAML, setup below "zproject.backends.ZulipLDAPAuthBackend", # LDAP, setup below # "zproject.backends.ZulipRemoteUserBackend", # Local SSO, setup docs on readthedocs # "zproject.backends.GenericOpenIdConnectBackend", # Generic OIDC integration, setup below ) ## LDAP integration. ## ## Zulip supports retrieving information about users via LDAP, and ## optionally using LDAP as an authentication mechanism. import ldap from django_auth_ldap.config import GroupOfUniqueNamesType, LDAPGroupQuery, LDAPSearch # noqa: F401 ## Connecting to the LDAP server. ## ## For detailed instructions, see the Zulip documentation: ## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#ldap ## The LDAP server to connect to. Setting this enables Zulip ## automatically fetching each new user's name from LDAP. AUTH_LDAP_SERVER_URI = "ldaps://ldapserver" ## The DN of the user to bind as (i.e., authenticate as) in order to ## query LDAP. If unset, Zulip does an anonymous bind. AUTH_LDAP_BIND_DN = "$LDAP_READONLY_USER_BIND_DN" ## Passwords and secrets are not stored in this file. The password ## corresponding to AUTH_LDAP_BIND_DN goes in /etc/zulip/zulip-secrets.conf. ## In that file, set auth_ldap_bind_password. For example: # auth_ldap_bind_password = abcd1234 ## Mapping user info from LDAP to Zulip. ## ## For detailed instructions, see the Zulip documentation: ## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#ldap ## The LDAP search query to find a given user. ## ## The arguments to LDAPSearch are (base DN, scope, filter). In the ## filter, the string %(user)s is a Python placeholder. The Zulip ## server will replace this with the user's Zulip username, i.e. the ## name they type into the Zulip login form. ## ## For more details and alternatives, see the documentation linked above. AUTH_LDAP_USER_SEARCH = LDAPSearch( "ou=people,$LDAP_BASE_DN", ldap.SCOPE_SUBTREE, "(&(uid=%(user)s)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))" ) ## Configuration to look up a user's LDAP data given their email address ## (for Zulip reverse mapping). If users log in as e.g. "sam" when ## their email address is "sam@example.com", set LDAP_APPEND_DOMAIN to ## "example.com". Otherwise, leave LDAP_APPEND_DOMAIN=None and set ## AUTH_LDAP_REVERSE_EMAIL_SEARCH and AUTH_LDAP_USERNAME_ATTR below. # LDAP_APPEND_DOMAIN = None ## LDAP attribute to find a user's email address. ## ## Leave as None if users log in with their email addresses, ## or if using LDAP_APPEND_DOMAIN. LDAP_EMAIL_ATTR = "mail" ## AUTH_LDAP_REVERSE_EMAIL_SEARCH works like AUTH_LDAP_USER_SEARCH and ## should query an LDAP user given their email address. It and ## AUTH_LDAP_USERNAME_ATTR are required when LDAP_APPEND_DOMAIN is None. AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch( "ou=people,$LDAP_BASE_DN", ldap.SCOPE_SUBTREE, "(&(email=%(email)s)(memberOf=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN))" ) ## AUTH_LDAP_USERNAME_ATTR should be the Zulip username attribute ## (defined in AUTH_LDAP_USER_SEARCH). AUTH_LDAP_USERNAME_ATTR = "uid" ## This map defines how to populate attributes of a Zulip user from LDAP. ## ## The format is zulip_name: ldap_name; each entry maps a Zulip ## concept (on the left) to the LDAP attribute name (on the right) your ## LDAP database uses for the same concept. AUTH_LDAP_USER_ATTR_MAP = { ## full_name is required; common values include "cn" or "displayName". ## If names are encoded in your LDAP directory as first and last ## name, you can instead specify first_name and last_name, and ## Zulip will combine those to construct a full_name automatically. "full_name": "cn", "first_name": "givenName", "last_name": "sn", # ## A stable unique identifier for a user allows Zulip to ## automatically handle email address changes. ## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#identifying-user-accounts-via-a-unique-ldap-attribute "unique_account_id": "uid", ## ## Profile pictures can be pulled from the LDAP "thumbnailPhoto"/"jpegPhoto" field. "avatar": "jpegPhoto", ## ## This line is for having Zulip to automatically deactivate users ## who are disabled in LDAP/Active Directory (and reactivate users who are not). ## See docs for usage details and precise semantics. # "userAccountControl": "userAccountControl", ## Alternatively, you can map "deactivated" to a boolean attribute ## that is "TRUE" for deactivated users and "FALSE" otherwise. # "deactivated": "nsAccountLock", ## Restrict access to organizations using an LDAP attribute. ## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#restricting-ldap-user-access-to-specific-organizations # "org_membership": "department", } ## Whether to automatically deactivate users not found in LDAP. If LDAP ## is the only authentication method, then this setting defaults to ## True. If other authentication methods are enabled, it defaults to ## False. # LDAP_DEACTIVATE_NON_MATCHING_USERS = True ## See: https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#restricting-ldap-user-access-to-specific-organizations # AUTH_LDAP_ADVANCED_REALM_ACCESS_CONTROL = { # "zulip": # [ # OR # { # AND # "department": "main", # "employeeType": "staff" # } # ] # } ## LDAP group sync configuration. ## See: https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#synchronizing-groups AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType() AUTH_LDAP_GROUP_SEARCH = LDAPSearch( "ou=groups,$LDAP_BASE_DN", ldap.SCOPE_SUBTREE, "(objectClass=groupOfUniqueNames)" ) # LDAP_SYNCHRONIZED_GROUPS_BY_REALM = { # "subdomain1" : [ # "group1", # "group2", # ] # } ######## ## Google OAuth. ## ## To set up Google authentication, you'll need to do the following: ## ## (1) Visit https://console.developers.google.com/ , navigate to ## "APIs & Services" > "Credentials", and create a "Project" which will ## correspond to your Zulip instance. ## ## (2) Go to "Oauth consent screen" and create a consent screen, ## authorizing your domain and enabling the .../auth/userinfo.email, ## .../auth/userinfo.profile and openid scopes. If all of your users ## share a Google Workspace, you can select the "Internal" user type ## during Oauth screen creation to limit authentication via this ## backend to users within your Google Workspace organization. ## ## (3) Return to "Credentials", and select "Create credentials". ## Choose "OAuth client ID", and fill in the app name as desired ## and "Authorized redirect URIs" with a value like ## https://zulip.example.com/complete/google/ ## based on your value for EXTERNAL_HOST. ## ## (4) You should get a client ID and a client secret. Copy them. ## Use the client ID as SOCIAL_AUTH_GOOGLE_KEY here, and put the ## client secret in zulip-secrets.conf as social_auth_google_secret. # SOCIAL_AUTH_GOOGLE_KEY = "" ######## ## GitLab OAuth. ## ## To set up GitLab authentication, you'll need to do the following: ## ## (1) Register an OAuth application with GitLab at ## https://gitlab.com/oauth/applications ## Or the equivalent URL on a self-hosted GitLab server. ## (2) Fill in the "Redirect URI" with a value like ## http://zulip.example.com/complete/gitlab/ ## based on your value for EXTERNAL_HOST. ## (3) For "scopes", select only "read_user", and create the application. ## (4) You'll end up on a page with the Application ID and Secret for ## your new GitLab application. Use the Application ID as ## SOCIAL_AUTH_GITLAB_KEY here, and put the Secret in ## zulip-secrets.conf as social_auth_gitlab_secret. ## (5) If you are self-hosting GitLab, provide the URL of the ## GitLab server as SOCIAL_AUTH_GITLAB_API_URL here. # SOCIAL_AUTH_GITLAB_KEY = "" # SOCIAL_AUTH_GITLAB_API_URL = "https://gitlab.example.com" ######## ## GitHub OAuth. ## ## To set up GitHub authentication, you'll need to do the following: ## ## (1) Register an OAuth2 application with GitHub at one of: ## https://github.com/settings/developers ## https://github.com/organizations/ORGNAME/settings/developers ## Fill in "Callback URL" with a value like ## https://zulip.example.com/complete/github/ as ## based on your values for EXTERNAL_HOST and SOCIAL_AUTH_SUBDOMAIN. ## ## (2) You should get a page with settings for your new application, ## showing a client ID and a client secret. Use the client ID as ## SOCIAL_AUTH_GITHUB_KEY here, and put the client secret in ## zulip-secrets.conf as social_auth_github_secret. # SOCIAL_AUTH_GITHUB_KEY = "" ## (3) Optionally, you can configure the GitHub integration to only ## allow members of a particular GitHub team or organization to log ## into your Zulip server through GitHub authentication. To enable ## this, set one of the two parameters below: # SOCIAL_AUTH_GITHUB_TEAM_ID = "" # SOCIAL_AUTH_GITHUB_ORG_NAME = "" ## (4) If you are serving multiple Zulip organizations on different ## subdomains, you need to set SOCIAL_AUTH_SUBDOMAIN. You can set it ## to any subdomain on which you do not plan to host a Zulip ## organization. The default recommendation, auth, is a reserved ## subdomain; if you're using this setting, the "Callback URL" should be e.g.: ## https://auth.zulip.example.com/complete/github/ # # SOCIAL_AUTH_SUBDOMAIN = "auth" ######## ## Generic OpenID Connect (OIDC). See also documentation here: ## ## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#openid-connect ## SOCIAL_AUTH_OIDC_ENABLED_IDPS: dict[str, Any] = { ## This field (example: "idp_name") may appear in URLs during ## authentication, but is otherwise not user-visible. "idp_name": { ## The base path to the provider's OIDC API. Zulip fetches the ## IdP's configuration from the discovery endpoint, which will be ## "{oidc_url}/.well-known/openid-configuration". "oidc_url": "https://example.com/api/openid", ## The display name, used for "Log in with " buttons. "display_name": "Example", ## Optional: URL of an icon to decorate "Log in with " buttons. "display_icon": None, ## The client_id and secret provided by your OIDC IdP. To keep ## settings.py free of secrets, the get_secret call below ## reads the secret with the specified name from zulip-secrets.conf. "client_id": "", "secret": get_secret("social_auth_oidc_secret"), ## Determines whether "Log in with OIDC" will automatically ## register a new account if one does not already exist. By ## default, Zulip asks the user whether they want to create an ## account or try to log in again using another method. # "auto_signup": False, } } ## For documentation on this setting, see the relevant part of ## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#openid-connect # SOCIAL_AUTH_OIDC_FULL_NAME_VALIDATED = True ######## ## SAML authentication ## ## For SAML authentication, you will need to configure the settings ## below using information from your SAML identity provider, as ## explained in: ## ## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#saml ## ## You will need to modify these SAML settings: SOCIAL_AUTH_SAML_ORG_INFO = { "en-US": { "displayname": "Example, Inc. Zulip", "name": "zulip", "url": "{}{}".format("https://", EXTERNAL_HOST), }, } SOCIAL_AUTH_SAML_ENABLED_IDPS: dict[str, Any] = { ## The fields are explained in detail here: ## https://python-social-auth.readthedocs.io/en/latest/backends/saml.html "idp_name": { ## Configure entity_id and url according to information provided to you by your IdP: "entity_id": "https://idp.testshib.org/idp/shibboleth", "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", ## ## The part below corresponds to what's likely referred to as something like ## "Attribute Statements" (with Okta as your IdP) or "Attribute Mapping" (with Google Workspace). ## The names on the right side need to correspond to the names under which ## the IdP will send the user attributes. With these defaults, it's expected ## that the user's email will be sent with the "email" attribute name, ## the first name and the last name with the "first_name", "last_name" attribute names. "attr_user_permanent_id": "email", "attr_first_name": "first_name", "attr_last_name": "last_name", "attr_username": "email", "attr_email": "email", ## List of additional attributes to fetch from the SAMLResponse. ## These attributes will be available for synchronizing user profile fields. ## in SOCIAL_AUTH_SYNC_ATTRS_DICT. # "extra_attrs": ["title", "mobilePhone", "zulip_role"], ## ## The "x509cert" attribute is automatically read from ## /etc/zulip/saml/idps/{idp_name}.crt; don't specify it here. ## ## Optionally, you can edit display_name and display_icon ## settings below to change the name and icon that will show on ## the login button. "display_name": "SAML", ## ## Path to a square image file containing a logo to appear at ## the left end of the login/register buttons for this IDP. ## The default of None results in a text-only button. # "display_icon": "/path/to/icon.png", ## ## If you want this IdP to only be enabled for authentication ## to certain subdomains, uncomment and edit the setting below. # "limit_to_subdomains": ["subdomain1", "subdomain2"], ## ## You can also limit subdomains by setting "attr_org_membership" ## to be a SAML attribute containing the allowed subdomains for a user. # "attr_org_membership": "member", ## ## Determines whether "Log in with SAML" will automatically ## register a new account if one does not already exist. By ## default, Zulip asks the user whether they want to create an ## account or try to log in again using another method. # "auto_signup": False, ## Determines whether Service Provider initiated SAML Single Logout should be enabled. ## Note that IdP-initiated Single Logout must be configured before enabling this. # "sp_initiated_logout_enabled": False, }, } # More complete documentation of the configurable security settings # are available in the "security" part of https://github.com/onelogin/python3-saml#settings. SOCIAL_AUTH_SAML_SECURITY_CONFIG: dict[str, Any] = { ## If you've set up the optional private and public server keys, ## set this to True to enable signing of SAMLRequests using the ## private key. "authnRequestsSigned": False, ## If you'd like the Zulip server to request that the IdP limit user identity ## verification to a specific set of authentication contexts, you can do this ## by changing the requestedAuthnContext parameter to a list of specific ## Authentication Context Classes that you want to include in the AuthnContext. E.g.: ## # "requestedAuthnContext": ["urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", # "urn:oasis:names:tc:SAML:2.0:ac:classes:X509"], ## ## For details on this, see the aforementioned python3-saml documentation ## and https://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf "requestedAuthnContext": False, } ## These SAML settings you likely won't need to modify. SOCIAL_AUTH_SAML_SP_ENTITY_ID = "https://" + EXTERNAL_HOST SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = { "givenName": "Technical team", "emailAddress": ZULIP_ADMINISTRATOR, } SOCIAL_AUTH_SAML_SUPPORT_CONTACT = { "givenName": "Support team", "emailAddress": ZULIP_ADMINISTRATOR, } ## Note: Any additional SAML attributes that'll be used here must be ## listed in the "extra_attrs" field in the SOCIAL_AUTH_SAML_ENABLED_IDPS ## configuration for your IdP. # SOCIAL_AUTH_SYNC_ATTRS_DICT = { # "example_org": { # "saml": { # # role is currently the only supported major attribute. # "role": "zulip_role", # # Specify custom profile fields with a custom__ prefix for the # # Zulip field name. # "custom__title": "title", # # Sync the membership of the listed Zulip groups with # # the list of group names sent in the "zulip_groups" # # attribute in the SAMLResponse. # "groups": ["group1", "group2", ("samlgroup3", "zulipgroup3"), "group4"], # } # } # } ######## ## Apple authentication ("Sign in with Apple"). ## ## Configure the below settings by following the instructions here: ## ## https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#sign-in-with-apple # # SOCIAL_AUTH_APPLE_TEAM = "" # SOCIAL_AUTH_APPLE_SERVICES_ID = "" # SOCIAL_AUTH_APPLE_APP_ID = "" # SOCIAL_AUTH_APPLE_KEY = "" ######## ## Microsoft Entra ID (AzureAD) OAuth. ## ## To set up Microsoft Entra ID authentication, you'll need to do the following: ## ## (1) Open "App registrations" at ## https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade ## and click "New registration". ## ## (2) In the "Redirect URI (optional)" section, select Web as the platform ## and enter https://zulip.example.com/complete/azuread-oauth2/ as the redirect URI, ## based on your values of EXTERNAL_HOST and SOCIAL_AUTH_SUBDOMAIN. ## ## (3) After registering the app, go to "Certificates & secrets" and ## generate a new client secret. Make sure to save the generated Value. ## ## (4) Enter the application ID for the app as SOCIAL_AUTH_AZUREAD_OAUTH2_KEY below ## and the generated secret Value in zulip-secrets.conf as social_auth_azuread_oauth2_secret. # SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = "" ######## ## SSO via REMOTE_USER. ## ## If you are using the ZulipRemoteUserBackend authentication backend, ## and REMOTE_USER does not already include a domain, set this to your ## domain (e.g. if REMOTE_USER is "username" and the corresponding ## email address is "username@example.com", set SSO_APPEND_DOMAIN = ## "example.com"), otherwise leave this as None. # SSO_APPEND_DOMAIN = None ## JWT authentication. ## ## JWT authentication is supported both to transparently log users ## into Zulip or to fetch users' API keys. The JWT secret key and ## algorithm must be configured here. ## ## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#jwt # JWT_AUTH_KEYS = { # # Subdomain for which this JWT configuration will apply. # "zulip": { # # Shared secret key used to validate jwt tokens, which should be stored # # in zulip-secrets.conf and is read by the get_secret call below. # # The key needs to be securely, randomly generated. Note that if you're # # using the default HS256 algorithm, per RFC 7518, the key needs # # to have at least 256 bits of entropy. # "key": get_secret("jwt_auth_key"), # # Algorithm with which the JWT token are signed. # "algorithms": ["HS256"], # } # } ################ ## Service configuration ######## ## PostgreSQL configuration. ## ## To access an external PostgreSQL database you should define the host name in ## REMOTE_POSTGRES_HOST, port in REMOTE_POSTGRES_PORT, password in the secrets file in the ## property postgres_password, and the SSL connection mode in REMOTE_POSTGRES_SSLMODE ## Valid values for REMOTE_POSTGRES_SSLMODE are documented in the ## "SSL Mode Descriptions" table in ## https://www.postgresql.org/docs/9.5/static/libpq-ssl.html REMOTE_POSTGRES_HOST = "zulip-db" REMOTE_POSTGRES_PORT = "5432" REMOTE_POSTGRES_SSLMODE = "prefer" ######## ## RabbitMQ configuration. ## ## By default, Zulip connects to RabbitMQ running locally on the ## machine, but Zulip also supports connecting to RabbitMQ over the ## network; to use a remote RabbitMQ instance, set RABBITMQ_HOST, and ## optionally RABBITMQ_PORT, to the hostname and port here. RABBITMQ_HOST = "zulip-rabbitmq" #RABBITMQ_PORT = 5672 ## To use a different RabbitMQ "virtual host", adjust this. # RABBITMQ_VHOST = "/" ## To use another RabbitMQ user than the default "zulip", set RABBITMQ_USERNAME here. RABBITMQ_USERNAME = "$ZULIP_RABBITMQ_USERNAME" ## To access the RabbitMQ server over TLS, set this to True; this is ## generally only necessary if RabbitMQ is running on a separate, ## cloud-managed, host. # RABBITMQ_USE_TLS = False ######## ## Redis configuration. ## ## By default, Zulip connects to Redis running locally on the machine, ## but Zulip also supports connecting to Redis over the network; ## to use a remote Redis instance, set REDIS_HOST here. REDIS_HOST = "zulip-redis" ## For a different Redis port set the REDIS_PORT here. REDIS_PORT = 6379 ## If you set redis_password in zulip-secrets.conf, Zulip will use that password ## to connect to the Redis server. ######## ## Memcached configuration. ## ## By default, Zulip connects to memcached running locally on the machine, ## but Zulip also supports connecting to memcached over the network; ## to use a remote Memcached instance, set MEMCACHED_LOCATION here. ## Format HOST:PORT MEMCACHED_LOCATION = "zulip-memcache:11211" ## To authenticate to memcached, set memcached_password in zulip-secrets.conf, ## and optionally change the default username "zulip@localhost" here. MEMCACHED_USERNAME = "zulip@zulip-memcache" ################ ## Previews. ######## ## Image and URL previews. ## ## Controls whether or not Zulip will provide inline image preview when ## a link to an image is referenced in a message. Note: this feature ## can also be disabled in a realm's organization settings. # INLINE_IMAGE_PREVIEW = True ## Controls whether or not Zulip will provide inline previews of ## websites that are referenced in links in messages. Note: this feature ## can also be disabled in a realm's organization settings. # INLINE_URL_EMBED_PREVIEW = True ######## ## Twitter previews. ## ## Zulip supports showing inline Tweet previews when a tweet is linked ## to in a message. To support this, Zulip must have access to the ## Twitter API via OAuth. To obtain the various access tokens needed ## below, you must register a new application under your Twitter ## account by doing the following: ## ## 1. Log in to http://dev.twitter.com. ## 2. In the menu under your username, click My Applications. From this page, create a new application. ## 3. Click on the application you created and click "create my access token". ## 4. Fill in the values for twitter_consumer_key, twitter_consumer_secret, twitter_access_token_key, ## and twitter_access_token_secret in /etc/zulip/zulip-secrets.conf. ################ ## Logging and error reporting. ## ## Controls whether or not error reports (tracebacks) are emailed to the ## server administrators. ERROR_REPORTING = False ## Controls the DSN used to report errors to Sentry.io # SENTRY_DSN = "https://aaa@bbb.ingest.sentry.io/1234" # SENTRY_FRONTEND_DSN = "https://aaa@bbb.ingest.sentry.io/1234" ## What portion of events are sampled (https://docs.sentry.io/platforms/javascript/configuration/sampling/): # SENTRY_FRONTEND_SAMPLE_RATE = 1.0 # SENTRY_FRONTEND_TRACE_RATE = 0.1 ## If True, each log message in the server logs will identify the ## Python module where it came from. Useful for tracking down a ## mysterious log message, but a little verbose. # LOGGING_SHOW_MODULE = False ## If True, each log message in the server logs will identify the ## process ID. Useful for correlating logs with information from ## system-level monitoring tools. # LOGGING_SHOW_PID = False ################# ## Animated GIF integration powered by GIPHY. See: ## https://zulip.readthedocs.io/en/latest/production/giphy-gif-integration.html # GIPHY_API_KEY = "" ################ ## Video call integrations. ## ## Controls the Zoom video call integrations. See: ## https://zulip.readthedocs.io/en/latest/production/video-calls.html # VIDEO_ZOOM_CLIENT_ID = "" # VIDEO_ZOOM_SERVER_TO_SERVER_ACCOUNT_ID = "" ## Controls the Jitsi Meet video call integration. By default, the ## integration uses the SaaS https://meet.jit.si server. You can specify ## your own Jitsi Meet server, or if you'd like to disable the ## integration, set JITSI_SERVER_URL = None. JITSI_SERVER_URL = "https://$SUB_JITSI.$HOMESERVER_DOMAIN" ## Controls the BigBlueButton video call integration. You must also ## set big_blue_button_secret in zulip-secrets.conf. # BIG_BLUE_BUTTON_URL = "https://bbb.example.com/bigbluebutton/" ################ ## AI Features ## ## Specify the model and provider to use for topic summarization. The ## model field from https://docs.litellm.ai/docs/providers specifies ## your preferred provider/model combination. # TOPIC_SUMMARIZATION_MODEL = "huggingface/meta-llama/Meta-Llama-3-8B-Instruct" ## Other configuration parameters, passed through to litellm's completion call ## See https://docs.litellm.ai/docs/completion/input # TOPIC_SUMMARIZATION_PARAMETERS = {} ## Set usage costs based on your model, and a maximum per-user monthly ## quota. Units are USD or other currency of your choice. # OUTPUT_COST_PER_GIGATOKEN = 500 # INPUT_COST_PER_GIGATOKEN = 400 # MAX_PER_USER_MONTHLY_AI_COST = 0.5 ################ ## Miscellaneous settings. ## If you host multiple organizations on the same Zulip server, you ## can customize the hostname that one of them uses by adding it here. ## See https://zulip.readthedocs.io/en/latest/production/multiple-organizations.html#other-hostnames ## Note that you still need SSL certificates for this other custom hostname: ## https://zulip.readthedocs.io/en/latest/production/multiple-organizations.html#ssl-certificates #REALM_HOSTS = { # "$(echo $HOMESERVER_DOMAIN | cut -d"." -f1)": "$SUB_ZULIP_APP.$HOMESERVER_DOMAIN", #} ## How long outgoing webhook requests time out after # OUTGOING_WEBHOOK_TIMEOUT_SECONDS = 10 ## Mobile push notifications require registering for the Zulip Mobile ## Push Notification Service and configuring your server to use the ## service here. For complete documentation, see: ## ## https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html ## ZULIP_SERVICE_PUSH_NOTIFICATIONS = True ## By default, a Zulip server that has registered for Zulip services ## submits both basic metadata (required for billing and for determining ## free plan eligibility), as well as aggregate usage statistics. You ## can disable submitting usage statistics here. ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = False ## Whether to lightly advertise sponsoring Zulip in the gear menu. PROMOTE_SPONSORING_ZULIP = False ## Controls whether session cookies expire when the browser closes SESSION_EXPIRE_AT_BROWSER_CLOSE = False ## Session cookie expiry in seconds after the last page load SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # 2 weeks ## Password strength requirements; learn about configuration at ## https://zulip.readthedocs.io/en/latest/production/security-model.html. # PASSWORD_MIN_LENGTH = 6 # PASSWORD_MAX_LENGTH = 100 # PASSWORD_MIN_GUESSES = 10000 ## Controls whether Zulip sends "new login" email notifications. SEND_LOGIN_EMAILS = True ## Controls whether or not Zulip will parse links starting with ## "file:///" as a hyperlink (useful if you have e.g. an NFS share). ENABLE_FILE_LINKS = False ## By default, files uploaded by users and profile pictures are stored ## directly on the Zulip server. You can configure files being instead ## stored in Amazon S3 or another scalable data store here. See docs at: ## ## https://zulip.readthedocs.io/en/latest/production/upload-backends.html ## ## If you change LOCAL_UPLOADS_DIR to a different path, you will also ## need to manually edit Zulip's nginx configuration to use the new ## path. For that reason, we recommend replacing /home/zulip/uploads ## with a symlink instead of changing LOCAL_UPLOADS_DIR. LOCAL_UPLOADS_DIR = "/home/zulip/uploads" #S3_AUTH_UPLOADS_BUCKET = "zulip-uploads" #S3_AVATAR_BUCKET = "zulip-avatars" #S3_EXPORT_BUCKET = "zulip-export" #S3_REGION = "" #S3_ENDPOINT_URL = "" #S3_AVATAR_PUBLIC_URL_PREFIX = None #S3_ADDRESSING_STYLE = "auto" #S3_SKIP_PROXY = True #S3_UPLOADS_STORAGE_CLASS = "STANDARD" #S3_SKIP_CHECKSUM = False ## Maximum allowed size of uploaded files, in megabytes. Set ## MAX_FILE_UPLOAD_SIZE to 0 to disable file uploads completely ## (including hiding upload-related options from UI). MAX_FILE_UPLOAD_SIZE = 100 ## Controls whether name changes are completely disabled for this ## installation. This is useful when you're syncing names from an ## integrated LDAP/Active Directory. NAME_CHANGES_DISABLED = True ## Controls whether avatar changes are completely disabled for this ## installation. This is useful when you're syncing avatars from an ## integrated LDAP/Active Directory. AVATAR_CHANGES_DISABLED = True ## Controls whether users who have not uploaded an avatar will receive an avatar ## from gravatar.com. # ENABLE_GRAVATAR = True ## To override the default avatar image if ENABLE_GRAVATAR is False, place your ## custom default avatar image at /home/zulip/local-static/default-avatar.png ## and uncomment the following line. # DEFAULT_AVATAR_URI = "/local-static/default-avatar.png" ## The default CAMO_URI of "/external_content/" is served by the camo ## setup in the default Zulip nginx configuration. Setting CAMO_URI ## to "" will disable the Camo integration. CAMO_URI = "/external_content/" ## Controls various features explaining Zulip to new users. Disabling ## this is only recommended for installations that are using a limited ## subset of the Zulip UI, such as embedding it in a larger app. TUTORIAL_ENABLED = True ## Controls whether Zulip will rate-limit user requests. RATE_LIMITING = True ## Entries in this dictionary will override Zulip's default rate ## limits. Rules which are not explicitly overridden here ## will be as default. View the current rules using: ## /home/zulip/deployments/current/scripts/get-django-setting RATE_LIMITING_RULES ## ## The limits are tuples of a number of seconds and a number of ## requests allowed over that many seconds. If multiple tuples are ## given in a rule, a request breaching any of them will trigger a ## rate-limited response to the client. For example, to change the ## limits for total API requests by each user to be at most 100 ## requests per minute, and at most 200 requests per hour, add: ## "api_by_user": [(60, 100), (3600, 200)], # RATE_LIMITING_RULES = { # "api_by_ip": [ # (60, 100), # ], # } ## Fetch TOR exit node list every hour, and group all TOR exit nodes ## together into one bucket when applying rate-limiting. # RATE_LIMIT_TOR_TOGETHER = False ## Configuration for Terms of Service and Privacy Policy for the ## server. If unset, Zulip will never prompt users to accept Terms of ## Service. Users will be prompted to accept the terms during account ## registration, and during login if this value has changed. # TERMS_OF_SERVICE_VERSION = "1.0" ## Directory containing Markdown files for the server's policies. # POLICIES_DIRECTORY = "/etc/zulip/policies/" ## URL of the navigation tour video to show to new users. You can use this ## to host the official video on your network, or to provide your own ## introductory video with details on how your organization uses Zulip. ## ## A value of None disables the navigation tour video experience. # NAVIGATION_TOUR_VIDEO_URL = "https://static.zulipchat.com/static/navigation-tour-video/zulip-10.mp4" EOFMT cat < $HSHQ_STACKS_DIR/zulip/config/zulip-secrets.conf [secrets] avatar_salt = $(pwgen -n 64 1) rabbitmq_password = $ZULIP_RABBITMQ_PASSWORD shared_secret = $(pwgen -n 64 1) secret_key = $(pwgen -n 32 1) camo_key = $(pwgen -c -n 64 1) memcached_password = $ZULIP_MEMCACHE_PASSWORD redis_password = $ZULIP_REDIS_PASSWORD zulip_org_key = $(pwgen -c -n 64 1) zulip_org_id = $(uuidgen) email_password = postgres_password = $ZULIP_DATABASE_USER_PASSWORD auth_ldap_bind_password = $LDAP_READONLY_USER_PASSWORD EOFMT cat < $HSHQ_STACKS_DIR/zulip/newentry.sh #!/bin/bash update-ca-certificates /usr/sbin/entrypoint.sh "\$@" EOFMT chmod 755 $HSHQ_STACKS_DIR/zulip/newentry.sh cat < $HSHQ_STACKS_DIR/zulip/config/zulip.conf [machine] puppet_classes = zulip::profile::docker deploy_type = production [postgresql] version = 14 [application_server] http_only = true [loadbalancer] ips = $DOCKER_NETWORK_RESERVED_RANGE EOFMT cat < $HSHQ_STACKS_DIR/zulip/config/initRealm.sh #!/bin/bash /home/zulip/deployments/current/manage.py create_realm --password $ZULIP_ADMIN_PASSWORD '$HOMESERVER_NAME' '$ZULIP_ADMIN_EMAIL_ADDRESS' 'Zulip $(getAdminEmailName)' EOFMT chmod 755 $HSHQ_STACKS_DIR/zulip/config/initRealm.sh } function performUpdateZulip() { perform_stack_name=zulip prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/zulip/zulip-postgresql:14,mirror.gcr.io/zulip/docker-zulip:11.2-0,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/rabbitmq:4.1.4,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/zulip/zulip-postgresql:14,mirror.gcr.io/zulip/zulip-postgresql:14" image_update_map[1]="mirror.gcr.io/zulip/docker-zulip:11.2-0,mirror.gcr.io/zulip/docker-zulip:11.4-0" image_update_map[2]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[3]="mirror.gcr.io/rabbitmq:4.1.4,mirror.gcr.io/rabbitmq:4.2.1" image_update_map[4]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 2) newVer=v2 curImageList=mirror.gcr.io/zulip/zulip-postgresql:14,mirror.gcr.io/zulip/docker-zulip:11.4-0,mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/rabbitmq:4.2.1,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/zulip/zulip-postgresql:14,mirror.gcr.io/zulip/zulip-postgresql:14" image_update_map[1]="mirror.gcr.io/zulip/docker-zulip:11.4-0,mirror.gcr.io/zulip/docker-zulip:11.4-0" image_update_map[2]="mirror.gcr.io/memcached:1.6.39-alpine,mirror.gcr.io/memcached:1.6.39-alpine" image_update_map[3]="mirror.gcr.io/rabbitmq:4.2.1,mirror.gcr.io/rabbitmq:4.2.1" image_update_map[4]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # KillBill function installKillBill() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory killbill "KillBill" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName killbill-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName killbill-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName killbill-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName killbill-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/killbill mkdir $HSHQ_STACKS_DIR/killbill/db mkdir $HSHQ_STACKS_DIR/killbill/dbexport chmod 777 $HSHQ_STACKS_DIR/killbill/dbexport initServicesCredentials set +e addUserMailu alias $KILLBILL_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS KILLBILL_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $KILLBILL_ADMIN_PASSWORD | tr -d ':\n') outputConfigKillBill installStack killbill killbill-app "" $HOME/killbill.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$KILLBILL_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_KILLBILL_WEB Admin Login Info" -b "$FMLNAME_KILLBILL_WEB Admin Username: $KILLBILL_ADMIN_USERNAME\n$FMLNAME_KILLBILL_WEB Admin Email: $KILLBILL_ADMIN_EMAIL_ADDRESS\n$FMLNAME_KILLBILL_WEB Admin Password: $KILLBILL_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" KILLBILL_INIT_ENV=true updateConfigVar KILLBILL_INIT_ENV $KILLBILL_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase KillBill mysql killbill-db $KILLBILL_KB_DATABASE_NAME root $KILLBILL_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/killbill/dbexport $KILLBILL_KB_DATABASE_READONLYUSER $KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase KillBill mysql killbill-db $KILLBILL_KAUI_DATABASE_NAME root $KILLBILL_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/killbill/dbexport $KILLBILL_KAUI_DATABASE_READONLYUSER $KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_KILLBILL_API.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://killbill-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KILLBILL_API $MANAGETLS_KILLBILL_API "$is_integrate_hshq" $NETDEFAULT_KILLBILL_API "$inner_block" insertSubAuthelia $SUB_KILLBILL_API.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_KILLBILL_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://killbill-web:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KILLBILL_WEB $MANAGETLS_KILLBILL_WEB "$is_integrate_hshq" $NETDEFAULT_KILLBILL_WEB "$inner_block" insertSubAuthelia $SUB_KILLBILL_WEB.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll killbill "$FMLNAME_KILLBILL_WEB" $USERTYPE_KILLBILL_WEB "https://$SUB_KILLBILL_WEB.$HOMESERVER_DOMAIN" "killbill.png" "$(getHeimdallOrderFromSub $SUB_KILLBILL_WEB $USERTYPE_KILLBILL_WEB)" restartAllCaddyContainers checkAddDBConnection true killbill-api "$FMLNAME_KILLBILL_API" mysql killbill-db $KILLBILL_KB_DATABASE_NAME $KILLBILL_DATABASE_USER $KILLBILL_DATABASE_USER_PASSWORD checkAddDBConnection true killbill-web "$FMLNAME_KILLBILL_WEB" mysql killbill-db $KILLBILL_KAUI_DATABASE_NAME $KILLBILL_DATABASE_USER $KILLBILL_DATABASE_USER_PASSWORD fi } function outputConfigKillBill() { cat < $HOME/killbill-compose.yml $STACK_VERSION_PREFIX killbill $(getScriptStackVersion killbill) services: killbill-db: image: $(getScriptImageByContainerName killbill-db) container_name: killbill-db hostname: killbill-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-killbill-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-killbill-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/killbill/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.killbill-hourly-db.schedule=@every 1h" - "ofelia.job-exec.killbill-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.killbill-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.killbill-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.killbill-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.killbill-hourly-db.email-from=KillBill Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.killbill-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.killbill-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.killbill-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.killbill-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.killbill-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.killbill-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.killbill-monthly-db.email-from=KillBill Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.killbill-monthly-db.mail-only-on-error=false" killbill-app: image: $(getScriptImageByContainerName killbill-app) container_name: killbill-app hostname: killbill-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - killbill-db networks: - int-killbill-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro killbill-web: image: $(getScriptImageByContainerName killbill-web) container_name: killbill-web hostname: killbill-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - killbill-app - killbill-db networks: - int-killbill-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro killbill-redis: image: $(getScriptImageByContainerName killbill-redis) container_name: killbill-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $KILLBILL_REDIS_PASSWORD --appendonly no networks: - int-killbill-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-killbill-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/killbill/db networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-killbill-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/killbill.env TZ=\${PORTAINER_TZ} MYSQL_ROOT_PASSWORD=$KILLBILL_DATABASE_USER_PASSWORD KILLBILL_DAO_URL=jdbc:mysql://killbill-db:3306/$KILLBILL_KB_DATABASE_NAME KILLBILL_DAO_USER=$KILLBILL_DATABASE_USER KILLBILL_DAO_PASSWORD=$KILLBILL_DATABASE_USER_PASSWORD KILLBILL_METRICS_INFLUXDB=false KILLBILL_METRICS_INFLUXDB_HOST=https://$SUB_INFLUXDB.$HOMESERVER_DOMAIN KILLBILL_METRICS_INFLUXDB_PORT=8086 KAUI_CONFIG_DAO_URL=jdbc:mysql://killbill-db:3306/$KILLBILL_KAUI_DATABASE_NAME KAUI_CONFIG_DAO_USER=$KILLBILL_DATABASE_USER KAUI_CONFIG_DAO_PASSWORD=$KILLBILL_DATABASE_USER_PASSWORD KAUI_KILLBILL_URL=http://killbill-app:8080 KAUI_KILLBILL_API_KEY=$KILLBILL_API_KEY KAUI_KILLBILL_API_SECRET=$KILLBILL_API_SECRET EOFMT } function performUpdateKillBill() { perform_stack_name=killbill prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/killbill/mariadb:0.24,mirror.gcr.io/killbill/killbill:0.24.15,mirror.gcr.io/killbill/kaui:3.0.23,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/killbill/mariadb:0.24,mirror.gcr.io/killbill/mariadb:0.24" image_update_map[1]="mirror.gcr.io/killbill/killbill:0.24.15,mirror.gcr.io/killbill/killbill:0.24.15" image_update_map[2]="mirror.gcr.io/killbill/kaui:3.0.23,mirror.gcr.io/killbill/kaui:3.0.23" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # InvoiceShelf function installInvoiceShelf() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory invoiceshelf "InvoiceShelf" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceshelf-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceshelf-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceshelf-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/invoiceshelf mkdir $HSHQ_STACKS_DIR/invoiceshelf/db mkdir $HSHQ_STACKS_DIR/invoiceshelf/dbimport mkdir $HSHQ_STACKS_DIR/invoiceshelf/dbexport mkdir $HSHQ_STACKS_DIR/invoiceshelf/storage mkdir $HSHQ_STACKS_DIR/invoiceshelf/modules chmod 775 $HSHQ_STACKS_DIR/invoiceshelf/storage chmod 775 $HSHQ_STACKS_DIR/invoiceshelf/modules sudo chown 82:82 $HSHQ_STACKS_DIR/invoiceshelf/storage sudo chown 82:82 $HSHQ_STACKS_DIR/invoiceshelf/modules chmod 777 $HSHQ_STACKS_DIR/invoiceshelf/dbexport initServicesCredentials set +e addUserMailu alias $INVOICESHELF_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS INVOICESHELF_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $INVOICESHELF_ADMIN_PASSWORD | tr -d ':\n') outputConfigInvoiceShelf installStack invoiceshelf invoiceshelf-app "ready to handle connections" $HOME/invoiceshelf.env 3 retVal=$? if [ $retVal -ne 0 ]; then echo "There was a problem with the InvoiceShelf installation process, returning..." return $retVal fi INVOICESHELF_INIT_ENV=true updateConfigVar INVOICESHELF_INIT_ENV $INVOICESHELF_INIT_ENV sleep 3 docker exec invoiceshelf-app bash -c "chmod -R 775 /var/www/html/storage;chmod -R 775 /var/www/html/bootstrap" performOnboardingInvoiceShelf retVal=$? if [ $retVal -ne 0 ]; then echo "There was a problem with the InvoiceShelf onboarding process, returning..." return $retVal fi waitForContainerLogString invoiceshelf-app 3 60 "PHP-FPM is running correctly" sleep 10 addReadOnlyUserToDatabase InvoiceShelf postgres invoiceshelf-db $INVOICESHELF_DATABASE_NAME $INVOICESHELF_DATABASE_USER $INVOICESHELF_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/invoiceshelf/dbexport $INVOICESHELF_DATABASE_READONLYUSER $INVOICESHELF_DATABASE_READONLYUSER_PASSWORD INVOICESHELF_ADMIN_API_KEY=$(generateAPITokenInvoiceShelf "$INVOICESHELF_ADMIN_EMAIL_ADDRESS" "$INVOICESHELF_ADMIN_PASSWORD") updateConfigVar INVOICESHELF_ADMIN_API_KEY "$INVOICESHELF_ADMIN_API_KEY" set -e inner_block="" inner_block=$inner_block">>https://$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://invoiceshelf-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_INVOICESHELF_APP $MANAGETLS_INVOICESHELF_APP "$is_integrate_hshq" $NETDEFAULT_INVOICESHELF_APP "$inner_block" insertSubAuthelia $SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll invoiceshelf "$FMLNAME_INVOICESHELF_APP" $USERTYPE_INVOICESHELF_APP "https://$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN" "invoiceshelf.png" "$(getHeimdallOrderFromSub $SUB_INVOICESHELF_APP $USERTYPE_INVOICESHELF_APP)" restartAllCaddyContainers checkAddDBConnection true invoiceshelf "$FMLNAME_INVOICESHELF_APP" postgres invoiceshelf-db $INVOICESHELF_DATABASE_NAME $INVOICESHELF_DATABASE_USER $INVOICESHELF_DATABASE_USER_PASSWORD fi } function outputConfigInvoiceShelf() { cat < $HOME/invoiceshelf-compose.yml $STACK_VERSION_PREFIX invoiceshelf $(getScriptStackVersion invoiceshelf) services: invoiceshelf-db: image: $(getScriptImageByContainerName invoiceshelf-db) container_name: invoiceshelf-db hostname: invoiceshelf-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-invoiceshelf-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/dbexport:/dbexport - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/dbimport:/dbimport labels: - "ofelia.enabled=true" - "ofelia.job-exec.invoiceshelf-hourly-db.schedule=@every 1h" - "ofelia.job-exec.invoiceshelf-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceshelf-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceshelf-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceshelf-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceshelf-hourly-db.email-from=InvoiceShelf Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceshelf-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.invoiceshelf-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.invoiceshelf-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceshelf-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceshelf-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceshelf-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceshelf-monthly-db.email-from=InvoiceShelf Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceshelf-monthly-db.mail-only-on-error=false" invoiceshelf-app: image: $(getScriptImageByContainerName invoiceshelf-app) container_name: invoiceshelf-app hostname: invoiceshelf-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - invoiceshelf-db networks: - int-invoiceshelf-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-invoiceshelf-storage:/var/www/html/storage - v-invoiceshelf-modules:/var/www/html/Modules invoiceshelf-mcp: image: $(getScriptImageByContainerName invoiceshelf-mcp) container_name: invoiceshelf-mcp hostname: invoiceshelf-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-invoiceshelf-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-invoiceshelf-storage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/storage v-invoiceshelf-modules: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/modules networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true dock-dbs-net: name: dock-dbs external: true int-invoiceshelf-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/invoiceshelf.env TZ=\${PORTAINER_TZ} PHP_TZ=\${PORTAINER_TZ} TIMEZONE=\${PORTAINER_TZ} POSTGRES_DB=$INVOICESHELF_DATABASE_NAME POSTGRES_USER=$INVOICESHELF_DATABASE_USER POSTGRES_PASSWORD=$INVOICESHELF_DATABASE_USER_PASSWORD APP_NAME=InvoiceShelf APP_ENV=production APP_DEBUG=true APP_URL=https://$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN DB_CONNECTION=pgsql DB_HOST=invoiceshelf-db DB_PORT=5432 DB_DATABASE=$INVOICESHELF_DATABASE_NAME DB_USERNAME=$INVOICESHELF_DATABASE_USER DB_PASSWORD=$INVOICESHELF_DATABASE_USER_PASSWORD CACHE_STORE=file SESSION_DRIVER=file SESSION_DOMAIN=$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN SANCTUM_STATEFUL_DOMAINS=$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN TRUSTED_PROXIES=$DOCKER_NETWORK_RESERVED_RANGE INVOICESHELF_BASE_URL=http://invoiceshelf-app:8080 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false INVOICESHELF_PUBLIC_URL=$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN EOFMT cat < $HSHQ_STACKS_DIR/invoiceshelf/dbimport/addreadonly.sql DO \$do\$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'readaccess') THEN CREATE ROLE readaccess LOGIN PASSWORD '$INVOICESHELF_DATABASE_READONLYUSER_PASSWORD'; ELSE RAISE NOTICE 'Role "readaccess" already exists. Skipping.'; END IF; END \$do\$; GRANT CONNECT ON DATABASE $INVOICESHELF_DATABASE_NAME TO readaccess; GRANT USAGE ON SCHEMA public TO readaccess; GRANT SELECT ON ALL TABLES IN SCHEMA public TO readaccess; CREATE USER "$INVOICESHELF_DATABASE_READONLYUSER" WITH PASSWORD '$INVOICESHELF_DATABASE_READONLYUSER_PASSWORD'; GRANT readaccess TO "$INVOICESHELF_DATABASE_READONLYUSER"; EOFPC } function performUpdateInvoiceShelf() { perform_stack_name=invoiceshelf prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/invoiceshelf/invoiceshelf:2.2.0-alpha2 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/invoiceshelf/invoiceshelf:2.2.0-alpha2,mirror.gcr.io/invoiceshelf/invoiceshelf:2.4.2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfInvoiceShelfV2AddMCP perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/invoiceshelf/invoiceshelf:2.4.2,ghcr.io/homeserverhq/invoiceshelf-mcp:v2 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/invoiceshelf/invoiceshelf:2.4.2,mirror.gcr.io/invoiceshelf/invoiceshelf:2.4.2" image_update_map[2]="ghcr.io/homeserverhq/invoiceshelf-mcp:v2,ghcr.io/homeserverhq/invoiceshelf-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function performOnboardingInvoiceShelf() { echo "==> Fresh migrate + seed (wipes existing DB) on invoiceshelf-app" if ! docker exec "invoiceshelf-app" php artisan migrate:fresh --seed --force --no-interaction > /dev/null 2>&1; then echo "ERROR: migrate:fresh --seed failed." >&2 return 1 fi echo "==> Applying onboarding answers" if ! docker exec -e ADMIN_NAME="InvoiceShelf $(getAdminEmailName)" \ -e ADMIN_EMAIL="${INVOICESHELF_ADMIN_EMAIL_ADDRESS}" \ -e ADMIN_PASSWORD="${INVOICESHELF_ADMIN_PASSWORD}" \ -e COMPANY_NAME="${HOMESERVER_NAME}" \ -e CURRENCY_INPUT="${HOMESERVER_CURRENCY_CODE}" \ -e TIME_ZONE="${TZ}" \ -e MAIL_DRIVER="smtp" \ -e MAIL_HOST="${SMTP_HOSTNAME}" \ -e MAIL_PORT="${SMTP_HOSTPORT}" \ -e MAIL_USERNAME="" \ -e MAIL_PASSWORD="" \ -e MAIL_ENCRYPTION="starttls" \ -e MAIL_FROM_EMAIL="${EMAIL_ADMIN_EMAIL_ADDRESS}" \ -e MAIL_FROM_NAME="InvoiceShelf $(getAdminEmailName)" \ "invoiceshelf-app" php artisan tinker --execute "$(cat <<'PHP' use App\Models\User; use App\Models\Company; use App\Models\Currency; use App\Models\CompanySetting; use App\Models\Setting; use App\Space\InstallUtils; if (!$adminEmail = getenv('ADMIN_EMAIL')) { throw new RuntimeException('ADMIN_EMAIL missing'); } if (!$adminPassword = getenv('ADMIN_PASSWORD')) { throw new RuntimeException('ADMIN_PASSWORD missing'); } if (!$companyName = getenv('COMPANY_NAME')) { throw new RuntimeException('COMPANY_NAME missing'); } $user = User::where('role', 'super admin')->orderBy('id')->first(); if (!$user) { $user = User::whereNotNull('id')->orderBy('id')->first(); } if (!$user) { throw new RuntimeException('No super admin user found after seeding.'); } $user->name = getenv('ADMIN_NAME') ?: $user->name; $user->email = $adminEmail; $user->password = $adminPassword; $user->save(); $user = $user->fresh(); $company = $user->companies()->orderBy('id')->first(); if (!$company) { $company = Company::orderBy('id')->first(); } if (!$company) { throw new RuntimeException('No company found after seeding.'); } $company->name = $companyName; $company->slug = \Illuminate\Support\Str::slug($companyName); $company->save(); $currencyInput = getenv('CURRENCY_INPUT') ?: ''; $currencyId = null; if (is_numeric($currencyInput)) { $currencyId = (int) $currencyInput; } else { $currency = Currency::where('code', strtoupper($currencyInput))->first(); if ($currency) { $currencyId = $currency->id; } } if (!$currencyId) { $currency = Currency::where('code', 'USD')->first(); $currencyId = $currency ? $currency->id : Currency::orderBy('id')->value('id'); } $timeZone = getenv('TIME_ZONE') ?: 'America/Chicago'; CompanySetting::setSettings([ 'currency' => $currencyId, 'time_zone' => $timeZone, ], $company->id); $mailSettings = [ 'mail_driver' => getenv('MAIL_DRIVER') ?: 'smtp', 'from_name' => getenv('MAIL_FROM_NAME') ?: '', 'from_mail' => getenv('MAIL_FROM_EMAIL') ?: '', ]; switch ($mailSettings['mail_driver']) { case 'smtp': $mailSettings['mail_host'] = getenv('MAIL_HOST') ?: ''; $mailSettings['mail_port'] = getenv('MAIL_PORT') ?: ''; $mailSettings['mail_username'] = getenv('MAIL_USERNAME') ?: ''; $mailSettings['mail_password'] = getenv('MAIL_PASSWORD') ?: ''; $mailSettings['mail_encryption'] = getenv('MAIL_ENCRYPTION') ?: 'none'; break; case 'sendmail': $mailSettings['mail_sendmail_path'] = getenv('MAIL_SENDMAIL_PATH') ?: '/usr/sbin/sendmail -bs -i'; break; } Setting::setSettings($mailSettings); Setting::setSetting('profile_complete', 'COMPLETED'); Setting::setSetting('profile_language', 'en'); InstallUtils::setCurrentVersion(); echo 'profile_complete='.Setting::getSetting('profile_complete'); echo ' | language='.Setting::getSetting('profile_language'); echo ' | admin='.$user->email; echo ' | company='.$company->name; echo ' | currency='.Currency::find($currencyId)?->code; echo ' | timezone='.CompanySetting::getSetting('time_zone', $company->id); echo ' | mail_driver='.Setting::getSetting('mail_driver'); echo PHP_EOL; PHP )" > /dev/null 2>&1; then echo "ERROR: onboarding could not be applied." >&2 return 1 fi echo "==> Clearing config cache (pick up mail/company settings)" docker exec "invoiceshelf-app" php artisan config:clear >/dev/null 2>&1 || true echo "==> Onboarding complete. Log in with: ${INVOICESHELF_ADMIN_EMAIL_ADDRESS}" } function generateAPITokenInvoiceShelf() { is_username="$1" is_password="$2" num_tries=0 max_tries=5 is_invoiceshelf_token=false while [ "$is_invoiceshelf_token" = "false" ] && [ $num_tries -lt $max_tries ] do ivshelf_resp=$(docker exec -e U="${is_username}" \ -e PW="${is_password}" invoiceshelf-app \ sh -c 'curl -s -X POST "http://127.0.0.1:8080/api/v1/auth/login" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d "{\"username\":\"$U\",\"password\":\"$PW\",\"device_name\":\"MCP\"}"') ivshelf_token=$(echo "$ivshelf_resp" | jq -r .token) if ! [ -z "$ivshelf_token" ]; then break fi ((num_tries++)) echo "Failed to get InvoiceShelf API token ($num_tries of $max_tries), retrying in 5 seconds..." >&2 sleep 5 done echo "${ivshelf_token}" } function mfInvoiceShelfV2AddMCP() { cat < $HOME/invoiceshelf-compose.yml $STACK_VERSION_PREFIX invoiceshelf $(getScriptStackVersion invoiceshelf) services: invoiceshelf-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: invoiceshelf-db hostname: invoiceshelf-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-invoiceshelf-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/dbexport:/dbexport - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/dbimport:/dbimport labels: - "ofelia.enabled=true" - "ofelia.job-exec.invoiceshelf-hourly-db.schedule=@every 1h" - "ofelia.job-exec.invoiceshelf-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceshelf-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceshelf-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceshelf-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceshelf-hourly-db.email-from=InvoiceShelf Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceshelf-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.invoiceshelf-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.invoiceshelf-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceshelf-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceshelf-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceshelf-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceshelf-monthly-db.email-from=InvoiceShelf Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceshelf-monthly-db.mail-only-on-error=false" invoiceshelf-app: image: mirror.gcr.io/invoiceshelf/invoiceshelf:2.4.2 container_name: invoiceshelf-app hostname: invoiceshelf-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - invoiceshelf-db networks: - int-invoiceshelf-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-invoiceshelf-storage:/var/www/html/storage - v-invoiceshelf-modules:/var/www/html/Modules invoiceshelf-mcp: image: ghcr.io/homeserverhq/invoiceshelf-mcp:v2 container_name: invoiceshelf-mcp hostname: invoiceshelf-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always networks: - int-invoiceshelf-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-invoiceshelf-storage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/storage v-invoiceshelf-modules: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceshelf/modules networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-invoiceshelf-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/invoiceshelf.env TZ=\${PORTAINER_TZ} PHP_TZ=\${PORTAINER_TZ} TIMEZONE=\${PORTAINER_TZ} POSTGRES_DB=$INVOICESHELF_DATABASE_NAME POSTGRES_USER=$INVOICESHELF_DATABASE_USER POSTGRES_PASSWORD=$INVOICESHELF_DATABASE_USER_PASSWORD APP_NAME=InvoiceShelf APP_ENV=production APP_DEBUG=true APP_URL=https://$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN DB_CONNECTION=pgsql DB_HOST=invoiceshelf-db DB_PORT=5432 DB_DATABASE=$INVOICESHELF_DATABASE_NAME DB_USERNAME=$INVOICESHELF_DATABASE_USER DB_PASSWORD=$INVOICESHELF_DATABASE_USER_PASSWORD CACHE_STORE=file SESSION_DRIVER=file SESSION_DOMAIN=$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN SANCTUM_STATEFUL_DOMAINS=$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN TRUSTED_PROXIES=$DOCKER_NETWORK_RESERVED_RANGE INVOICESHELF_BASE_URL=http://invoiceshelf-app:8080 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false INVOICESHELF_PUBLIC_URL=$SUB_INVOICESHELF_APP.$HOMESERVER_DOMAIN EOFMT } # InvoiceNinja function installInvoiceNinja() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory invoiceninja "InvoiceNinja" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceninja-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceninja-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceninja-web) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceninja-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invoiceninja-minio) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/invoiceninja mkdir $HSHQ_STACKS_DIR/invoiceninja/config mkdir $HSHQ_STACKS_DIR/invoiceninja/db mkdir $HSHQ_STACKS_DIR/invoiceninja/dbexport mkdir $HSHQ_STACKS_DIR/invoiceninja/nginx mkdir $HSHQ_STACKS_DIR/invoiceninja/public mkdir $HSHQ_STACKS_DIR/invoiceninja/minio mkdir $HSHQ_STACKS_DIR/invoiceninja/storage mkdir $HSHQ_STACKS_DIR/invoiceninja/storage/logs touch $HSHQ_STACKS_DIR/invoiceninja/storage/logs/invoiceninja.log sudo chown -R 33:33 $HSHQ_STACKS_DIR/invoiceninja/storage chmod 777 $HSHQ_STACKS_DIR/invoiceninja/dbexport initServicesCredentials set +e addUserMailu alias $INVOICENINJA_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS INVOICENINJA_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $INVOICENINJA_ADMIN_PASSWORD | tr -d ':\n') outputConfigInvoiceNinja installStack invoiceninja invoiceninja-app "ready to handle connections" $HOME/invoiceninja.env 15 900 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$INVOICENINJA_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_INVOICENINJA_WEB Admin Login Info" -b "$FMLNAME_INVOICENINJA_WEB Admin Username: $INVOICENINJA_ADMIN_EMAIL_ADDRESS\n$FMLNAME_INVOICENINJA_WEB Admin Password: $INVOICENINJA_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" INVOICENINJA_INIT_ENV=true updateConfigVar INVOICENINJA_INIT_ENV $INVOICENINJA_INIT_ENV fi sleep 3 docker exec invoiceninja-app bash -c "php artisan optimize;php artisan config:clear;php artisan optimize:clear;php artisan cache:clear" > /dev/null 2>&1 addReadOnlyUserToDatabase InvoiceNinja mysql invoiceninja-db $INVOICENINJA_DATABASE_NAME root $INVOICENINJA_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/invoiceninja/dbexport $INVOICENINJA_DATABASE_READONLYUSER $INVOICENINJA_DATABASE_READONLYUSER_PASSWORD set -e inner_block="" inner_block=$inner_block">>https://$SUB_INVOICENINJA_WEB.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://invoiceninja-web {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_INVOICENINJA_WEB $MANAGETLS_INVOICENINJA_WEB "$is_integrate_hshq" $NETDEFAULT_INVOICENINJA_WEB "$inner_block" insertSubAuthelia $SUB_INVOICENINJA_WEB.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll invoiceninja "$FMLNAME_INVOICENINJA_WEB" $USERTYPE_INVOICENINJA_WEB "https://$SUB_INVOICENINJA_WEB.$HOMESERVER_DOMAIN" "invoiceninja.png" "$(getHeimdallOrderFromSub $SUB_INVOICENINJA_WEB $USERTYPE_INVOICENINJA_WEB)" restartAllCaddyContainers checkAddDBConnection true invoiceninja "$FMLNAME_INVOICENINJA_WEB" mysql invoiceninja-db $INVOICENINJA_DATABASE_NAME $INVOICENINJA_DATABASE_USER $INVOICENINJA_DATABASE_USER_PASSWORD fi } function outputConfigInvoiceNinja() { cat < $HOME/invoiceninja-compose.yml $STACK_VERSION_PREFIX invoiceninja $(getScriptStackVersion invoiceninja) services: invoiceninja-db: image: $(getScriptImageByContainerName invoiceninja-db) container_name: invoiceninja-db hostname: invoiceninja-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-invoiceninja-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-invoiceninja-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.invoiceninja-hourly-db.schedule=@every 1h" - "ofelia.job-exec.invoiceninja-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceninja-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceninja-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceninja-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceninja-hourly-db.email-from=InvoiceNinja Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceninja-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.invoiceninja-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.invoiceninja-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceninja-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceninja-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceninja-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceninja-monthly-db.email-from=InvoiceNinja Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceninja-monthly-db.mail-only-on-error=false" invoiceninja-app: image: $(getScriptImageByContainerName invoiceninja-app) container_name: invoiceninja-app hostname: invoiceninja-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - invoiceninja-db networks: - int-invoiceninja-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/env:/var/www/html/.env #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/php.ini:/usr/local/etc/php/conf.d/invoiceninja.ini:ro #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/php-fpm.conf:/usr/local/etc/php-fpm.d/invoiceninja.conf:ro #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf:ro - v-invoiceninja-public:/var/www/html/public - v-invoiceninja-storage:/var/www/html/storage invoiceninja-web: image: $(getScriptImageByContainerName invoiceninja-web) container_name: invoiceninja-web hostname: invoiceninja-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - invoiceninja-db networks: - int-invoiceninja-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/nginx:/etc/nginx/conf.d:ro - v-invoiceninja-public:/var/www/html/public - v-invoiceninja-storage:/var/www/html/storage invoiceninja-redis: image: $(getScriptImageByContainerName invoiceninja-redis) container_name: invoiceninja-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $INVOICENINJA_REDIS_PASSWORD --appendonly no networks: - int-invoiceninja-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro # To enable, set FILESYSTEM_DRIVER=s3 in environment. However, # this feature is inop at the moment. # # invoiceninja-minio: # image: $(getScriptImageByContainerName invoiceninja-minio) # container_name: invoiceninja-minio # hostname: invoiceninja-minio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # command: server /data # networks: # - dock-proxy-net # - int-invoiceninja-net # entrypoint: > # /bin/sh -c " # minio server /data --address ':9000' --console-address ':9001' & # MINIO_PID=\\\$! # while ! curl -s http://localhost:9000/minio/health/live; do # echo 'Waiting for MinIO to start...' # sleep 1 # done # sleep 5 # /usr/bin/mc alias set minio http://invoiceninja-minio:9000 ${INVOICENINJA_MINIO_KEY} ${INVOICENINJA_MINIO_SECRET} # echo 'Creating bucket invoiceninja' # /usr/bin/mc mb minio/invoiceninja # wait \\\$MINIO_PID # " # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/minio:/data volumes: v-invoiceninja-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/db v-invoiceninja-public: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/public v-invoiceninja-storage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/storage networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-invoiceninja-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/invoiceninja.env TZ=$TZ MYSQL_DATABASE=$INVOICENINJA_DATABASE_NAME MYSQL_ROOT_PASSWORD=$INVOICENINJA_DATABASE_ROOT_PASSWORD MYSQL_USER=$INVOICENINJA_DATABASE_USER MYSQL_PASSWORD=$INVOICENINJA_DATABASE_USER_PASSWORD APP_NAME=Invoice Ninja APP_URL=https://$SUB_INVOICENINJA_WEB.$HOMESERVER_DOMAIN APP_KEY=base64:$(openssl rand -base64 32) APP_ENV=production APP_DEBUG=true REQUIRE_HTTPS=false EXPANDED_LOGGING=true DB_TYPE=mysql DB_CONNECTION=mysql DB_HOST=invoiceninja-db DB_DATABASE=$INVOICENINJA_DATABASE_NAME DB_USERNAME=$INVOICENINJA_DATABASE_USER DB_PASSWORD=$INVOICENINJA_DATABASE_USER_PASSWORD DB_ROOT_PASSWORD=$INVOICENINJA_DATABASE_ROOT_PASSWORD DB_PORT=3306 CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis REDIS_HOST=invoiceninja-redis REDIS_PASSWORD=$INVOICENINJA_REDIS_PASSWORD REDIS_PORT=6379 FILESYSTEM_DISK=debian_docker IN_USER_EMAIL=$INVOICENINJA_ADMIN_EMAIL_ADDRESS IN_PASSWORD=$INVOICENINJA_ADMIN_PASSWORD MAIL_MAILER=smtp MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=starttls MAIL_FROM_ADDRESS=$EMAIL_ADMIN_EMAIL_ADDRESS MAIL_FROM_NAME=Invoice Ninja $(getAdminEmailName) FILESYSTEM_DRIVER=local AWS_ACCESS_KEY_ID=$INVOICENINJA_MINIO_KEY AWS_SECRET_ACCESS_KEY=$INVOICENINJA_MINIO_SECRET AWS_DEFAULT_REGION=invoiceninja AWS_BUCKET=invoiceninja AWS_URL=http://invoiceninja-minio:9000 AWS_ENDPOINT=http://invoiceninja-minio:9000/ MINIO_ROOT_USER=$INVOICENINJA_MINIO_KEY MINIO_ROOT_PASSWORD=$INVOICENINJA_MINIO_SECRET MINIO_DOMAIN=invoiceninja MINIO_CONSOLE_ADDRESS=:9090 PHANTOMJS_PDF_GENERATION=false PDF_GENERATOR=snappdf TRUSTED_PROXIES=$DOCKER_NETWORK_RESERVED_RANGE IS_DOCKER=true SCOUT_DRIVER=null NINJA_ENVIRONMENT=selfhost WEBCRON_SECRET=$INVOICENINJA_WEBCRON_SECRET UPDATE_SECRET=$INVOICENINJA_UPDATE_SECRET EOFMT cp $HOME/invoiceninja.env $HSHQ_STACKS_DIR/invoiceninja/config/env sed -i "s/\"//g" $HOME/invoiceninja.env cat < $HSHQ_STACKS_DIR/invoiceninja/nginx/invoiceninja.conf # https://nginx.org/en/docs/http/ngx_http_core_module.html client_max_body_size 10M; client_body_buffer_size 10M; server_tokens off; # https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html fastcgi_buffers 32 16K; # https://nginx.org/en/docs/http/ngx_http_gzip_module.html gzip on; gzip_comp_level 2; gzip_min_length 1M; gzip_proxied any; gzip_types *; EOFMT cat < $HSHQ_STACKS_DIR/invoiceninja/nginx/laravel.conf # https://laravel.com/docs/master/deployment#nginx server { listen 80 default_server; server_name _; root /var/www/html/public; add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; index index.php; charset utf-8; location / { try_files \$uri \$uri/ /index.php?\$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location ~ \.php\$ { fastcgi_pass invoiceninja-app:9000; fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known).* { deny all; } } EOFMT } function performUpdateInvoiceNinja() { perform_stack_name=invoiceninja prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12,mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37" image_update_map[2]="mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[3]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-07-23T15-54-02Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[5]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" ;; 2) newVer=v3 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37,mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37" image_update_map[2]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[3]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[5]="mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z,mirror.gcr.io/minio/mc:RELEASE.2025-08-13T08-35-41Z" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true fixV2ComposeInvoiceNinja perform_update_report="${perform_update_report}$stack_upgrade_report" startStopStack invoiceninja stop sleep 2 startStopStack invoiceninja start return ;; 3) newVer=v4 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37,mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37" image_update_map[2]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[3]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" ;; 4) newVer=v4 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37,mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/redis:8.4.0-bookworm,ghcr.io/homeserverhq/minio:v1.0 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37,mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37" image_update_map[2]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine" image_update_map[3]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" image_update_map[4]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function fixV2ComposeInvoiceNinja() { rm -f $HOME/invoiceninja-compose.yml cat < $HOME/invoiceninja-compose.yml $STACK_VERSION_PREFIX invoiceninja v3 services: invoiceninja-db: image: mirror.gcr.io/mariadb:10.7.3 container_name: invoiceninja-db hostname: invoiceninja-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-invoiceninja-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-invoiceninja-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.invoiceninja-hourly-db.schedule=@every 1h" - "ofelia.job-exec.invoiceninja-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceninja-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceninja-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceninja-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceninja-hourly-db.email-from=InvoiceNinja Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceninja-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.invoiceninja-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.invoiceninja-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.invoiceninja-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.invoiceninja-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.invoiceninja-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.invoiceninja-monthly-db.email-from=InvoiceNinja Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.invoiceninja-monthly-db.mail-only-on-error=false" invoiceninja-app: image: mirror.gcr.io/invoiceninja/invoiceninja-debian:5.12.37 container_name: invoiceninja-app hostname: invoiceninja-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - invoiceninja-db networks: - int-invoiceninja-net - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/env:/var/www/html/.env #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/php.ini:/usr/local/etc/php/conf.d/invoiceninja.ini:ro #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/php-fpm.conf:/usr/local/etc/php-fpm.d/invoiceninja.conf:ro #- \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/config/supervisord.conf:/etc/supervisor/conf.d/supervisord.conf:ro - v-invoiceninja-public:/var/www/html/public - v-invoiceninja-storage:/var/www/html/storage invoiceninja-web: image: mirror.gcr.io/nginx:1.29.3-alpine container_name: invoiceninja-web hostname: invoiceninja-web restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - invoiceninja-db networks: - int-invoiceninja-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/nginx:/etc/nginx/conf.d:ro - v-invoiceninja-public:/var/www/html/public - v-invoiceninja-storage:/var/www/html/storage invoiceninja-redis: image: mirror.gcr.io/redis:8.4.0-bookworm container_name: invoiceninja-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $INVOICENINJA_REDIS_PASSWORD --appendonly no networks: - int-invoiceninja-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro # To enable, set FILESYSTEM_DRIVER=s3 in environment. However, # this feature is inop at the moment. # # invoiceninja-minio: # image: mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z # container_name: invoiceninja-minio # hostname: invoiceninja-minio # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # command: server /data # networks: # - dock-proxy-net # - int-invoiceninja-net # entrypoint: > # /bin/sh -c " # minio server /data --address ':9000' --console-address ':9001' & # MINIO_PID=\\\$! # while ! curl -s http://localhost:9000/minio/health/live; do # echo 'Waiting for MinIO to start...' # sleep 1 # done # sleep 5 # /usr/bin/mc alias set minio http://invoiceninja-minio:9000 ${INVOICENINJA_MINIO_KEY} ${INVOICENINJA_MINIO_SECRET} # echo 'Creating bucket invoiceninja' # /usr/bin/mc mb minio/invoiceninja # wait \\\$MINIO_PID # " # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/minio:/data volumes: v-invoiceninja-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/db v-invoiceninja-public: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/public v-invoiceninja-storage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invoiceninja/storage networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-invoiceninja-net: driver: bridge internal: true ipam: driver: default EOFMT } # Dolibarr function installDolibarr() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory dolibarr "Dolibarr" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName dolibarr-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName dolibarr-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName dolibarr-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName dolibarr-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/dolibarr mkdir $HSHQ_STACKS_DIR/dolibarr/custom mkdir $HSHQ_STACKS_DIR/dolibarr/documents mkdir $HSHQ_STACKS_DIR/dolibarr/db mkdir $HSHQ_STACKS_DIR/dolibarr/dbexport mkdir $HSHQ_STACKS_DIR/dolibarr/mcp mkdir $HSHQ_STACKS_DIR/dolibarr/mcp/config mkdir $HSHQ_STACKS_DIR/dolibarr/mcp/plugins chmod 777 $HSHQ_STACKS_DIR/dolibarr/dbexport initServicesCredentials set +e addUserMailu alias $DOLIBARR_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS DOLIBARR_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $DOLIBARR_ADMIN_PASSWORD | tr -d ':\n') generateCert dolibarr-app dolibarr-app generateCert dolibarr-cron dolibarr-cron outputConfigDolibarr installStack dolibarr dolibarr-app "You can connect to the running Dolibarr web application with" $HOME/dolibarr.env 20 900 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$DOLIBARR_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_DOLIBARR_APP Admin Login Info" -b "$FMLNAME_DOLIBARR_APP Admin Username: $DOLIBARR_ADMIN_USERNAME\n$FMLNAME_DOLIBARR_APP Admin Password: $DOLIBARR_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" DOLIBARR_INIT_ENV=true updateConfigVar DOLIBARR_INIT_ENV $DOLIBARR_INIT_ENV fi echo "Performing integrations..." set +e sleep 3 docker exec dolibarr-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/dolibarr/dbexport/setupDBSettings.sh rm -f $HSHQ_STACKS_DIR/dolibarr/dbexport/initdb.sql addReadOnlyUserToDatabase Dolibarr mysql dolibarr-db $DOLIBARR_DATABASE_NAME root $DOLIBARR_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/dolibarr/dbexport $DOLIBARR_DATABASE_READONLYUSER $DOLIBARR_DATABASE_READONLYUSER_PASSWORD addMCPServerLiteLLM "dolibarr" "doli" "http://dolibarr-mcp:80/mcp" http none "" #echo "Restarting Dolibarr..." #docker container restart dolibarr-app > /dev/null 2>&1 set -e inner_block="" inner_block=$inner_block">>https://$SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://dolibarr-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DOLIBARR_APP $MANAGETLS_DOLIBARR_APP "$is_integrate_hshq" $NETDEFAULT_DOLIBARR_APP "$inner_block" insertSubAuthelia $SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll dolibarr "$FMLNAME_DOLIBARR_APP" $USERTYPE_DOLIBARR_APP "https://$SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN" "dolibarr.png" "$(getHeimdallOrderFromSub $SUB_DOLIBARR_APP $USERTYPE_DOLIBARR_APP)" restartAllCaddyContainers checkAddDBConnection true dolibarr "$FMLNAME_DOLIBARR_APP" mysql dolibarr-db $DOLIBARR_DATABASE_NAME $DOLIBARR_DATABASE_USER $DOLIBARR_DATABASE_USER_PASSWORD fi } function outputConfigDolibarr() { outputComposeDolibarr cat < $HOME/dolibarr.env TZ=\${PORTAINER_TZ} PHP_INI_DATE_TIMEZONE=\${PORTAINER_TZ} WWW_USER_ID=$USERID WWW_GROUP_ID=$GROUPID DOLI_INIT_DEMO=$(if [ "$IS_HSHQ_DEV_TEST" = "true" ]; then echo "1"; else echo "0"; fi) MAIN_FEATURES_LEVEL=2 DOLI_CRON_KEY=$DOLIBARR_CRON_SECRET DOLI_COMPANY_NAME=$HOMESERVER_NAME MYSQL_DATABASE=$DOLIBARR_DATABASE_NAME MYSQL_ROOT_PASSWORD=$DOLIBARR_DATABASE_ROOT_PASSWORD MYSQL_USER=$DOLIBARR_DATABASE_USER MYSQL_PASSWORD=$DOLIBARR_DATABASE_USER_PASSWORD DOLI_INSTALL_AUTO=1 DOLI_DB_TYPE=mysqli DOLI_DB_HOST=dolibarr-db DOLI_DB_HOST_PORT=3306 DOLI_DB_USER=$DOLIBARR_DATABASE_USER DOLI_DB_PASSWORD=$DOLIBARR_DATABASE_USER_PASSWORD DOLI_DB_NAME=$DOLIBARR_DATABASE_NAME DOLI_URL_ROOT=https://$SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN DOLI_ADMIN_LOGIN=$DOLIBARR_ADMIN_USERNAME DOLI_ADMIN_PASSWORD=$DOLIBARR_ADMIN_PASSWORD DOLI_AUTH=ldap,dolibarr DOLI_LDAP_HOST=ldaps://ldapserver DOLI_LDAP_PORT=636 DOLI_LDAP_VERSION=3 DOLI_LDAP_SERVER_TYPE=openldap DOLI_LDAP_LOGIN_ATTRIBUTE=uid DOLI_LDAP_DN=ou=people,$LDAP_BASE_DN DOLI_LDAP_FILTER=(&(objectClass=person)(memberof=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)(uid=%1%)) DOLI_LDAP_BIND_DN=$LDAP_READONLY_USER_BIND_DN DOLI_LDAP_BIND_PASS=$LDAP_READONLY_USER_PASSWORD DOLI_INSTANCE_UNIQUE_ID=$DOLIBARR_INSTANCE_UNIQUE_ID API_PRODUCTION_DO_NOT_ALWAYS_REFRESH_CACHE=true LOG_LEVEL=INFO PYTHONUNBUFFERED=1 DOLIBARR_BASE_URL=http://dolibarr-app:80 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFMT cat < $HSHQ_STACKS_DIR/dolibarr/ldap-app.conf TLS_CERT /ldapcerts/dolibarr-app.crt TLS_KEY /ldapcerts/dolibarr-app.key TLS_CACERT /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT always TLS_REQSAN always EOFLD cat < $HSHQ_STACKS_DIR/dolibarr/ldap-cron.conf TLS_CERT /ldapcerts/dolibarr-cron.crt TLS_KEY /ldapcerts/dolibarr-cron.key TLS_CACERT /usr/local/share/ca-certificates/${CERTS_ROOT_CA_NAME}.crt TLS_REQCERT always TLS_REQSAN always EOFLD cat < $HSHQ_STACKS_DIR/dolibarr/dbexport/setupDBSettings.sh #!/bin/bash mysql -N -u $DOLIBARR_DATABASE_USER -p$DOLIBARR_DATABASE_USER_PASSWORD < /dbexport/initdb.sql EOFDS chmod +x $HSHQ_STACKS_DIR/dolibarr/dbexport/setupDBSettings.sh cat < $HOME/pwenc.php EOFMT DOLIBARR_LDAPREADONLY_PASSWORD_ENC=$(php $HOME/pwenc.php) cat < $HOME/pwenc.php EOFMT DOLIBARR_API_KEY_ENC=$(php $HOME/pwenc.php) rm -f $HOME/pwenc.php dtnow=$(date -u '+%Y-%m-%d %H:%M:%S') cat < $HSHQ_STACKS_DIR/dolibarr/dbexport/initdb.sql use $DOLIBARR_DATABASE_NAME; delete from llx_const where name='MAIN_MONNAIE'; delete from llx_const where name='MAIN_MAIL_SMTP_SERVER'; delete from llx_const where name='MAIN_MAIL_SMTP_PORT'; delete from llx_const where name='MAIN_MAIL_EMAIL_FROM'; delete from llx_const where name='MAIN_INFO_SOCIETE_NOM'; #delete from llx_const where name='MAIN_THEME'; #delete from llx_const where name='THEME_DARKMODEENABLED'; #delete from llx_const where name='MAIN_IHM_PARAMS_REV'; insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_INFO_SOCIETE_COUNTRY',1,'11:US:United States','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_INFO_SOCIETE_NOM',1,'$HOMESERVER_NAME','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MONNAIE',1,'USD','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_INFO_SOCIETE_FORME_JURIDIQUE',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('SOCIETE_FISCAL_MONTH_START',1,'1','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('FACTURE_TVAOPTION',1,'1','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_DISABLE_ALL_MAILS',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_ENABLED_USER_DEST_SELECT',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_NO_WITH_TO_SELECTED',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_SENDMODE',1,'smtps','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_SMTP_PORT',1,'$SMTP_HOSTPORT','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_SMTP_SERVER',1,'$SMTP_HOSTNAME','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_SMTPS_AUTH_TYPE',1,'PLAIN','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_EMAIL_TLS',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_EMAIL_STARTTLS',1,'1','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_EMAIL_SMTP_ALLOW_SELF_SIGNED',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_EMAIL_DKIM_ENABLED',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_EMAIL_FROM',1,'$EMAIL_ADMIN_EMAIL_ADDRESS','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MAIL_DEFAULT_FROMTYPE',1,'user','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MODULE_LDAP',1,'1','string',0,'{\"authorid\":\"1\",\"ip\":\"\",\"lastactivationversion\":\"dolibarr\"}','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_MODULE_SETUP_ON_LIST_BY_DEFAULT',1,'commonkanban','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_SERVER_TYPE',1,'openldap','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_USERACCOUNTCONTROL',1,'512','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_SERVER_PROTOCOLVERSION',1,'3','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_SERVER_HOST',1,'ldaps://ldapserver','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_SERVER_PORT',1,'636','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_SERVER_DN',1,'$LDAP_BASE_DN','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_ADMIN_DN',1,'$LDAP_READONLY_USER_BIND_DN','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_ADMIN_PASS',1,'$DOLIBARR_LDAPREADONLY_PASSWORD_ENC','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_SERVER_USE_TLS',1,'0','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_SYNCHRO_ACTIVE',1,'2','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_PASSWORD_HASH_TYPE',1,'md5','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_USER_DN',1,'ou=people,$LDAP_BASE_DN','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_USER_OBJECT_CLASS',1,'person','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_FILTER_CONNECTION',1,'&(objectClass=person)(memberof=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_FIELD_FULLNAME',1,'cn','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_FIELD_LOGIN',1,'uid','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_FIELD_NAME',1,'sn','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_FIELD_FIRSTNAME',1,'givenname','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_FIELD_MAIL',1,'mail','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_KEY_USERS',1,'uid','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_GROUP_DN',1,'ou=groups,$LDAP_BASE_DN','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_GROUP_OBJECT_CLASS',1,'groupOfUniqueNames','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_GROUP_FILTER',1,'&(objectClass=groupOfUniqueNames)','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_GROUP_FIELD_FULLNAME',1,'cn','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_GROUP_FIELD_GROUPMEMBERS',1,'uniqueMember','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_GROUP_FIELD_GROUPID',1,'gidnumber','chaine',0,'','$dtnow'); insert into llx_const(name,entity,value,type,visible,note,tms) values('LDAP_KEY_GROUPS',1,'cn','chaine',0,'','$dtnow'); #insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_THEME',1,'eldy','chaine',0,'','$dtnow'); #insert into llx_const(name,entity,value,type,visible,note,tms) values('THEME_DARKMODEENABLED',1,'2','chaine',0,'','$dtnow'); #insert into llx_const(name,entity,value,type,visible,note,tms) values('MAIN_IHM_PARAMS_REV',1,'3','chaine',0,'','$dtnow'); update llx_user set api_key='$DOLIBARR_API_KEY_ENC' where login='$DOLIBARR_ADMIN_USERNAME'; EOFSQ } function outputComposeDolibarr() { cat < $HOME/dolibarr-compose.yml $STACK_VERSION_PREFIX dolibarr $(getScriptStackVersion dolibarr) services: dolibarr-db: image: $(getScriptImageByContainerName dolibarr-db) container_name: dolibarr-db hostname: dolibarr-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-dolibarr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-dolibarr-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.dolibarr-hourly-db.schedule=@every 1h" - "ofelia.job-exec.dolibarr-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.dolibarr-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.dolibarr-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.dolibarr-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.dolibarr-hourly-db.email-from=Dolibarr Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.dolibarr-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.dolibarr-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.dolibarr-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.dolibarr-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.dolibarr-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.dolibarr-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.dolibarr-monthly-db.email-from=Dolibarr Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.dolibarr-monthly-db.mail-only-on-error=false" dolibarr-app: image: $(getScriptImageByContainerName dolibarr-app) container_name: dolibarr-app hostname: dolibarr-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-app.crt:/ldapcerts/dolibarr-app.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-app.key:/ldapcerts/dolibarr-app.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/ldap-app.conf:/etc/ldap/ldap.conf:ro - v-dolibarr-custom:/var/www/html/custom - v-dolibarr-documents:/var/www/documents environment: - DOLI_CRON=0 dolibarr-cron: image: $(getScriptImageByContainerName dolibarr-cron) container_name: dolibarr-cron hostname: dolibarr-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-cron.crt:/ldapcerts/dolibarr-cron.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-cron.key:/ldapcerts/dolibarr-cron.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/ldap-cron.conf:/etc/ldap/ldap.conf:ro - v-dolibarr-custom:/var/www/html/custom - v-dolibarr-documents:/var/www/documents environment: - DOLI_CRON=1 dolibarr-redis: image: $(getScriptImageByContainerName dolibarr-redis) container_name: dolibarr-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $DOLIBARR_REDIS_PASSWORD --appendonly no networks: - int-dolibarr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro dolibarr-mcp: image: $(getScriptImageByContainerName dolibarr-mcp) container_name: dolibarr-mcp hostname: dolibarr-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-dolibarr-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/db v-dolibarr-custom: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/custom v-dolibarr-documents: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/documents networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-dolibarr-net: driver: bridge internal: true ipam: driver: default EOFMT } function outputComposeV3Dolibarr() { cat < $HOME/dolibarr-compose.yml $STACK_VERSION_PREFIX dolibarr v3 services: dolibarr-db: image: mirror.gcr.io/mariadb:10.7.3 container_name: dolibarr-db hostname: dolibarr-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-dolibarr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-dolibarr-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.dolibarr-hourly-db.schedule=@every 1h" - "ofelia.job-exec.dolibarr-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.dolibarr-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.dolibarr-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.dolibarr-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.dolibarr-hourly-db.email-from=Dolibarr Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.dolibarr-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.dolibarr-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.dolibarr-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.dolibarr-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.dolibarr-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.dolibarr-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.dolibarr-monthly-db.email-from=Dolibarr Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.dolibarr-monthly-db.mail-only-on-error=false" dolibarr-app: image: mirror.gcr.io/dolibarr/dolibarr:23.0.1-php8.2 container_name: dolibarr-app hostname: dolibarr-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-app.crt:/ldapcerts/dolibarr-app.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-app.key:/ldapcerts/dolibarr-app.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/ldap-app.conf:/etc/ldap/ldap.conf:ro - v-dolibarr-custom:/var/www/html/custom - v-dolibarr-documents:/var/www/documents environment: - DOLI_CRON=0 dolibarr-cron: image: mirror.gcr.io/dolibarr/dolibarr:23.0.1-php8.2 container_name: dolibarr-cron hostname: dolibarr-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-ext-net - dock-internalmail-net - dock-ldap-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-cron.crt:/ldapcerts/dolibarr-cron.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-cron.key:/ldapcerts/dolibarr-cron.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/ldap-cron.conf:/etc/ldap/ldap.conf:ro - v-dolibarr-custom:/var/www/html/custom - v-dolibarr-documents:/var/www/documents environment: - DOLI_CRON=1 dolibarr-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: dolibarr-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $DOLIBARR_REDIS_PASSWORD --appendonly no networks: - int-dolibarr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro dolibarr-mcp: image: hshq/dolibarr-mcp:v1 container_name: dolibarr-mcp hostname: dolibarr-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/mcp/config:/app/config:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/mcp/plugins:/app/plugins:ro dolibarr-mcp-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: dolibarr-mcp-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $DOLIBARR_MCP_REDIS_PASSWORD --appendonly no networks: - int-dolibarr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-dolibarr-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/db v-dolibarr-custom: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/custom v-dolibarr-documents: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/documents networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-dolibarr-net: driver: bridge internal: true ipam: driver: default EOFMT } function buildImageDolibarrMCPV1() { set +e echo -e "\n========================================================================" echo -e " The Dolibarr MCP image is being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/dolibarr-mcp-multitenant cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 https://github.com/homeserverhq/dolibarr-mcp-multitenant.git cd $HSHQ_BUILD_DIR/dolibarr-mcp-multitenant docker image build -t hshq/dolibarr-mcp:v1 -f docker/Dockerfile . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/dolibarr-mcp-multitenant return $rtval } function performUpdateDolibarr() { perform_stack_name=dolibarr prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. docker exec dolibarr-app rm -f /var/www/documents/install.lock > /dev/null 2>&1 case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/dolibarr/dolibarr:20.0.4-php8.2,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/dolibarr/dolibarr:20.0.4-php8.2,mirror.gcr.io/dolibarr/dolibarr:21.0.4-php8.2" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 2) newVer=v3 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/dolibarr/dolibarr:23.0.1-php8.2,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/dolibarr/dolibarr:23.0.1-php8.2,mirror.gcr.io/dolibarr/dolibarr:23.0.1-php8.2" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfDolibarrUpdateV3 perform_update_report="${perform_update_report}$stack_upgrade_report" waitForContainerLogString dolibarr-db 3 60 "ready for connections" docker exec dolibarr-db /dbexport/setAPIKey.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/dolibarr/dbexport/setAPIKey.sh return ;; 3) newVer=v3 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/dolibarr/dolibarr:23.0.1-php8.2,mirror.gcr.io/valkey/valkey:alpine3.23,hshq/dolibarr-mcp:v1 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/dolibarr/dolibarr:23.0.1-php8.2,mirror.gcr.io/dolibarr/dolibarr:23.0.3-php8.2" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="hshq/dolibarr-mcp:v1,ghcr.io/homeserverhq/dolibarr-mcp:v2" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfDolibarrUpdateV4 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 4) newVer=v5 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/dolibarr/dolibarr:23.0.3-php8.2,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/dolibarr-mcp:v2 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/dolibarr/dolibarr:23.0.3-php8.2,ghcr.io/homeserverhq/dolibarr:v23.0.4" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="ghcr.io/homeserverhq/dolibarr-mcp:v2,ghcr.io/homeserverhq/dolibarr-mcp:v2" ;; 5) newVer=v6 curImageList=mirror.gcr.io/mariadb:10.7.3,ghcr.io/homeserverhq/dolibarr:v23.0.4,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/dolibarr-mcp:v2 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="ghcr.io/homeserverhq/dolibarr:v23.0.4,mirror.gcr.io/dolibarr/dolibarr:23.0.4" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="ghcr.io/homeserverhq/dolibarr-mcp:v2,ghcr.io/homeserverhq/dolibarr-mcp:v2" ;; 6) newVer=v6 curImageList=mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/dolibarr/dolibarr:23.0.4,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/dolibarr-mcp:v2 image_update_map[0]="mirror.gcr.io/mariadb:10.7.3,mirror.gcr.io/mariadb:10.7.3" image_update_map[1]="mirror.gcr.io/dolibarr/dolibarr:23.0.4,mirror.gcr.io/dolibarr/dolibarr:23.0.4" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="ghcr.io/homeserverhq/dolibarr-mcp:v2,ghcr.io/homeserverhq/dolibarr-mcp:v2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfDolibarrUpdateV3() { set +e buildImageDolibarrMCPV1 if [ $? -ne 0 ]; then return 1 fi pullImage mirror.gcr.io/valkey/valkey:alpine3.23 if [ $? -ne 0 ]; then return 1 fi if [ "$stackStatus" = "1" ];then # Stack is running, stop it, then clear data startStopStack dolibarr stop isStartStackFromStopped=true sleep 3 fi sudo rm -fr $HSHQ_NONBACKUP_DIR/dolibarr/redis/* mkdir -p $HSHQ_NONBACKUP_DIR/dolibarr/mcpredis mkdir -p $HSHQ_STACKS_DIR/dolibarr/mcp/{config,plugins} DOLIBARR_INSTANCE_UNIQUE_ID=$(grep ^DOLI_INSTANCE_UNIQUE_ID= $HOME/dolibarr.env | sed 's/^[^=]*=//' | sed 's/ *$//g' | sed 's/"//g') updateConfigVar DOLIBARR_INSTANCE_UNIQUE_ID $DOLIBARR_INSTANCE_UNIQUE_ID grep -q MCP_HTTP_PORT $HOME/dolibarr.env if [ $? -ne 0 ]; then echo "API_PRODUCTION_DO_NOT_ALWAYS_REFRESH_CACHE=true" >> $HOME/dolibarr.env echo "DOLIBARR_URL=http://dolibarr-app/api/index.php" >> $HOME/dolibarr.env echo "LOG_LEVEL=INFO" >> $HOME/dolibarr.env echo "PYTHONUNBUFFERED=1" >> $HOME/dolibarr.env echo "MCP_TRANSPORT=http" >> $HOME/dolibarr.env echo "MCP_HTTP_PORT=8080" >> $HOME/dolibarr.env echo "MCP_HTTP_HOST=0.0.0.0" >> $HOME/dolibarr.env echo "MCP_HOST_PORT=18004" >> $HOME/dolibarr.env echo "MCP_SERVER_NAME=dolibarr-mcp" >> $HOME/dolibarr.env echo "MCP_SERVER_VERSION=2.1.0" >> $HOME/dolibarr.env echo "MCP_NETWORK_NAME=dock-aipriv-net" >> $HOME/dolibarr.env echo "MCP_NETWORK_EXTERNAL=true" >> $HOME/dolibarr.env echo "OUTPUT_FORMAT=toon" >> $HOME/dolibarr.env echo "MCP_AUTH_ENABLED=true" >> $HOME/dolibarr.env echo "MCP_API_KEY=$DOLIBARR_MCP_API_KEY" >> $HOME/dolibarr.env echo "CACHE_ENABLED=true" >> $HOME/dolibarr.env echo "DRAGONFLY_HOST=dolibarr-mcp-redis" >> $HOME/dolibarr.env echo "DRAGONFLY_PORT=6379" >> $HOME/dolibarr.env echo "DRAGONFLY_PASSWORD=$DOLIBARR_MCP_REDIS_PASSWORD" >> $HOME/dolibarr.env fi outputComposeV3Dolibarr cat < $HOME/pwenc.php EOFMT DOLIBARR_API_KEY_ENC=$(php $HOME/pwenc.php) rm -f $HOME/pwenc.php cat < $HSHQ_STACKS_DIR/dolibarr/dbexport/setAPIKey.sh #!/bin/bash mysql -N -u $DOLIBARR_DATABASE_USER -p$DOLIBARR_DATABASE_USER_PASSWORD -e "use $DOLIBARR_DATABASE_NAME; update llx_user set api_key='$DOLIBARR_API_KEY_ENC' where login='$DOLIBARR_ADMIN_USERNAME';" EOFDS chmod +x $HSHQ_STACKS_DIR/dolibarr/dbexport/setAPIKey.sh addMCPServerLiteLLM "dolibarr" "doli" "http://dolibarr-mcp:80/mcp" http none "" } function mfDolibarrUpdateV4() { buildOrPullImage $(getScriptImageByContainerName dolibarr-mcp) if [ $? -ne 0 ]; then return 1 fi cat < $HOME/dolibarr-compose.yml $STACK_VERSION_PREFIX dolibarr v4 services: dolibarr-db: image: mirror.gcr.io/mariadb:10.7.3 container_name: dolibarr-db hostname: dolibarr-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-dolibarr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-dolibarr-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.dolibarr-hourly-db.schedule=@every 1h" - "ofelia.job-exec.dolibarr-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.dolibarr-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.dolibarr-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.dolibarr-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.dolibarr-hourly-db.email-from=Dolibarr Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.dolibarr-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.dolibarr-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.dolibarr-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.dolibarr-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.dolibarr-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.dolibarr-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.dolibarr-monthly-db.email-from=Dolibarr Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.dolibarr-monthly-db.mail-only-on-error=false" dolibarr-app: image: mirror.gcr.io/dolibarr/dolibarr:23.0.3-php8.2 container_name: dolibarr-app hostname: dolibarr-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-app.crt:/ldapcerts/dolibarr-app.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-app.key:/ldapcerts/dolibarr-app.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/ldap-app.conf:/etc/ldap/ldap.conf:ro - v-dolibarr-custom:/var/www/html/custom - v-dolibarr-documents:/var/www/documents environment: - DOLI_CRON=0 dolibarr-cron: image: mirror.gcr.io/dolibarr/dolibarr:23.0.3-php8.2 container_name: dolibarr-cron hostname: dolibarr-cron restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-ext-net - dock-internalmail-net - dock-ldap-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-cron.crt:/ldapcerts/dolibarr-cron.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/dolibarr-cron.key:/ldapcerts/dolibarr-cron.key:ro - \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/ldap-cron.conf:/etc/ldap/ldap.conf:ro - v-dolibarr-custom:/var/www/html/custom - v-dolibarr-documents:/var/www/documents environment: - DOLI_CRON=1 dolibarr-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: dolibarr-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $DOLIBARR_REDIS_PASSWORD --appendonly no networks: - int-dolibarr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro dolibarr-mcp: image: ghcr.io/homeserverhq/dolibarr-mcp:v2 container_name: dolibarr-mcp hostname: dolibarr-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true pull_policy: always depends_on: - dolibarr-db networks: - int-dolibarr-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-dolibarr-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/db v-dolibarr-custom: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/custom v-dolibarr-documents: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/dolibarr/documents networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-dolibarr-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/dolibarr.env TZ=\${PORTAINER_TZ} PHP_INI_DATE_TIMEZONE=\${PORTAINER_TZ} WWW_USER_ID=$USERID WWW_GROUP_ID=$GROUPID DOLI_INIT_DEMO=$(if [ "$IS_HSHQ_DEV_TEST" = "true" ]; then echo "1"; else echo "0"; fi) MAIN_FEATURES_LEVEL=2 DOLI_CRON_KEY=$DOLIBARR_CRON_SECRET DOLI_COMPANY_NAME=$HOMESERVER_NAME MYSQL_DATABASE=$DOLIBARR_DATABASE_NAME MYSQL_ROOT_PASSWORD=$DOLIBARR_DATABASE_ROOT_PASSWORD MYSQL_USER=$DOLIBARR_DATABASE_USER MYSQL_PASSWORD=$DOLIBARR_DATABASE_USER_PASSWORD DOLI_INSTALL_AUTO=1 DOLI_DB_TYPE=mysqli DOLI_DB_HOST=dolibarr-db DOLI_DB_HOST_PORT=3306 DOLI_DB_USER=$DOLIBARR_DATABASE_USER DOLI_DB_PASSWORD=$DOLIBARR_DATABASE_USER_PASSWORD DOLI_DB_NAME=$DOLIBARR_DATABASE_NAME DOLI_URL_ROOT=https://$SUB_DOLIBARR_APP.$HOMESERVER_DOMAIN DOLI_ADMIN_LOGIN=$DOLIBARR_ADMIN_USERNAME DOLI_ADMIN_PASSWORD=$DOLIBARR_ADMIN_PASSWORD DOLI_AUTH=ldap,dolibarr DOLI_LDAP_HOST=ldaps://ldapserver DOLI_LDAP_PORT=636 DOLI_LDAP_VERSION=3 DOLI_LDAP_SERVER_TYPE=openldap DOLI_LDAP_LOGIN_ATTRIBUTE=uid DOLI_LDAP_DN=ou=people,$LDAP_BASE_DN DOLI_LDAP_FILTER=(&(objectClass=person)(memberof=cn=$LDAP_PRIMARY_USER_GROUP_NAME,ou=groups,$LDAP_BASE_DN)(uid=%1%)) DOLI_LDAP_BIND_DN=$LDAP_READONLY_USER_BIND_DN DOLI_LDAP_BIND_PASS=$LDAP_READONLY_USER_PASSWORD DOLI_INSTANCE_UNIQUE_ID=$DOLIBARR_INSTANCE_UNIQUE_ID API_PRODUCTION_DO_NOT_ALWAYS_REFRESH_CACHE=true LOG_LEVEL=INFO PYTHONUNBUFFERED=1 DOLIBARR_BASE_URL=http://dolibarr-app:80 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFMT } # n8n function installn8n() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory n8n "n8n" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName n8n-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName n8n-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName n8n-worker) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName n8n-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/n8n mkdir $HSHQ_STACKS_DIR/n8n/data mkdir $HSHQ_STACKS_DIR/n8n/files mkdir $HSHQ_STACKS_DIR/n8n/db mkdir $HSHQ_STACKS_DIR/n8n/dbexport chmod 777 $HSHQ_STACKS_DIR/n8n/dbexport initServicesCredentials set +e addUserMailu alias $N8N_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS N8N_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $N8N_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2a/' | sed 's/\$/\\\$/g') outputConfign8n installStack n8n n8n-app "Editor is now accessible via" $HOME/n8n.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$N8N_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_N8N_APP Admin Login Info" -b "$FMLNAME_N8N_APP Admin Username: $N8N_ADMIN_EMAIL_ADDRESS\n$FMLNAME_N8N_APP Admin Password: $N8N_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" N8N_INIT_ENV=true updateConfigVar N8N_INIT_ENV $N8N_INIT_ENV fi sleep 3 docker exec n8n-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/n8n/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase n8n postgres n8n-db $N8N_DATABASE_NAME $N8N_DATABASE_USER $N8N_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/n8n/dbexport $N8N_DATABASE_READONLYUSER $N8N_DATABASE_READONLYUSER_PASSWORD docker container restart n8n-app set -e inner_block="" inner_block=$inner_block">>https://$SUB_N8N_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://n8n-app:5678 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>flush_interval -1\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_N8N_APP $MANAGETLS_N8N_APP "$is_integrate_hshq" $NETDEFAULT_N8N_APP "$inner_block" insertSubAuthelia $SUB_N8N_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll n8n "$FMLNAME_N8N_APP" $USERTYPE_N8N_APP "https://$SUB_N8N_APP.$HOMESERVER_DOMAIN" "n8n.png" "$(getHeimdallOrderFromSub $SUB_N8N_APP $USERTYPE_N8N_APP)" restartAllCaddyContainers checkAddDBConnection true n8n "$FMLNAME_N8N_APP" postgres n8n-db $N8N_DATABASE_NAME $N8N_DATABASE_USER $N8N_DATABASE_USER_PASSWORD fi } function outputConfign8n() { cat < $HOME/n8n-compose.yml $STACK_VERSION_PREFIX n8n $(getScriptStackVersion n8n) services: n8n-db: image: $(getScriptImageByContainerName n8n-db) container_name: n8n-db hostname: n8n-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-n8n-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/n8n/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/n8n/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.n8n-hourly-db.schedule=@every 1h" - "ofelia.job-exec.n8n-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.n8n-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.n8n-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.n8n-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.n8n-hourly-db.email-from=n8n Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.n8n-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.n8n-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.n8n-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.n8n-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.n8n-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.n8n-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.n8n-monthly-db.email-from=n8n Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.n8n-monthly-db.mail-only-on-error=false" n8n-app: image: $(getScriptImageByContainerName n8n-app) container_name: n8n-app hostname: n8n-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - n8n-db networks: - int-n8n-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-n8n-data:/home/node/.n8n - \${PORTAINER_HSHQ_STACKS_DIR}/n8n/files:/files n8n-worker: image: $(getScriptImageByContainerName n8n-worker) container_name: n8n-worker hostname: n8n-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - n8n-db - n8n-app command: worker networks: - int-n8n-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-n8n-data:/home/node/.n8n - \${PORTAINER_HSHQ_STACKS_DIR}/n8n/files:/files n8n-redis: image: $(getScriptImageByContainerName n8n-redis) container_name: n8n-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $N8N_REDIS_PASSWORD --appendonly no networks: - int-n8n-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-n8n-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/n8n/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-n8n-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/n8n.env TZ=\${PORTAINER_TZ} GENERIC_TIMEZONE=\${PORTAINER_TZ} POSTGRES_DB=$N8N_DATABASE_NAME POSTGRES_USER=$N8N_DATABASE_USER POSTGRES_PASSWORD=$N8N_DATABASE_USER_PASSWORD N8N_BASIC_AUTH_ACTIVE=true N8N_BASIC_AUTH_USER=$N8N_ADMIN_USERNAME N8N_BASIC_AUTH_PASSWORD=$N8N_ADMIN_PASSWORD DB_TYPE=postgresdb DB_POSTGRESDB_HOST=n8n-db DB_POSTGRESDB_PORT=5432 DB_POSTGRESDB_DATABASE=$N8N_DATABASE_NAME DB_POSTGRESDB_USER=$N8N_DATABASE_USER DB_POSTGRESDB_PASSWORD=$N8N_DATABASE_USER_PASSWORD N8N_PROTOCOL=http N8N_HOST=$SUB_N8N_APP.$HOMESERVER_DOMAIN VUE_APP_URL_BASE_API=https://$SUB_N8N_APP.$HOMESERVER_DOMAIN WEBHOOK_TUNNEL_URL=https://$SUB_N8N_APP.$HOMESERVER_DOMAIN WEBHOOK_URL=https://$SUB_N8N_APP.$HOMESERVER_DOMAIN N8N_EMAIL_MODE=smtp N8N_SMTP_HOST=$SMTP_HOSTNAME N8N_SMTP_PORT=$SMTP_HOSTPORT N8N_SMTP_USER= N8N_SMTP_PASS= N8N_SMTP_SENDER=n8n HSHQ Admin<$EMAIL_ADMIN_EMAIL_ADDRESS> N8N_SMTP_SSL=false N8N_SMTP_STARTTLS=true NODE_ENV=production NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt QUEUE_BULL=true QUEUE_BULL_REDIS_HOST=n8n-redis QUEUE_BULL_REDIS_PASSWORD=$N8N_REDIS_PASSWORD N8N_ENCRYPTION_KEY=$N8N_ENCRYPTION_KEY EXECUTIONS_MODE=queue EXECUTIONS_DATA_MAX_AGE=72 EXECUTIONS_DATA_PRUNE=true N8N_RUNNERS_ENABLED=true OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true N8N_BLOCK_ENV_ACCESS_IN_NODE=false N8N_GIT_NODE_DISABLE_BARE_REPOS=true N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true N8N_PROXY_HOPS=1 EOFMT dtnow=$(date -u '+%Y-%m-%dT%H:%M:%S.%3NZ') cat < $HSHQ_STACKS_DIR/n8n/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$N8N_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do admUser=\$(echo "select slug from role where \"displayName\"='Admin';" | psql -t -A -U $N8N_DATABASE_USER $N8N_DATABASE_NAME 2> /dev/null) if ! [ -z "\$admUser" ] && [ "\$admUser" = "global:admin" ]; then break fi echo "Database not ready, sleeping 3 seconds, total wait=\$curSeconds seconds..." sleep 3 curSeconds=\$((curSeconds+3)) done echo "update project set name='n8n $(getAdminEmailName) <$N8N_ADMIN_EMAIL_ADDRESS>' where \"type\"='personal';" | psql -U $N8N_DATABASE_USER $N8N_DATABASE_NAME echo "update \"user\" set email='$N8N_ADMIN_EMAIL_ADDRESS', \"firstName\"='$(getAdminEmailName)', \"lastName\"='n8n', password='$N8N_ADMIN_PASSWORD_HASH', \"personalizationAnswers\"='{\"version\":\"v4\",\"personalization_survey_submitted_at\":\"$dtnow\",\"personalization_survey_n8n_version\":\"1.114.0\",\"companyType\":\"personal\",\"reportedSource\":\"other\",\"reportedSourceOther\":\"na\"}' where \"roleSlug\"='global:owner';" | psql -U $N8N_DATABASE_USER $N8N_DATABASE_NAME echo "update settings set value='true' where key='userManagement.isInstanceOwnerSetUp';" | psql -U $N8N_DATABASE_USER $N8N_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/n8n/dbexport/setupDBSettings.sh } function performUpdaten8n() { perform_stack_name=n8n prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/n8nio/n8n:1.114.0,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/n8nio/n8n:1.114.0,mirror.gcr.io/n8nio/n8n:1.122.5" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/n8nio/n8n:1.122.5,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/n8nio/n8n:1.122.5,mirror.gcr.io/n8nio/n8n:1.122.5" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Automatisch function installAutomatisch() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory automatisch "Automatisch" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName automatisch-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName automatisch-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName automatisch-worker) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName automatisch-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/automatisch mkdir $HSHQ_STACKS_DIR/automatisch/db mkdir $HSHQ_STACKS_DIR/automatisch/dbexport mkdir $HSHQ_STACKS_DIR/automatisch/storage chmod 777 $HSHQ_STACKS_DIR/automatisch/dbexport initServicesCredentials set +e addUserMailu alias $AUTOMATISCH_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS AUTOMATISCH_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $AUTOMATISCH_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2b/' | sed 's/\$/\\\$/g') outputConfigAutomatisch installStack automatisch automatisch-app "Server is listening on" $HOME/automatisch.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$AUTOMATISCH_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_AUTOMATISCH_APP Admin Login Info" -b "$FMLNAME_AUTOMATISCH_APP Admin Username: $AUTOMATISCH_ADMIN_EMAIL_ADDRESS\n$FMLNAME_AUTOMATISCH_APP Admin Password: $AUTOMATISCH_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" AUTOMATISCH_INIT_ENV=true updateConfigVar AUTOMATISCH_INIT_ENV $AUTOMATISCH_INIT_ENV fi sleep 3 docker exec automatisch-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/automatisch/dbexport/setupDBSettings.sh addReadOnlyUserToDatabase Automatisch postgres automatisch-db $AUTOMATISCH_DATABASE_NAME $AUTOMATISCH_DATABASE_USER $AUTOMATISCH_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/automatisch/dbexport $AUTOMATISCH_DATABASE_READONLYUSER $AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_AUTOMATISCH_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://automatisch-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AUTOMATISCH_APP $MANAGETLS_AUTOMATISCH_APP "$is_integrate_hshq" $NETDEFAULT_AUTOMATISCH_APP "$inner_block" insertSubAuthelia $SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll automatisch "$FMLNAME_AUTOMATISCH_APP" $USERTYPE_AUTOMATISCH_APP "https://$SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN" "automatisch.png" "$(getHeimdallOrderFromSub $SUB_AUTOMATISCH_APP $USERTYPE_AUTOMATISCH_APP)" restartAllCaddyContainers checkAddDBConnection true automatisch "$FMLNAME_AUTOMATISCH_APP" postgres automatisch-db $AUTOMATISCH_DATABASE_NAME $AUTOMATISCH_DATABASE_USER $AUTOMATISCH_DATABASE_USER_PASSWORD fi } function outputConfigAutomatisch() { cat < $HOME/automatisch-compose.yml $STACK_VERSION_PREFIX automatisch $(getScriptStackVersion automatisch) services: automatisch-db: image: $(getScriptImageByContainerName automatisch-db) container_name: automatisch-db hostname: automatisch-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-automatisch-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/automatisch/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/automatisch/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.automatisch-hourly-db.schedule=@every 1h" - "ofelia.job-exec.automatisch-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.automatisch-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.automatisch-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.automatisch-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.automatisch-hourly-db.email-from=Automatisch Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.automatisch-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.automatisch-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.automatisch-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.automatisch-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.automatisch-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.automatisch-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.automatisch-monthly-db.email-from=Automatisch Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.automatisch-monthly-db.mail-only-on-error=false" automatisch-app: image: $(getScriptImageByContainerName automatisch-app) container_name: automatisch-app hostname: automatisch-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - automatisch-db - automatisch-redis networks: - int-automatisch-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-automatisch-storage:/automatisch/storage automatisch-worker: image: $(getScriptImageByContainerName automatisch-worker) container_name: automatisch-worker hostname: automatisch-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - automatisch-db - automatisch-redis - automatisch-app networks: - int-automatisch-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-automatisch-storage:/automatisch/storage environment: - WORKER=true automatisch-redis: image: $(getScriptImageByContainerName automatisch-redis) container_name: automatisch-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $AUTOMATISCH_REDIS_PASSWORD --appendonly no networks: - int-automatisch-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-automatisch-storage: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/automatisch/storage networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true int-automatisch-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/automatisch.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$AUTOMATISCH_DATABASE_NAME POSTGRES_USER=$AUTOMATISCH_DATABASE_USER POSTGRES_PASSWORD=$AUTOMATISCH_DATABASE_USER_PASSWORD APP_ENV=production REDIS_HOST=automatisch-redis REDIS_PASSWORD=$AUTOMATISCH_REDIS_PASSWORD POSTGRES_HOST=automatisch-db POSTGRES_DATABASE=$AUTOMATISCH_DATABASE_NAME POSTGRES_USERNAME=$AUTOMATISCH_DATABASE_USER WEB_APP_URL=https://$SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN WEBHOOK_URL=https://$SUB_AUTOMATISCH_APP.$HOMESERVER_DOMAIN ENCRYPTION_KEY=$AUTOMATISCH_ENCRYPTION_KEY WEBHOOK_SECRET_KEY=$AUTOMATISCH_WEBHOOK_SECRET_KEY APP_SECRET_KEY=$AUTOMATISCH_APP_SECRET_KEY TELEMETRY_ENABLED=false SMTP_HOST=$SMTP_HOSTNAME SMTP_PORT=$SMTP_HOSTPORT SMTP_SECURE=true FROM_EMAIL=Automatisch $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> EOFMT dtnow=$(date -u '+%Y-%m-%dT%H:%M:%S.%3NZ') cat < $HSHQ_STACKS_DIR/automatisch/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$AUTOMATISCH_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do admUser=\$(echo "select email from users;" | psql -t -A -U $AUTOMATISCH_DATABASE_USER $AUTOMATISCH_DATABASE_NAME 2> /dev/null) if ! [ -z "\$admUser" ]; then break fi echo "Database not ready, sleeping 3 seconds, total wait=\$curSeconds seconds..." sleep 3 curSeconds=\$((curSeconds+3)) done echo "update users set email='$AUTOMATISCH_ADMIN_EMAIL_ADDRESS', password='$AUTOMATISCH_ADMIN_PASSWORD_HASH', full_name='Automatisch $(getAdminEmailName)';" | psql -U $AUTOMATISCH_DATABASE_USER $AUTOMATISCH_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/automatisch/dbexport/setupDBSettings.sh } function performUpdateAutomatisch() { perform_stack_name=automatisch prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/automatischio/automatisch:0.15.0,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/automatischio/automatisch:0.15.0,mirror.gcr.io/automatischio/automatisch:0.15.0" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.2.0-bookworm" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # ActivePieces function installActivePieces() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory activepieces "ActivePieces" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName activepieces-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName activepieces-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName activepieces-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/activepieces mkdir $HSHQ_STACKS_DIR/activepieces/db mkdir $HSHQ_STACKS_DIR/activepieces/dbexport mkdir $HSHQ_STACKS_DIR/activepieces/cache chmod 777 $HSHQ_STACKS_DIR/activepieces/dbexport initServicesCredentials set +e addUserMailu alias $ACTIVEPIECES_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS ACTIVEPIECES_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $ACTIVEPIECES_ADMIN_PASSWORD | tr -d ':\n' | sed 's/\$2y/\$2b/' | sed 's/\$/\\\$/g') outputConfigActivePieces installStack activepieces activepieces-app "The application started on" $HOME/activepieces.env 20 900 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$ACTIVEPIECES_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_ACTIVEPIECES_APP Admin Login Info" -b "$FMLNAME_ACTIVEPIECES_APP Admin Username: $ACTIVEPIECES_ADMIN_EMAIL_ADDRESS\n$FMLNAME_ACTIVEPIECES_APP Admin Password: $ACTIVEPIECES_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" ACTIVEPIECES_INIT_ENV=true updateConfigVar ACTIVEPIECES_INIT_ENV $ACTIVEPIECES_INIT_ENV fi sleep 3 docker exec activepieces-db /dbexport/setupDBSettings.sh > /dev/null 2>&1 rm -f $HSHQ_STACKS_DIR/activepieces/dbexport/setupDBSettings.sh docker container restart activepieces-app > /dev/null 2>&1 addReadOnlyUserToDatabase ActivePieces postgres activepieces-db $ACTIVEPIECES_DATABASE_NAME $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/activepieces/dbexport $ACTIVEPIECES_DATABASE_READONLYUSER $ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_ACTIVEPIECES_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_ACTIVEPIECES_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://activepieces-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ACTIVEPIECES_APP $MANAGETLS_ACTIVEPIECES_APP "$is_integrate_hshq" $NETDEFAULT_ACTIVEPIECES_APP "$inner_block" insertSubAuthelia $SUB_ACTIVEPIECES_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll activepieces "$FMLNAME_ACTIVEPIECES_APP" $USERTYPE_ACTIVEPIECES_APP "https://$SUB_ACTIVEPIECES_APP.$HOMESERVER_DOMAIN" "activepieces.png" "$(getHeimdallOrderFromSub $SUB_ACTIVEPIECES_APP $USERTYPE_ACTIVEPIECES_APP)" restartAllCaddyContainers checkAddDBConnection true activepieces "$FMLNAME_ACTIVEPIECES_APP" postgres activepieces-db $ACTIVEPIECES_DATABASE_NAME $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_USER_PASSWORD fi } function outputConfigActivePieces() { cat < $HOME/activepieces-compose.yml $STACK_VERSION_PREFIX activepieces $(getScriptStackVersion activepieces) services: activepieces-db: image: $(getScriptImageByContainerName activepieces-db) container_name: activepieces-db hostname: activepieces-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-activepieces-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.activepieces-hourly-db.schedule=@every 1h" - "ofelia.job-exec.activepieces-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.activepieces-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.activepieces-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.activepieces-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.activepieces-hourly-db.email-from=ActivePieces Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.activepieces-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.activepieces-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.activepieces-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.activepieces-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.activepieces-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.activepieces-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.activepieces-monthly-db.email-from=ActivePieces Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.activepieces-monthly-db.mail-only-on-error=false" activepieces-app: image: $(getScriptImageByContainerName activepieces-app) container_name: activepieces-app hostname: activepieces-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - activepieces-db - activepieces-redis networks: - int-activepieces-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/cache:/usr/src/app/cache environment: - AP_CONTAINER_TYPE=APP activepieces-worker: image: $(getScriptImageByContainerName activepieces-worker) container_name: activepieces-worker hostname: activepieces-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - activepieces-app networks: - int-activepieces-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/cache:/usr/src/app/cache environment: - AP_CONTAINER_TYPE=WORKER activepieces-redis: image: $(getScriptImageByContainerName activepieces-redis) container_name: activepieces-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $ACTIVEPIECES_REDIS_PASSWORD --appendonly no networks: - int-activepieces-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-activepieces-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/activepieces.env TZ=\${PORTAINER_TZ} NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt POSTGRES_DB=$ACTIVEPIECES_DATABASE_NAME POSTGRES_USER=$ACTIVEPIECES_DATABASE_USER POSTGRES_PASSWORD=$ACTIVEPIECES_DATABASE_USER_PASSWORD AP_ENGINE_EXECUTABLE_PATH=dist/packages/engine/main.js AP_ENCRYPTION_KEY=$ACTIVEPIECES_ENCRYPTION_KEY AP_API_KEY=$ACTIVEPIECES_API_KEY AP_JWT_SECRET=$ACTIVEPIECES_JWT_SECRET AP_ENVIRONMENT=prod AP_FRONTEND_URL=https://$SUB_ACTIVEPIECES_APP.$HOMESERVER_DOMAIN AP_WEBHOOK_TIMEOUT_SECONDS=30 AP_TRIGGER_DEFAULT_POLL_INTERVAL=5 AP_DB_TYPE=POSTGRES AP_POSTGRES_DATABASE=$ACTIVEPIECES_DATABASE_NAME AP_POSTGRES_HOST=activepieces-db AP_POSTGRES_PORT=5432 AP_POSTGRES_USERNAME=$ACTIVEPIECES_DATABASE_USER AP_POSTGRES_PASSWORD=$ACTIVEPIECES_DATABASE_USER_PASSWORD AP_EXECUTION_MODE=UNSANDBOXED AP_REDIS_TYPE=STANDALONE AP_REDIS_HOST=activepieces-redis AP_REDIS_PORT=6379 AP_REDIS_PASSWORD=$ACTIVEPIECES_REDIS_PASSWORD AP_FLOW_TIMEOUT_SECONDS=600 AP_TELEMETRY_ENABLED=false AP_SIGN_UP_ENABLED=false AP_TEMPLATES_SOURCE_URL="https://cloud.activepieces.com/api/v1/flow-templates" AP_SMTP_HOST=$SMTP_HOSTNAME AP_SMTP_PORT=465 AP_SMTP_USERNAME=$EMAIL_SMTP_EMAIL_ADDRESS AP_SMTP_PASSWORD=$EMAIL_SMTP_PASSWORD AP_SMTP_SENDER_EMAIL=$EMAIL_SMTP_EMAIL_ADDRESS AP_SMTP_SENDER_NAME=ActivePieces $(getAdminEmailName) EOFMT dtnow=$(date -u '+%Y-%m-%d %H:%M:%S.%3N') ident_id=$(pwgen -c -n 21 1) tok_ver=$(pwgen -c -n 21 1) user_id=$(pwgen -c -n 21 1) plat_id=$(pwgen -c -n 21 1) proj_id=$(pwgen -c -n 21 1) cat < $HSHQ_STACKS_DIR/activepieces/dbexport/setupDBSettings.sh #!/bin/bash PGPASSWORD=$ACTIVEPIECES_DATABASE_USER_PASSWORD curSeconds=0 maxSeconds=300 while [ \$curSeconds -lt \$maxSeconds ] do admUser=\$(echo "select id from worker_machine limit 1;" | psql -t -A -U $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_NAME 2> /dev/null) if ! [ -z "\$admUser" ]; then break fi echo "Database not ready, sleeping 3 seconds, total wait=\$curSeconds seconds..." sleep 3 curSeconds=\$((curSeconds+3)) done echo "insert into user_identity(id,created,updated,email,password,\"trackEvents\",\"newsLetter\",verified,\"firstName\",\"lastName\",\"tokenVersion\",provider) values('$ident_id','$dtnow','$dtnow','$ACTIVEPIECES_ADMIN_EMAIL_ADDRESS','$ACTIVEPIECES_ADMIN_PASSWORD_HASH',true,false,true,'$(getAdminEmailName)','ActivePieces','$tok_ver','EMAIL');" | psql -U $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_NAME echo "insert into \"user\"(id,created,updated,status,\"platformId\",\"platformRole\",\"identityId\") values('$user_id','$dtnow','$dtnow','ACTIVE','$plat_id','ADMIN','$ident_id');" | psql -U $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_NAME echo "insert into platform(id,created,updated,\"ownerId\",name,\"primaryColor\",\"logoIconUrl\",\"fullLogoUrl\",\"favIconUrl\",\"cloudAuthEnabled\",\"filteredPieceNames\",\"filteredPieceBehavior\",\"allowedAuthDomains\",\"enforceAllowedAuthDomains\",\"emailAuthEnabled\",\"federatedAuthProviders\",\"pinnedPieces\") values('$plat_id','$dtnow','$dtnow','$user_id','ActivePieces Platform','#6e41e2','https://cdn.activepieces.com/brand/logo.svg','https://cdn.activepieces.com/brand/full-logo.png','https://cdn.activepieces.com/brand/favicon.ico',true,'{}','BLOCKED','{}',false,true,'{}','{}');" | psql -U $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_NAME echo "insert into project(id,created,updated,\"ownerId\",\"displayName\",\"platformId\",\"releasesEnabled\",icon,type) values('$proj_id','$dtnow','$dtnow','$user_id','ActivePieces Project','$plat_id',false,'{\"color\": \"GREEN\"}','PERSONAL');" | psql -U $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_NAME echo "insert into flag(id,created,updated,value) values('USER_CREATED','$dtnow','$dtnow','true');" | psql -U $ACTIVEPIECES_DATABASE_USER $ACTIVEPIECES_DATABASE_NAME EOFDS chmod +x $HSHQ_STACKS_DIR/activepieces/dbexport/setupDBSettings.sh } function performUpdateActivePieces() { perform_stack_name=activepieces prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/activepieces/activepieces:0.70.1,mirror.gcr.io/redis:8.2.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/activepieces/activepieces:0.70.1,ghcr.io/activepieces/activepieces:0.72.4" image_update_map[2]="mirror.gcr.io/redis:8.2.0-bookworm,mirror.gcr.io/redis:8.4.0-bookworm" ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/activepieces/activepieces:0.72.4,mirror.gcr.io/redis:8.4.0-bookworm image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/activepieces/activepieces:0.72.4,ghcr.io/activepieces/activepieces:0.90.4" image_update_map[2]="mirror.gcr.io/redis:8.4.0-bookworm,mirror.gcr.io/valkey/valkey:alpine3.23" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfActivePiecesUpdateV3 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v3 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/activepieces/activepieces:0.90.4,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/activepieces/activepieces:0.90.4,ghcr.io/activepieces/activepieces:0.90.4" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfActivePiecesUpdateV3() { sudo rm -fr $HSHQ_NONBACKUP_DIR/activepieces/redis/* cat < $HOME/activepieces-compose.yml $STACK_VERSION_PREFIX activepieces v3 services: activepieces-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: activepieces-db hostname: activepieces-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-activepieces-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.activepieces-hourly-db.schedule=@every 1h" - "ofelia.job-exec.activepieces-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.activepieces-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.activepieces-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.activepieces-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.activepieces-hourly-db.email-from=ActivePieces Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.activepieces-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.activepieces-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.activepieces-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.activepieces-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.activepieces-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.activepieces-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.activepieces-monthly-db.email-from=ActivePieces Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.activepieces-monthly-db.mail-only-on-error=false" activepieces-app: image: ghcr.io/activepieces/activepieces:0.90.4 container_name: activepieces-app hostname: activepieces-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - activepieces-db - activepieces-redis networks: - int-activepieces-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/cache:/usr/src/app/cache environment: - AP_CONTAINER_TYPE=APP activepieces-worker: image: ghcr.io/activepieces/activepieces:0.90.4 container_name: activepieces-worker hostname: activepieces-worker restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - activepieces-app networks: - int-activepieces-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/activepieces/cache:/usr/src/app/cache environment: - AP_CONTAINER_TYPE=WORKER activepieces-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: activepieces-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $ACTIVEPIECES_REDIS_PASSWORD --appendonly no networks: - int-activepieces-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-ldap-net: name: dock-ldap external: true dock-aipriv-net: name: dock-aipriv external: true int-activepieces-net: driver: bridge internal: true ipam: driver: default EOFMT } # Beszel function installBeszel() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory beszel "Beszel" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName beszel-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName beszel-agent) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/beszel mkdir $HSHQ_STACKS_DIR/beszel/hubdata mkdir $HSHQ_STACKS_DIR/beszel/socket mkdir $HSHQ_STACKS_DIR/beszel/agentdata initServicesCredentials set +e addUserMailu alias $BESZEL_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS BESZEL_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $BESZEL_ADMIN_PASSWORD | tr -d ':\n') outputConfigBeszel installStack beszel beszel-app "Server started at http" $HOME/beszel.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$BESZEL_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_BESZEL_APP Admin Login Info" -b "$FMLNAME_BESZEL_APP Admin Username: $BESZEL_ADMIN_EMAIL_ADDRESS\n$FMLNAME_BESZEL_APP Admin Password: $BESZEL_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" BESZEL_INIT_ENV=true updateConfigVar BESZEL_INIT_ENV $BESZEL_INIT_ENV fi sleep 3 beszel_app_key=$(sudo ssh-keygen -y -f $HSHQ_STACKS_DIR/beszel/hubdata/id_ed25519) updateStackEnv beszel modFunBeszelUpdateKeyToken > /dev/null 2>&1 if [ -z "$FMLNAME_BESZEL_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://beszel-app:8090 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_BESZEL_APP $MANAGETLS_BESZEL_APP "$is_integrate_hshq" $NETDEFAULT_BESZEL_APP "$inner_block" insertSubAuthelia $SUB_BESZEL_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll beszel "$FMLNAME_BESZEL_APP" $USERTYPE_BESZEL_APP "https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN" "beszel.png" "$(getHeimdallOrderFromSub $SUB_BESZEL_APP $USERTYPE_BESZEL_APP)" restartAllCaddyContainers fi updateStackEnv beszel modFunBeszelUpdateAgentToken > /dev/null 2>&1 } function outputConfigBeszel() { cat < $HOME/beszel-compose.yml $STACK_VERSION_PREFIX beszel $(getScriptStackVersion beszel) services: beszel-app: image: $(getScriptImageByContainerName beszel-app) container_name: beszel-app hostname: beszel-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-internalmail-net ports: - 127.0.0.1:8090:8090 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-beszel-hubdata:/beszel_data - v-beszel-socket:/beszel_socket beszel-agent: image: $(getScriptImageByContainerName beszel-agent) container_name: beszel-agent hostname: beszel-agent restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true network_mode: host volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-beszel-agentdata:/var/lib/beszel-agent - v-beszel-socket:/beszel_socket - /var/run/docker.sock:/var/run/docker.sock:ro volumes: v-beszel-hubdata: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/beszel/hubdata v-beszel-socket: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/beszel/socket v-beszel-agentdata: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/beszel/agentdata networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-internalmail-net: name: dock-internalmail external: true EOFMT cat < $HOME/beszel.env TZ=\${PORTAINER_TZ} APP_URL=https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN LISTEN=/beszel_socket/beszel.sock HUB_URL=http://127.0.0.1:8090 USER_EMAIL=$BESZEL_ADMIN_EMAIL_ADDRESS USER_PASSWORD=$BESZEL_ADMIN_PASSWORD KEY= TOKEN= EOFMT } function modFunBeszelUpdateKeyToken() { sed -i "s|^KEY=.*|KEY=${beszel_app_key}|g" $HOME/beszel.env } function modFunBeszelUpdateAgentToken() { curTries=1 maxTries=30 is_success=false while [ $curTries -le $maxTries ] do if curl -sf "https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN/api/beszel/first-run" >/dev/null 2>&1; then is_success=true break fi sleep 3 ((curTries++)) done if ! [ "$is_success" = "true" ]; then echo "Failed to configure agent token, returning..." return fi AUTH_TOKEN=$(curl -s -X POST "https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN/api/collections/users/auth-with-password" \ -H "Content-Type: application/json" \ -d "{\"identity\":\"$BESZEL_ADMIN_EMAIL_ADDRESS\",\"password\":\"$BESZEL_ADMIN_PASSWORD\"}" | jq -r '.token') AGENT_TOKEN=$(curl -s "https://$SUB_BESZEL_APP.$HOMESERVER_DOMAIN/api/beszel/universal-token?enable=1&permanent=1" \ -H "Authorization: Bearer $AUTH_TOKEN" | jq -r '.token') sed -i "s|^TOKEN=.*|TOKEN=${AGENT_TOKEN}|g" $HOME/beszel.env } function performUpdateBeszel() { perform_stack_name=beszel prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/henrygd/beszel:0.13.2,mirror.gcr.io/henrygd/beszel-agent:0.13.2 image_update_map[0]="mirror.gcr.io/henrygd/beszel:0.13.2,mirror.gcr.io/henrygd/beszel:0.17.0" image_update_map[1]="mirror.gcr.io/henrygd/beszel-agent:0.13.2,mirror.gcr.io/henrygd/beszel-agent:0.17.0" ;; 2) newVer=v3 curImageList=mirror.gcr.io/henrygd/beszel:0.17.0,mirror.gcr.io/henrygd/beszel-agent:0.17.0 image_update_map[0]="mirror.gcr.io/henrygd/beszel:0.17.0,ghcr.io/henrygd/beszel/beszel:0.19.0" image_update_map[1]="mirror.gcr.io/henrygd/beszel-agent:0.17.0,ghcr.io/henrygd/beszel/beszel-agent:0.19.0" ;; 3) newVer=v3 curImageList=ghcr.io/henrygd/beszel/beszel:0.19.0,ghcr.io/henrygd/beszel/beszel-agent:0.19.0 image_update_map[0]="ghcr.io/henrygd/beszel/beszel:0.19.0,ghcr.io/henrygd/beszel/beszel:0.19.0" image_update_map[1]="ghcr.io/henrygd/beszel/beszel-agent:0.19.0,ghcr.io/henrygd/beszel/beszel-agent:0.19.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Taiga function installTaiga() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory taiga "Taiga" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-back) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-async) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-async-rabbitmq) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-front) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-events) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-events-rabbitmq) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-protected) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName taiga-gateway) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/taiga mkdir $HSHQ_STACKS_DIR/taiga/config mkdir $HSHQ_STACKS_DIR/taiga/static mkdir $HSHQ_STACKS_DIR/taiga/media mkdir $HSHQ_STACKS_DIR/taiga/async_rmq mkdir $HSHQ_STACKS_DIR/taiga/events_rmq mkdir $HSHQ_STACKS_DIR/taiga/web mkdir $HSHQ_STACKS_DIR/taiga/db mkdir $HSHQ_STACKS_DIR/taiga/dbexport chmod 777 $HSHQ_STACKS_DIR/taiga/dbexport initServicesCredentials set +e addUserMailu alias $TAIGA_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS TAIGA_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $TAIGA_ADMIN_PASSWORD | tr -d ':\n') outputConfigTaiga installStack taiga taiga-back "Listening at: http://0.0.0.0:8000" $HOME/taiga.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$TAIGA_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_TAIGA_APP Admin Login Info" -b "$FMLNAME_TAIGA_APP Admin Username: $TAIGA_ADMIN_USERNAME\n$FMLNAME_TAIGA_APP Admin Password: $TAIGA_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" TAIGA_INIT_ENV=true updateConfigVar TAIGA_INIT_ENV $TAIGA_INIT_ENV fi sleep 3 set +e docker exec -it taiga-back bash -c "export DJANGO_SUPERUSER_PASSWORD=$TAIGA_ADMIN_PASSWORD;python manage.py createsuperuser --username=$TAIGA_ADMIN_USERNAME --email $TAIGA_ADMIN_EMAIL_ADDRESS --noinput" > /dev/null 2>&1 addReadOnlyUserToDatabase Taiga postgres taiga-db $TAIGA_DATABASE_NAME $TAIGA_DATABASE_USER $TAIGA_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/taiga/dbexport $TAIGA_DATABASE_READONLYUSER $TAIGA_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_TAIGA_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_TAIGA_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://taiga-gateway {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_TAIGA_APP $MANAGETLS_TAIGA_APP "$is_integrate_hshq" $NETDEFAULT_TAIGA_APP "$inner_block" insertSubAuthelia $SUB_TAIGA_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll taiga "$FMLNAME_TAIGA_APP" $USERTYPE_TAIGA_APP "https://$SUB_TAIGA_APP.$HOMESERVER_DOMAIN" "taiga.png" "$(getHeimdallOrderFromSub $SUB_TAIGA_APP $USERTYPE_TAIGA_APP)" restartAllCaddyContainers checkAddDBConnection true taiga "$FMLNAME_TAIGA_APP" postgres taiga-db $TAIGA_DATABASE_NAME $TAIGA_DATABASE_USER $TAIGA_DATABASE_USER_PASSWORD fi } function outputConfigTaiga() { cat < $HOME/taiga-compose.yml $STACK_VERSION_PREFIX taiga $(getScriptStackVersion taiga) services: taiga-db: image: $(getScriptImageByContainerName taiga-db) container_name: taiga-db hostname: taiga-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-taiga-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/taiga/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/taiga/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.taiga-hourly-db.schedule=@every 1h" - "ofelia.job-exec.taiga-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.taiga-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.taiga-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.taiga-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.taiga-hourly-db.email-from=Taiga Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.taiga-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.taiga-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.taiga-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.taiga-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.taiga-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.taiga-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.taiga-monthly-db.email-from=Taiga Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.taiga-monthly-db.mail-only-on-error=false" taiga-back: image: $(getScriptImageByContainerName taiga-back) container_name: taiga-back hostname: taiga-back restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - taiga-db - taiga-events-rabbitmq - taiga-async-rabbitmq networks: - int-taiga-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-taiga-static:/taiga-back/static - v-taiga-media:/taiga-back/media taiga-async: image: $(getScriptImageByContainerName taiga-async) container_name: taiga-async hostname: taiga-async restart: unless-stopped env_file: stack.env entrypoint: ["/taiga-back/docker/async_entrypoint.sh"] security_opt: - no-new-privileges:true depends_on: - taiga-db - taiga-events-rabbitmq - taiga-async-rabbitmq networks: - int-taiga-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-taiga-static:/taiga-back/static - v-taiga-media:/taiga-back/media taiga-async-rabbitmq: image: $(getScriptImageByContainerName taiga-async-rabbitmq) container_name: taiga-async-rabbitmq hostname: taiga-async-rabbitmq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-taiga-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-taiga-async-rabbitmq:/var/lib/rabbitmq taiga-front: image: $(getScriptImageByContainerName taiga-front) container_name: taiga-front hostname: taiga-front restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-taiga-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro taiga-events: image: $(getScriptImageByContainerName taiga-events) container_name: taiga-events hostname: taiga-events restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - taiga-events-rabbitmq networks: - int-taiga-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro taiga-events-rabbitmq: image: $(getScriptImageByContainerName taiga-events-rabbitmq) container_name: taiga-events-rabbitmq hostname: taiga-events-rabbitmq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-taiga-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-taiga-events-rabbitmq:/var/lib/rabbitmq taiga-protected: image: $(getScriptImageByContainerName taiga-protected) container_name: taiga-protected hostname: taiga-protected restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-taiga-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro taiga-gateway: image: $(getScriptImageByContainerName taiga-gateway) container_name: taiga-gateway hostname: taiga-gateway restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - taiga-front - taiga-back - taiga-events networks: - int-taiga-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/taiga/web/taiga.conf:/etc/nginx/conf.d/default.conf - v-taiga-static:/taiga-back/static - v-taiga-media:/taiga-back/media volumes: v-taiga-static: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/taiga/static v-taiga-media: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/taiga/media v-taiga-async-rabbitmq: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/taiga/async_rmq v-taiga-events-rabbitmq: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/taiga/events_rmq networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-taiga-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/taiga.env TZ=\${PORTAINER_TZ} POSTGRES_HOST=taiga-db POSTGRES_DB=$TAIGA_DATABASE_NAME POSTGRES_USER=$TAIGA_DATABASE_USER POSTGRES_PASSWORD=$TAIGA_DATABASE_USER_PASSWORD TAIGA_SITES_SCHEME=https TAIGA_SITES_DOMAIN=$SUB_TAIGA_APP.$HOMESERVER_DOMAIN TAIGA_URL=https://$SUB_TAIGA_APP.$HOMESERVER_DOMAIN TAIGA_SUBPATH= WEBSOCKETS_SCHEME=wss TAIGA_WEBSOCKETS_URL=wss://$SUB_TAIGA_APP.$HOMESERVER_DOMAIN TAIGA_SECRET_KEY=$TAIGA_SECRET_KEY SECRET_KEY=$TAIGA_SECRET_KEY EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend DEFAULT_FROM_EMAIL=Taiga $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS> EMAIL_USE_TLS=True EMAIL_USE_SSL=False EMAIL_HOST=$SMTP_HOSTNAME EMAIL_PORT=$SMTP_HOSTPORT EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= RABBITMQ_USER=$TAIGA_RABBITMQ_USERNAME RABBITMQ_PASS=$TAIGA_RABBITMQ_PASSWORD RABBITMQ_ERLANG_COOKIE=$TAIGA_RABBITMQ_ERLANG_COOKIE RABBITMQ_DEFAULT_USER=$TAIGA_RABBITMQ_USERNAME RABBITMQ_DEFAULT_PASS=$TAIGA_RABBITMQ_PASSWORD RABBITMQ_DEFAULT_VHOST=taiga ATTACHMENTS_MAX_AGE=360 MAX_AGE=360 ENABLE_TELEMETRY=False EOFMT cat < $HSHQ_STACKS_DIR/taiga/web/taiga.conf server { listen 80 default_server; client_max_body_size 100M; charset utf-8; # Frontend location / { proxy_pass http://taiga-front/; proxy_pass_header Server; proxy_set_header Host \$http_host; proxy_redirect off; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Scheme \$scheme; } # API location /api/ { proxy_pass http://taiga-back:8000/api/; proxy_pass_header Server; proxy_set_header Host \$http_host; proxy_redirect off; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Scheme \$scheme; } # Admin location /admin/ { proxy_pass http://taiga-back:8000/admin/; proxy_pass_header Server; proxy_set_header Host \$http_host; proxy_redirect off; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Scheme \$scheme; } # Static location /static/ { alias /taiga/static/; } # Media location /_protected/ { internal; alias /taiga/media/; add_header Content-disposition "attachment"; } # Unprotected section location /media/exports/ { alias /taiga/media/exports/; add_header Content-disposition "attachment"; } location /media/ { proxy_set_header Host \$http_host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Scheme \$scheme; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_pass http://taiga-protected:8003/; proxy_redirect off; } # Events location /events { proxy_pass http://taiga-events:8888/events; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; } } EOFNC } function performUpdateTaiga() { perform_stack_name=taiga prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/taigaio/taiga-back:6.9.0,mirror.gcr.io/taigaio/taiga-back:6.9.0,mirror.gcr.io/rabbitmq:4.1.4-management-alpine,mirror.gcr.io/taigaio/taiga-front:6.9.0,mirror.gcr.io/taigaio/taiga-events:6.9.0,mirror.gcr.io/rabbitmq:4.1.4-management-alpine,mirror.gcr.io/taigaio/taiga-protected:6.9.0,mirror.gcr.io/nginx:1.28.0-alpine image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/taigaio/taiga-back:6.9.0,mirror.gcr.io/taigaio/taiga-back:6.9.0" image_update_map[2]="mirror.gcr.io/taigaio/taiga-back:6.9.0,mirror.gcr.io/taigaio/taiga-back:6.9.0" image_update_map[3]="mirror.gcr.io/rabbitmq:4.1.4-management-alpine,mirror.gcr.io/rabbitmq:4.1.4-management-alpine" image_update_map[4]="mirror.gcr.io/taigaio/taiga-front:6.9.0,mirror.gcr.io/taigaio/taiga-front:6.9.0" image_update_map[5]="mirror.gcr.io/taigaio/taiga-events:6.9.0,mirror.gcr.io/taigaio/taiga-events:6.9.0" image_update_map[6]="mirror.gcr.io/rabbitmq:4.1.4-management-alpine,mirror.gcr.io/rabbitmq:4.1.4-management-alpine" image_update_map[7]="mirror.gcr.io/taigaio/taiga-protected:6.9.0,mirror.gcr.io/taigaio/taiga-protected:6.9.0" image_update_map[8]="mirror.gcr.io/nginx:1.28.0-alpine,mirror.gcr.io/nginx:1.28.0-alpine" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # OpenSign function installOpenSign() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory opensign "OpenSign" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName opensign-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName opensign-server) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName opensign-client) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/opensign mkdir $HSHQ_STACKS_DIR/opensign/files mkdir $HSHQ_STACKS_DIR/opensign/db mkdir $HSHQ_STACKS_DIR/opensign/dbexport initServicesCredentials set +e addUserMailu alias $OPENSIGN_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS OPENSIGN_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $OPENSIGN_ADMIN_PASSWORD | tr -d ':\n') outputConfigOpenSign installStack opensign opensign-server "opensign-server running on port" $HOME/opensign.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$OPENSIGN_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_OPENSIGN_APP Admin Login Info" -b "Below are some generated credentials that you can use to configure $FMLNAME_OPENSIGN_APP. You will still need to go through the initial onboarding wizard the first time you access the site, and enter the information manually.\n\n$FMLNAME_OPENSIGN_APP Admin Email: $OPENSIGN_ADMIN_EMAIL_ADDRESS\n$FMLNAME_OPENSIGN_APP Admin Password: $OPENSIGN_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" OPENSIGN_INIT_ENV=true updateConfigVar OPENSIGN_INIT_ENV $OPENSIGN_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase OpenSign mongodb opensign-db $OPENSIGN_DATABASE_NAME $OPENSIGN_DATABASE_USER $OPENSIGN_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/opensign/dbexport $OPENSIGN_DATABASE_READONLYUSER $OPENSIGN_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_OPENSIGN_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>handle_path /api/* {\n" inner_block=$inner_block">>>>>>>>rewrite * {uri}\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://opensign-server:8080 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>reverse_proxy http://opensign-client:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENSIGN_APP $MANAGETLS_OPENSIGN_APP "$is_integrate_hshq" $NETDEFAULT_OPENSIGN_APP "$inner_block" insertSubAuthelia $SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll opensign "$FMLNAME_OPENSIGN_APP" $USERTYPE_OPENSIGN_APP "https://$SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN" "opensign.png" "$(getHeimdallOrderFromSub $SUB_OPENSIGN_APP $USERTYPE_OPENSIGN_APP)" restartAllCaddyContainers checkAddDBConnection true opensign "$FMLNAME_OPENSIGN_APP" mongo opensign-db $OPENSIGN_DATABASE_NAME $OPENSIGN_DATABASE_USER $OPENSIGN_DATABASE_USER_PASSWORD fi } function outputConfigOpenSign() { cat < $HOME/opensign-compose.yml $STACK_VERSION_PREFIX opensign $(getScriptStackVersion opensign) services: opensign-db: image: $(getScriptImageByContainerName opensign-db) container_name: opensign-db hostname: opensign-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mongod --oplogSize 128 --quiet --auth networks: - int-opensign-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-opensign-db:/data/db - v-opensign-dbexport:/dump opensign-server: image: $(getScriptImageByContainerName opensign-server) container_name: opensign-server hostname: opensign-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - opensign-db networks: - int-opensign-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-opensign-files:/usr/src/app/files opensign-client: image: $(getScriptImageByContainerName opensign-client) container_name: opensign-client hostname: opensign-client restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - opensign-db networks: - int-opensign-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro volumes: v-opensign-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/opensign/db v-opensign-dbexport: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/opensign/dbexport v-opensign-files: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/opensign/files networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-opensign-net: driver: bridge internal: true ipam: driver: default EOFMT openssl genrsa -des3 -passout pass:$OPENSIGN_CERTIFICATE_PASS_PHRASE -out $HOME/opensign.key 2048 openssl req -key $HOME/opensign.key -passin pass:$OPENSIGN_CERTIFICATE_PASS_PHRASE -new -x509 -days 365 -out $HOME/opensign.crt -subj "/C=$CERTS_INTERNAL_COUNTRY/ST=$CERTS_INTERNAL_STATE/L=$CERTS_INTERNAL_LOCALITY/O=$CERTS_INTERNAL_ORG_NAME/OU=$CERTS_INTERNAL_OU_NAME/CN=$HOMESERVER_DOMAIN" openssl pkcs12 -inkey $HOME/opensign.key -passin pass:$OPENSIGN_CERTIFICATE_PASS_PHRASE -passout pass:$OPENSIGN_CERTIFICATE_PASS_PHRASE -in $HOME/opensign.crt -export -out $HOME/opensign.pfx openssl base64 -in $HOME/opensign.pfx -out $HOME/os_pfx_file rm -f $HOME/opensign.key rm -f $HOME/opensign.crt rm -f $HOME/opensign.pfx cat < $HOME/opensign.env TZ=\${PORTAINER_TZ} NODE_ENV=production NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt MONGO_INITDB_DATABASE=$OPENSIGN_DATABASE_NAME MONGO_INITDB_ROOT_USERNAME=$OPENSIGN_DATABASE_USER MONGO_INITDB_ROOT_PASSWORD=$OPENSIGN_DATABASE_USER_PASSWORD SERVER_URL=https://$SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN/api/app PUBLIC_URL=https://$SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN GENERATE_SOURCEMAP=false REACT_APP_SERVERURL=https://$SUB_OPENSIGN_APP.$HOMESERVER_DOMAIN/api/app appName=OpenSign MASTER_KEY=$OPENSIGN_MASTER_KEY MONGODB_URI=mongodb://$OPENSIGN_DATABASE_USER:$OPENSIGN_DATABASE_USER_PASSWORD@opensign-db:27017/${OPENSIGN_DATABASE_NAME}?authSource=admin USE_LOCAL=true SMTP_ENABLE=true SMTP_HOST=$SMTP_HOSTNAME SMTP_PORT=$SMTP_HOSTPORT SMTP_USER_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS PASS_PHRASE=$OPENSIGN_CERTIFICATE_PASS_PHRASE PFX_BASE64='$(cat $HOME/os_pfx_file | tr -d "\r\n")' EOFMT rm -f $HOME/os_pfx_file } function performUpdateOpenSign() { perform_stack_name=opensign prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/opensign/opensignserver:main,mirror.gcr.io/opensign/opensign:main image_update_map[0]="mirror.gcr.io/mongo:8.0.13,mirror.gcr.io/mongo:8.0.13" image_update_map[1]="mirror.gcr.io/opensign/opensignserver:main,mirror.gcr.io/opensign/opensignserver:main" image_update_map[2]="mirror.gcr.io/opensign/opensign:main,mirror.gcr.io/opensign/opensign:main" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # DocuSeal function installDocuSeal() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory docuseal "DocuSeal" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName docuseal-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName docuseal-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/docuseal mkdir $HSHQ_STACKS_DIR/docuseal/config mkdir $HSHQ_STACKS_DIR/docuseal/data mkdir $HSHQ_STACKS_DIR/docuseal/db mkdir $HSHQ_STACKS_DIR/docuseal/dbexport chmod 777 $HSHQ_STACKS_DIR/docuseal/dbexport initServicesCredentials set +e addUserMailu alias $DOCUSEAL_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS DOCUSEAL_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $DOCUSEAL_ADMIN_PASSWORD | tr -d ':\n') outputConfigDocuSeal installStack docuseal docuseal-app "Listening on" $HOME/docuseal.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$DOCUSEAL_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_DOCUSEAL_APP Admin Login Info" -b "Below are some generated credentials that you can use to configure $FMLNAME_DOCUSEAL_APP. You will still need to go through the initial onboarding wizard the first time you access the site, and enter the information manually.\n\n$FMLNAME_DOCUSEAL_APP Admin Email: $DOCUSEAL_ADMIN_EMAIL_ADDRESS\n$FMLNAME_DOCUSEAL_APP Admin Password: $DOCUSEAL_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" DOCUSEAL_INIT_ENV=true updateConfigVar DOCUSEAL_INIT_ENV $DOCUSEAL_INIT_ENV fi sleep 3 performOnboardingDocuSeal addReadOnlyUserToDatabase DocuSeal postgres docuseal-db $DOCUSEAL_DATABASE_NAME $DOCUSEAL_DATABASE_USER $DOCUSEAL_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/docuseal/dbexport $DOCUSEAL_DATABASE_READONLYUSER $DOCUSEAL_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_DOCUSEAL_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://docuseal-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_DOCUSEAL_APP $MANAGETLS_DOCUSEAL_APP "$is_integrate_hshq" $NETDEFAULT_DOCUSEAL_APP "$inner_block" insertSubAuthelia $SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll docuseal "$FMLNAME_DOCUSEAL_APP" $USERTYPE_DOCUSEAL_APP "https://$SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN" "docuseal.png" "$(getHeimdallOrderFromSub $SUB_DOCUSEAL_APP $USERTYPE_DOCUSEAL_APP)" restartAllCaddyContainers checkAddDBConnection true docuseal "$FMLNAME_DOCUSEAL_APP" postgres docuseal-db $DOCUSEAL_DATABASE_NAME $DOCUSEAL_DATABASE_USER $DOCUSEAL_DATABASE_USER_PASSWORD fi } function outputConfigDocuSeal() { cat < $HOME/docuseal-compose.yml $STACK_VERSION_PREFIX docuseal $(getScriptStackVersion docuseal) services: docuseal-db: image: $(getScriptImageByContainerName docuseal-db) container_name: docuseal-db hostname: docuseal-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-docuseal-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/docuseal/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/docuseal/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.docuseal-hourly-db.schedule=@every 1h" - "ofelia.job-exec.docuseal-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.docuseal-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.docuseal-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.docuseal-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.docuseal-hourly-db.email-from=DocuSeal Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.docuseal-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.docuseal-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.docuseal-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.docuseal-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.docuseal-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.docuseal-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.docuseal-monthly-db.email-from=DocuSeal Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.docuseal-monthly-db.mail-only-on-error=false" docuseal-app: image: $(getScriptImageByContainerName docuseal-app) container_name: docuseal-app hostname: docuseal-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - docuseal-db networks: - int-docuseal-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/docuseal/data:/data/docuseal networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-docuseal-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/docuseal.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$DOCUSEAL_DATABASE_NAME POSTGRES_USER=$DOCUSEAL_DATABASE_USER POSTGRES_PASSWORD=$DOCUSEAL_DATABASE_USER_PASSWORD FORCE_SSL=$SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN DATABASE_URL=postgresql://$DOCUSEAL_DATABASE_USER:$DOCUSEAL_DATABASE_USER_PASSWORD@docuseal-db:5432/$DOCUSEAL_DATABASE_NAME EOFMT } function performOnboardingDocuSeal() { docker exec -i \ -e DOCUSEAL_FIRST_NAME="HSHQ" \ -e DOCUSEAL_LAST_NAME="Admin" \ -e DOCUSEAL_EMAIL="$DOCUSEAL_ADMIN_EMAIL_ADDRESS" \ -e DOCUSEAL_PASSWORD="$DOCUSEAL_ADMIN_PASSWORD" \ -e DOCUSEAL_COMPANY="$HOMESERVER_NAME" \ -e DOCUSEAL_URL="https://$SUB_DOCUSEAL_APP.$HOMESERVER_DOMAIN" \ -e DOCUSEAL_TIMEZONE="$TZ" \ -e DOCUSEAL_LOCALE="en-US" \ -e SMTP_HOSTNAME="$SMTP_HOSTNAME" \ -e SMTP_HOSTPORT="$SMTP_HOSTPORT" \ -e EMAIL_ADMIN_EMAIL_ADDRESS="$EMAIL_ADMIN_EMAIL_ADDRESS" \ docuseal-app sh -c 'cd /app && exec bin/rails runner -' 2>&1 <<'RUBY' # frozen_string_literal: true exit 0 if User.exists? app_url = ENV.fetch('DOCUSEAL_URL') unless URI.parse(app_url).class.in?([URI::HTTP, URI::HTTPS]) abort "Invalid app URL: #{app_url}" end account = Account.new( name: ENV.fetch('DOCUSEAL_COMPANY'), timezone: Accounts.normalize_timezone(ENV.fetch('DOCUSEAL_TIMEZONE', 'UTC')), locale: ENV.fetch('DOCUSEAL_LOCALE', 'en-US') ) user = account.users.new( first_name: ENV.fetch('DOCUSEAL_FIRST_NAME'), last_name: ENV.fetch('DOCUSEAL_LAST_NAME'), email: ENV.fetch('DOCUSEAL_EMAIL'), password: ENV.fetch('DOCUSEAL_PASSWORD') ) abort "Invalid account: #{account.errors.full_messages.join(', ')}" unless account.valid? unless user.save abort "User save failed: #{user.errors.full_messages.join(', ')}" end account.encrypted_configs.create!([ { key: EncryptedConfig::APP_URL_KEY, value: app_url }, { key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }, { key: EncryptedConfig::EMAIL_SMTP_KEY, value: { 'host' => ENV.fetch('SMTP_HOSTNAME'), 'port' => ENV.fetch('SMTP_HOSTPORT'), 'from_email' => ENV.fetch('EMAIL_ADMIN_EMAIL_ADDRESS'), 'security' => 'none', 'authentication' => 'plain' } } ]) account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists? Docuseal.refresh_default_url_options! puts "DocuSeal onboarded: #{user.email} (#{account.name})" RUBY sleep 3 docker container restart docuseal-app > /dev/null 2>&1 echo "DocuSeal onboarding wizard complete!" } function performUpdateDocuSeal() { perform_stack_name=docuseal prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/docuseal/docuseal:2.1.8 image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/docuseal/docuseal:2.1.8,mirror.gcr.io/docuseal/docuseal:2.2.4" ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/docuseal/docuseal:2.2.4 image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6" image_update_map[1]="mirror.gcr.io/docuseal/docuseal:2.2.4,mirror.gcr.io/docuseal/docuseal:2.2.4" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # ControlR function installControlR() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory controlr "ControlR" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName controlr-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName controlr-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName controlr-aspire) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/controlr mkdir $HSHQ_STACKS_DIR/controlr/db mkdir $HSHQ_STACKS_DIR/controlr/dbexport chmod 777 $HSHQ_STACKS_DIR/controlr/dbexport initServicesCredentials set +e addUserMailu alias $CONTROLR_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS CONTROLR_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $CONTROLR_ADMIN_PASSWORD | tr -d ':\n') outputConfigControlR installStack controlr controlr-app "Adding KnownNetwork" $HOME/controlr.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$CONTROLR_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_CONTROLR_APP Admin Login Info" -b "Below are some generated credentials that you can use to configure $FMLNAME_CONTROLR_APP. You will still need to go through the initial onboarding wizard the first time you access the site, and enter the information manually.\n\n$FMLNAME_CONTROLR_APP Admin Email: $CONTROLR_ADMIN_EMAIL_ADDRESS\n$FMLNAME_CONTROLR_APP Admin Password: $CONTROLR_ADMIN_PASSWORD\n\n$FMLNAME_CONTROLR_ASPIRE Token: $CONTROLR_ASPIRE_TOKEN" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" CONTROLR_INIT_ENV=true updateConfigVar CONTROLR_INIT_ENV $CONTROLR_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase ControlR postgres controlr-db $CONTROLR_DATABASE_NAME $CONTROLR_DATABASE_USER $CONTROLR_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/controlr/dbexport $CONTROLR_DATABASE_READONLYUSER $CONTROLR_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_CONTROLR_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_CONTROLR_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://controlr-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CONTROLR_APP $MANAGETLS_CONTROLR_APP "$is_integrate_hshq" $NETDEFAULT_CONTROLR_APP "$inner_block" insertSubAuthelia $SUB_CONTROLR_APP.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_CONTROLR_ASPIRE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://controlr-aspire:18888 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CONTROLR_ASPIRE $MANAGETLS_CONTROLR_ASPIRE "$is_integrate_hshq" $NETDEFAULT_CONTROLR_ASPIRE "$inner_block" insertSubAuthelia $SUB_CONTROLR_ASPIRE.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll controlr "$FMLNAME_CONTROLR_APP" $USERTYPE_CONTROLR_APP "https://$SUB_CONTROLR_APP.$HOMESERVER_DOMAIN" "controlr.png" "$(getHeimdallOrderFromSub $SUB_CONTROLR_APP $USERTYPE_CONTROLR_APP)" insertEnableSvcHeimdall controlr "$FMLNAME_CONTROLR_ASPIRE" $USERTYPE_CONTROLR_ASPIRE "https://$SUB_CONTROLR_ASPIRE.$HOMESERVER_DOMAIN" "controlr.png" true "$(getHeimdallOrderFromSub $SUB_CONTROLR_ASPIRE $USERTYPE_CONTROLR_ASPIRE)" restartAllCaddyContainers checkAddDBConnection true controlr "$FMLNAME_CONTROLR_APP" postgres controlr-db $CONTROLR_DATABASE_NAME $CONTROLR_DATABASE_USER $CONTROLR_DATABASE_USER_PASSWORD fi } function outputConfigControlR() { cat < $HOME/controlr-compose.yml $STACK_VERSION_PREFIX controlr $(getScriptStackVersion controlr) services: controlr-db: image: $(getScriptImageByContainerName controlr-db) container_name: controlr-db hostname: controlr-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-controlr-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/controlr/db:/var/lib/postgresql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/controlr/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.controlr-hourly-db.schedule=@every 1h" - "ofelia.job-exec.controlr-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.controlr-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.controlr-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.controlr-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.controlr-hourly-db.email-from=ControlR Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.controlr-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.controlr-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.controlr-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.controlr-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.controlr-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.controlr-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.controlr-monthly-db.email-from=ControlR Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.controlr-monthly-db.mail-only-on-error=false" controlr-app: image: $(getScriptImageByContainerName controlr-app) container_name: controlr-app hostname: controlr-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - controlr-db - controlr-aspire networks: - int-controlr-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro controlr-aspire: image: $(getScriptImageByContainerName controlr-aspire) container_name: controlr-aspire hostname: controlr-aspire restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - controlr-db networks: - int-controlr-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-controlr-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/controlr.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$CONTROLR_DATABASE_NAME POSTGRES_USER=$CONTROLR_DATABASE_USER POSTGRES_PASSWORD=$CONTROLR_DATABASE_USER_PASSWORD ASPNETCORE_ENVIRONMENT=Production ASPNETCORE_HTTP_PORTS=8080 ControlR_POSTGRES_USER=$CONTROLR_DATABASE_USER ControlR_POSTGRES_PASSWORD=$CONTROLR_DATABASE_USER_PASSWORD ControlR_POSTGRES_HOST=controlr-db ControlR_AppOptions__EnablePublicRegistration=false ControlR_AppOptions__AllowAgentsToSelfBootstrap=false ControlR_AppOptions__EnableCloudflareProxySupport=false ControlR_OTLP_ENDPOINT_URL=http://controlr-aspire:18889 ControlR_Logging__LogLevel__Default=Information ControlR_AppOptions__UseHttpLogging=false ControlR_AppOptions__DockerGatewayIp=$NET_WEBPROXY_SUBNET_PREFIX.1 ControlR_AppOptions__MaxFileTransferSize=104857600 ControlR_AppOptions__RequireUserEmailConfirmation=true ControlR_AppOptions__SmtpDisplayName=ControlR $(getAdminEmailName) ControlR_AppOptions__SmtpEmail=$EMAIL_ADMIN_EMAIL_ADDRESS ControlR_AppOptions__SmtpHost=$SMTP_HOSTNAME ControlR_AppOptions__SmtpPort=$SMTP_HOSTPORT ControlR_AppOptions__SmtpLocalDomain= ControlR_AppOptions__SmtpCheckCertificateRevocation=false ControlR_AppOptions__SmtpUserName= ControlR_AppOptions__SmtpPassword= ControlR_AppOptions__KnownNetworks__0=$DOCKER_NETWORK_RESERVED_RANGE ControlR_AppOptions__KnownNetworks__1=10.0.0.0/8 ControlR_AppOptions__KnownNetworks__2=192.168.0.0/16 ControlR_AppOptions__UseExternalWebSocketRelay=false DOTNET_DASHBOARD_OTLP_ENDPOINT_URL=http://controlr-aspire:18889 Dashboard__Frontend__BrowserToken=$CONTROLR_ASPIRE_TOKEN DASHBOARD__OTLP__AUTHMODE=ApiKey DASHBOARD__OTLP__PRIMARYAPIKEY=$CONTROLR_ASPIRE_APIKEY EOFMT } function performUpdateControlR() { perform_stack_name=controlr prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:18.0-bookworm,mirror.gcr.io/translucency/controlr:0.14.88.0,mcr.microsoft.com/dotnet/aspire-dashboard:9.2 image_update_map[0]="mirror.gcr.io/postgres:18.0-bookworm,mirror.gcr.io/postgres:18.0-bookworm" image_update_map[1]="mirror.gcr.io/translucency/controlr:0.14.88.0,mirror.gcr.io/translucency/controlr:0.16.0.0" image_update_map[2]="mcr.microsoft.com/dotnet/aspire-dashboard:9.2,mcr.microsoft.com/dotnet/aspire-dashboard:9.2" ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:18.0-bookworm,mirror.gcr.io/translucency/controlr:0.16.0.0,mcr.microsoft.com/dotnet/aspire-dashboard:9.2 image_update_map[0]="mirror.gcr.io/postgres:18.0-bookworm,mirror.gcr.io/postgres:18.0-bookworm" image_update_map[1]="mirror.gcr.io/translucency/controlr:0.16.0.0,mirror.gcr.io/translucency/controlr:0.16.0.0" image_update_map[2]="mcr.microsoft.com/dotnet/aspire-dashboard:9.2,mcr.microsoft.com/dotnet/aspire-dashboard:9.2" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfControlRV230Fix() { sed -i "s/ControlR_AppOptions__DockerGatewayIp=.*/ControlR_AppOptions__DockerGatewayIp=$NET_WEBPROXY_SUBNET_PREFIX\.1/" $HOME/controlr.env } # Akaunting function installAkaunting() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory akaunting "Akaunting" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName akaunting-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName akaunting-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/akaunting mkdir $HSHQ_STACKS_DIR/akaunting/data mkdir $HSHQ_STACKS_DIR/akaunting/db mkdir $HSHQ_STACKS_DIR/akaunting/dbexport chmod 777 $HSHQ_STACKS_DIR/akaunting/dbexport initServicesCredentials set +e addUserMailu alias $AKAUNTING_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigAkaunting installStack akaunting akaunting-app "" $HOME/akaunting.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$AKAUNTING_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_AKAUNTING_APP Admin Login Info" -b "Below are some generated credentials that you can use to configure $FMLNAME_AKAUNTING_APP. You will still need to go through the initial onboarding wizard the first time you access the site, and enter the information manually.\n\n$FMLNAME_AKAUNTING_APP Admin Email: $AKAUNTING_ADMIN_EMAIL_ADDRESS\n$FMLNAME_AKAUNTING_APP Admin Password: $AKAUNTING_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" AKAUNTING_INIT_ENV=true updateConfigVar AKAUNTING_INIT_ENV $AKAUNTING_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase Akaunting mysql akaunting-db $AKAUNTING_DATABASE_NAME root $AKAUNTING_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/akaunting/dbexport $AKAUNTING_DATABASE_READONLYUSER $AKAUNTING_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_AKAUNTING_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_AKAUNTING_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://akaunting-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AKAUNTING_APP $MANAGETLS_AKAUNTING_APP "$is_integrate_hshq" $NETDEFAULT_AKAUNTING_APP "$inner_block" insertSubAuthelia $SUB_AKAUNTING_APP.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll akaunting "$FMLNAME_AKAUNTING_APP" $USERTYPE_AKAUNTING_APP "https://$SUB_AKAUNTING_APP.$HOMESERVER_DOMAIN" "akaunting.png" "$(getHeimdallOrderFromSub $SUB_AKAUNTING_APP $USERTYPE_AKAUNTING_APP)" restartAllCaddyContainers checkAddDBConnection true akaunting "$FMLNAME_AKAUNTING_APP" mysql akaunting-db $AKAUNTING_DATABASE_NAME $AKAUNTING_DATABASE_USER $AKAUNTING_DATABASE_USER_PASSWORD fi } function outputConfigAkaunting() { cat < $HOME/akaunting-compose.yml $STACK_VERSION_PREFIX akaunting $(getScriptStackVersion akaunting) services: akaunting-db: image: $(getScriptImageByContainerName akaunting-db) container_name: akaunting-db hostname: akaunting-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mysqld --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 networks: - int-akaunting-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-akaunting-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMySQL.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/akaunting/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.akaunting-hourly-db.schedule=@every 1h" - "ofelia.job-exec.akaunting-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.akaunting-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.akaunting-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.akaunting-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.akaunting-hourly-db.email-from=Akaunting Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.akaunting-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.akaunting-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.akaunting-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.akaunting-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.akaunting-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.akaunting-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.akaunting-monthly-db.email-from=Akaunting Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.akaunting-monthly-db.mail-only-on-error=false" akaunting-app: image: $(getScriptImageByContainerName akaunting-app) container_name: akaunting-app hostname: akaunting-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - akaunting-db networks: - int-akaunting-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-akaunting-data:/var/www/html:z volumes: v-akaunting-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/akaunting/db v-akaunting-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/akaunting/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-akaunting-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/akaunting.env TZ=\${PORTAINER_TZ} MYSQL_DATABASE=$AKAUNTING_DATABASE_NAME MYSQL_ROOT_PASSWORD=$AKAUNTING_DATABASE_ROOT_PASSWORD MYSQL_USER=$AKAUNTING_DATABASE_USER MYSQL_PASSWORD=$AKAUNTING_DATABASE_USER_PASSWORD APP_URL=https://$SUB_AKAUNTING_APP.$HOMESERVER_DOMAIN LOCALE=en-US DB_HOST=akaunting-db DB_PORT=3306 DB_NAME=$AKAUNTING_DATABASE_NAME DB_USERNAME=$AKAUNTING_DATABASE_USER DB_PASSWORD=$AKAUNTING_DATABASE_USER_PASSWORD DB_PREFIX=akt_ COMPANY_NAME=$HOMESERVER_NAME COMPANY_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS ADMIN_EMAIL=$AKAUNTING_ADMIN_EMAIL_ADDRESS ADMIN_PASSWORD=$AKAUNTING_ADMIN_PASSWORD AKAUNTING_SETUP=true EOFMT } function performUpdateAkaunting() { perform_stack_name=akaunting prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/akaunting/akaunting:3.1.19 image_update_map[0]="mirror.gcr.io/mariadb:12.0.2-ubi10,mirror.gcr.io/mariadb:12.0.2-ubi10" image_update_map[1]="mirror.gcr.io/akaunting/akaunting:3.1.19,mirror.gcr.io/akaunting/akaunting:3.1.19" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Axelor function installAxelor() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory axelor "Axelor" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName axelor-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName axelor-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName axelor-goovee) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/axelor mkdir $HSHQ_STACKS_DIR/axelor/data mkdir $HSHQ_STACKS_DIR/axelor/db mkdir $HSHQ_STACKS_DIR/axelor/dbexport chmod 777 $HSHQ_STACKS_DIR/axelor/dbexport initServicesCredentials set +e addUserMailu alias $AXELOR_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigAxelor installStack axelor axelor-app "" $HOME/axelor.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$AXELOR_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_AXELOR_APP Admin Login Info" -b "$FMLNAME_AXELOR_APP Admin Username: $AXELOR_ADMIN_USERNAME\n$FMLNAME_AXELOR_APP Admin Password: $AXELOR_ADMIN_PASSWORD\n\n$FMLNAME_AXELOR_GOOVEE Admin Username: $AXELOR_GOOVEE_ADMIN_USERNAME\n$FMLNAME_AXELOR_GOOVEE Admin Password: $AXELOR_GOOVEE_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" AXELOR_INIT_ENV=true updateConfigVar AXELOR_INIT_ENV $AXELOR_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase Axelor postgres axelor-db $AXELOR_DATABASE_NAME $AXELOR_DATABASE_USER $AXELOR_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/axelor/dbexport $AXELOR_DATABASE_READONLYUSER $AXELOR_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_AXELOR_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_AXELOR_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://axelor-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AXELOR_APP $MANAGETLS_AXELOR_APP "$is_integrate_hshq" $NETDEFAULT_AXELOR_APP "$inner_block" insertSubAuthelia $SUB_AXELOR_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://axelor-goovee:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_AXELOR_GOOVEE $MANAGETLS_AXELOR_GOOVEE "$is_integrate_hshq" $NETDEFAULT_AXELOR_GOOVEE "$inner_block" insertSubAuthelia $SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll axelor "$FMLNAME_AXELOR_APP" $USERTYPE_AXELOR_APP "https://$SUB_AXELOR_APP.$HOMESERVER_DOMAIN" "axelor.png" "$(getHeimdallOrderFromSub $SUB_AXELOR_APP $USERTYPE_AXELOR_APP)" insertEnableSvcAll axelor "$FMLNAME_AXELOR_GOOVEE" $USERTYPE_AXELOR_GOOVEE "https://$SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN" "goovee.png" "$(getHeimdallOrderFromSub $SUB_AXELOR_GOOVEE $USERTYPE_AXELOR_GOOVEE)" restartAllCaddyContainers checkAddDBConnection true axelor "$FMLNAME_AXELOR_APP" postgres axelor-db $AXELOR_DATABASE_NAME $AXELOR_DATABASE_USER $AXELOR_DATABASE_USER_PASSWORD fi } function outputConfigAxelor() { cat < $HOME/axelor-compose.yml $STACK_VERSION_PREFIX axelor $(getScriptStackVersion axelor) services: axelor-db: image: $(getScriptImageByContainerName axelor-db) container_name: axelor-db hostname: axelor-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-axelor-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/axelor/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/axelor/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.axelor-hourly-db.schedule=@every 1h" - "ofelia.job-exec.axelor-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.axelor-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.axelor-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.axelor-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.axelor-hourly-db.email-from=Axelor Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.axelor-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.axelor-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.axelor-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.axelor-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.axelor-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.axelor-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.axelor-monthly-db.email-from=Axelor Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.axelor-monthly-db.mail-only-on-error=false" axelor-app: image: $(getScriptImageByContainerName axelor-app) container_name: axelor-app hostname: axelor-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - axelor-db networks: - int-axelor-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-axelor-data:/data:z axelor-goovee: image: $(getScriptImageByContainerName axelor-goovee) container_name: axelor-goovee hostname: axelor-goovee restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - axelor-app networks: - int-axelor-net - dock-ext-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-axelor-data:/data:z volumes: v-axelor-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/axelor/data networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-axelor-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/axelor.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$AXELOR_DATABASE_NAME POSTGRES_USER=$AXELOR_DATABASE_USER POSTGRES_PASSWORD=$AXELOR_DATABASE_USER_PASSWORD APP_USER=$AXELOR_ADMIN_USERNAME APP_PASS=$AXELOR_ADMIN_PASSWORD APP_DEMO_DATA=$IS_HSHQ_DEV_TEST APP_LOAD_APPS= APP_MODE=prod LOG_LEVEL=debug ENABLE_QUARTZ=false PGHOST=axelor-db PGPORT=5432 PGUSER=$AXELOR_DATABASE_USER PGPASSWORD=$AXELOR_DATABASE_USER_PASSWORD PGDATABASE=$AXELOR_DATABASE_NAME DATA_STORAGE=/data/attachments MULTI_TENANCY=false INCLUDE_LANGUAGE=true GOOVEE_PUBLIC_HOST=https://$SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN GOOVEE_PUBLIC_AOS_URL=http://axelor-app:8080 NEXTAUTH_URL=https://$SUB_AXELOR_GOOVEE.$HOMESERVER_DOMAIN NEXTAUTH_SECRET=$AXELOR_NEXTAUTH_SECRET BASIC_AUTH_USERNAME=$AXELOR_GOOVEE_ADMIN_USERNAME BASIC_AUTH_PASSWORD=$AXELOR_GOOVEE_ADMIN_PASSWORD MAIL_HOST=$SMTP_HOSTNAME MAIL_PORT=$SMTP_HOSTPORT MAIL_SECURE=true MAIL_USER= MAIL_PASSWORD= MAIL_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS JAVA_XMX=4096m EOFMT } function performUpdateAxelor() { perform_stack_name=axelor prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/axelor/aos-ce:8.4,mirror.gcr.io/axelor/goovee-ce:v1.1.0 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/axelor/aos-ce:8.4,mirror.gcr.io/axelor/axelor/aos-ce:8.4" image_update_map[2]="mirror.gcr.io/axelor/goovee-ce:v1.1.0,mirror.gcr.io/axelor/goovee-ce:v1.1.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # ConvertX function installConvertX() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory convertx "ConvertX" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName convertx-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/convertx mkdir $HSHQ_STACKS_DIR/convertx/data initServicesCredentials set +e addUserMailu alias $CONVERTX_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS CONVERTX_ADMIN_PASSWORD_HASH=$(echo -n "$CONVERTX_ADMIN_PASSWORD" | argon2 $(pwgen -c -n 32 1) -e -id -m 16 -t 2) outputConfigConvertX installStack convertx convertx-app "Elysia is running at" $HOME/convertx.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$CONVERTX_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_CONVERTX_APP Admin Login Info" -b "$FMLNAME_CONVERTX_APP Admin Username: $CONVERTX_ADMIN_EMAIL_ADDRESS\n$FMLNAME_CONVERTX_APP Admin Password: $CONVERTX_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" CONVERTX_INIT_ENV=true updateConfigVar CONVERTX_INIT_ENV $CONVERTX_INIT_ENV fi sleep 3 sudo sqlite3 $HSHQ_STACKS_DIR/convertx/data/mydb.sqlite "insert into users(email,password) values('$CONVERTX_ADMIN_EMAIL_ADDRESS','$CONVERTX_ADMIN_PASSWORD_HASH');" echo "Restarting convertx container..." docker container restart convertx-app > /dev/null 2>&1 echo "Container restarted, continuing..." if [ -z "$FMLNAME_CONVERTX_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_CONVERTX_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://convertx-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CONVERTX_APP $MANAGETLS_CONVERTX_APP "$is_integrate_hshq" $NETDEFAULT_CONVERTX_APP "$inner_block" insertSubAuthelia $SUB_CONVERTX_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll convertx "$FMLNAME_CONVERTX_APP" $USERTYPE_CONVERTX_APP "https://$SUB_CONVERTX_APP.$HOMESERVER_DOMAIN" "convertx.png" "$(getHeimdallOrderFromSub $SUB_CONVERTX_APP $USERTYPE_CONVERTX_APP)" restartAllCaddyContainers fi } function outputConfigConvertX() { cat < $HOME/convertx-compose.yml $STACK_VERSION_PREFIX convertx $(getScriptStackVersion convertx) services: convertx-app: image: $(getScriptImageByContainerName convertx-app) container_name: convertx-app hostname: convertx-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/convertx/data:/app/data networks: dock-proxy-net: name: dock-proxy external: true EOFMT cat < $HOME/convertx.env TZ=\${PORTAINER_TZ} JWT_SECRET=$CONVERTX_JWT_SECRET HTTP_ALLOWED=true EOFMT } function performUpdateConvertX() { perform_stack_name=convertx prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=ghcr.io/c4illin/convertx:v0.15.1 image_update_map[0]="ghcr.io/c4illin/convertx:v0.15.1,ghcr.io/c4illin/convertx:v0.16.0" ;; 2) newVer=v2 curImageList=ghcr.io/c4illin/convertx:v0.16.0 image_update_map[0]="ghcr.io/c4illin/convertx:v0.16.0,ghcr.io/c4illin/convertx:v0.16.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Kopia function installKopia() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory kopia "Kopia" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName kopia-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/kopia mkdir $HSHQ_STACKS_DIR/kopia/config mkdir $HSHQ_STACKS_DIR/kopia/cache mkdir $HSHQ_STACKS_DIR/kopia/logs mkdir $HSHQ_NONBACKUP_DIR/kopia mkdir $HSHQ_NONBACKUP_DIR/kopia/snapshots sudo mkdir -p $HSHQ_BACKUP_DIR/kopia initServicesCredentials set +e generateCert kopia-app kopia-app outputConfigKopia installStack kopia kopia-app "SERVER ADDRESS" $HOME/kopia.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$KOPIA_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_KOPIA_APP Admin Login Info" -b "$FMLNAME_KOPIA_APP Admin Username: $KOPIA_ADMIN_USERNAME\n$FMLNAME_KOPIA_APP Admin Password: $KOPIA_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" KOPIA_INIT_ENV=true updateConfigVar KOPIA_INIT_ENV $KOPIA_INIT_ENV fi sleep 3 if [ -z "$FMLNAME_KOPIA_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_KOPIA_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy https://kopia-app:51515 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KOPIA_APP $MANAGETLS_KOPIA_APP "$is_integrate_hshq" $NETDEFAULT_KOPIA_APP "$inner_block" insertSubAuthelia $SUB_KOPIA_APP.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcHeimdall kopia "$FMLNAME_KOPIA_APP" $USERTYPE_KOPIA_APP "https://$SUB_KOPIA_APP.$HOMESERVER_DOMAIN" "kopia.png" true "$(getHeimdallOrderFromSub $SUB_KOPIA_APP $USERTYPE_KOPIA_APP)" restartAllCaddyContainers fi } function outputConfigKopia() { cat < $HOME/kopia-compose.yml $STACK_VERSION_PREFIX kopia $(getScriptStackVersion kopia) services: kopia-app: image: $(getScriptImageByContainerName kopia-app) container_name: kopia-app hostname: kopia-app restart: unless-stopped env_file: stack.env privileged: true cap_add: - SYS_ADMIN security_opt: - apparmor:unconfined devices: - /dev/fuse:/dev/fuse:rwm command: - server - start - --disable-csrf-token-checks - --tls-cert-file=/certs/kopia-app.crt - --tls-key-file=/certs/kopia-app.key - --address=0.0.0.0:51515 - --server-username=$KOPIA_ADMIN_USERNAME - --server-password=$KOPIA_ADMIN_PASSWORD networks: - dock-proxy-net - dock-internalmail-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/kopia/config:/app/config - \${PORTAINER_HSHQ_STACKS_DIR}/kopia/cache:/app/cache - \${PORTAINER_HSHQ_STACKS_DIR}/kopia/logs:/app/logs - \${PORTAINER_HSHQ_DATA_DIR}:/data:ro - \${PORTAINER_HSHQ_BACKUP_DIR}/kopia:/repository - \${PORTAINER_HSHQ_NONBACKUP_DIR}/kopia/snapshots:/tmp:shared - \${PORTAINER_HSHQ_SSL_DIR}/kopia-app.crt:/certs/kopia-app.crt:ro - \${PORTAINER_HSHQ_SSL_DIR}/kopia-app.key:/certs/kopia-app.key:ro networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true EOFMT cat < $HOME/kopia.env TZ=\${PORTAINER_TZ} KOPIA_PASSWORD=$KOPIA_ADMIN_PASSWORD USER=$USERID EOFMT } function performUpdateKopia() { perform_stack_name=kopia prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/kopia/kopia:0.21.1 image_update_map[0]="mirror.gcr.io/kopia/kopia:0.21.1,mirror.gcr.io/kopia/kopia:0.22.3" ;; 2) newVer=v2 curImageList=mirror.gcr.io/kopia/kopia:0.22.3 image_update_map[0]="mirror.gcr.io/kopia/kopia:0.22.3,mirror.gcr.io/kopia/kopia:0.22.3" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # LocalAI function installLocalAI() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory localai "LocalAI" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName localai-server) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName localai-recall) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName localai-dind) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName localai-agi) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName localai-sshbox) if [ $? -ne 0 ]; then buildOrPullImage hshq/sshbox:v1 fi if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/localai mkdir $HSHQ_STACKS_DIR/localai/agi mkdir $HSHQ_STACKS_DIR/localai/assets mkdir $HSHQ_STACKS_DIR/localai/backends mkdir $HSHQ_STACKS_DIR/localai/config mkdir $HSHQ_STACKS_DIR/localai/data mkdir $HSHQ_STACKS_DIR/localai/db mkdir $HSHQ_STACKS_DIR/localai/dind mkdir $HSHQ_STACKS_DIR/localai/images mkdir -p $HSHQ_AIMODELS_DIR/localai initServicesCredentials set +e outputConfigLocalAI installStack localai localai-server "" $HOME/localai.env 1 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$LOCALAI_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_LOCALAI_SERVER Admin Login Info" -b "$FMLNAME_LOCALAI_SERVER Admin Username: $LOCALAI_ADMIN_USERNAME\n$FMLNAME_LOCALAI_SERVER Admin Email: $LOCALAI_ADMIN_EMAIL_ADDRESS\n$FMLNAME_LOCALAI_SERVER Admin Password: $LOCALAI_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" LOCALAI_INIT_ENV=true updateConfigVar LOCALAI_INIT_ENV $LOCALAI_INIT_ENV fi sleep 3 performIntegrationLocalAI if [ -z "$FMLNAME_LOCALAI_SERVER" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_LOCALAI_SERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://localai-server:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_LOCALAI_SERVER $MANAGETLS_LOCALAI_SERVER "$is_integrate_hshq" $NETDEFAULT_LOCALAI_SERVER "$inner_block" insertSubAuthelia $SUB_LOCALAI_SERVER.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_LOCALAI_RECALL.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://localai-recall:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_LOCALAI_RECALL $MANAGETLS_LOCALAI_RECALL "$is_integrate_hshq" $NETDEFAULT_LOCALAI_RECALL "$inner_block" insertSubAuthelia $SUB_LOCALAI_RECALL.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_LOCALAI_AGI.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://localai-agi:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_LOCALAI_AGI $MANAGETLS_LOCALAI_AGI "$is_integrate_hshq" $NETDEFAULT_LOCALAI_AGI "$inner_block" insertSubAuthelia $SUB_LOCALAI_AGI.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcHeimdall localai "$FMLNAME_LOCALAI_SERVER" $USERTYPE_LOCALAI_SERVER "https://$SUB_LOCALAI_SERVER.$HOMESERVER_DOMAIN" "localai.png" true "$(getHeimdallOrderFromSub $SUB_LOCALAI_SERVER $USERTYPE_LOCALAI_SERVER)" insertEnableSvcAll localai "$FMLNAME_LOCALAI_RECALL" $USERTYPE_LOCALAI_RECALL "https://$SUB_LOCALAI_RECALL.$HOMESERVER_DOMAIN" "localrecall.png" "$(getHeimdallOrderFromSub $SUB_LOCALAI_RECALL $USERTYPE_LOCALAI_RECALL)" insertEnableSvcAll localai "$FMLNAME_LOCALAI_AGI" $USERTYPE_LOCALAI_AGI "https://$SUB_LOCALAI_AGI.$HOMESERVER_DOMAIN" "localagi.png" "$(getHeimdallOrderFromSub $SUB_LOCALAI_AGI $USERTYPE_LOCALAI_AGI)" restartAllCaddyContainers fi } function outputConfigLocalAI() { cat < $HOME/localai-compose.yml $STACK_VERSION_PREFIX localai $(getScriptStackVersion localai) services: localai-server: image: $(getScriptImageByContainerName localai-server) container_name: localai-server hostname: localai-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-localai-net - dock-ext-net - dock-aipriv-net healthcheck: test: ["CMD", "curl", "-f", "-H", "Authorization: Bearer $LOCALAI_API_KEY", "http://localhost:8080/readyz"] interval: 60s timeout: 10m retries: 120 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/localai:/models - \${PORTAINER_HSHQ_STACKS_DIR}/localai/backends:/backends - \${PORTAINER_HSHQ_STACKS_DIR}/localai/config:/configuration - \${PORTAINER_HSHQ_STACKS_DIR}/localai/data:/data - \${PORTAINER_HSHQ_STACKS_DIR}/localai/images:/tmp/generated/images localai-recall: image: $(getScriptImageByContainerName localai-recall) container_name: localai-recall hostname: localai-recall restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-localai-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/localai/db:/db - \${PORTAINER_HSHQ_STACKS_DIR}/localai/assets:/assets localai-sshbox: image: $(getScriptImageByContainerName localai-sshbox) container_name: localai-sshbox hostname: localai-sshbox restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-localai-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro environment: - SSH_USER=root - SSH_PASSWORD=$LOCALAI_SSHBOX_PASSWORD localai-dind: image: $(getScriptImageByContainerName localai-dind) container_name: localai-dind hostname: localai-dind restart: unless-stopped privileged: true networks: - int-localai-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/localai/dind:/var/lib/docker environment: - DOCKER_TLS_CERTDIR="" localai-agi: image: $(getScriptImageByContainerName localai-agi) container_name: localai-agi hostname: localai-agi restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-localai-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/localai/agi:/pool networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true int-localai-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/localai.env TZ=\${PORTAINER_TZ} DEBUG=false DOCKER_HOST=tcp://localai-dind:2375 LOCALAI_API_KEY=$LOCALAI_API_KEY LOCALAI_MAX_ACTIVE_BACKENDS=4 COLLECTION_DB_PATH=/db EMBEDDING_MODEL=granite-embedding-107m-multilingual FILE_ASSETS=/assets OPENAI_API_KEY=$LOCALAI_API_KEY OPENAI_BASE_URL=http://localai-server:8080 LOCALAGI_MODEL=gemma-3-4b-it-qat LOCALAGI_MULTIMODAL_MODEL=moondream2-20250414 LOCALAGI_IMAGE_MODEL=sd-1.5-ggml LOCALAGI_LLM_API_URL=http://localai-server:8080 LOCALAGI_LLM_API_KEY=$LOCALAI_API_KEY LOCALAGI_LOCALRAG_URL=http://localai-recall:8080 LOCALAGI_STATE_DIR=/pool LOCALAGI_TIMEOUT=5m LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false LOCALAGI_SSHBOX_URL=root:$LOCALAI_SSHBOX_PASSWORD@localai-sshbox:22 EOFMT } function performIntegrationLocalAI() { return } function performUpdateLocalAI() { perform_stack_name=localai prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/localai/localai:v3.8.0-aio-cpu,quay.io/mudler/localrecall:v0.4.1,mirror.gcr.io/busybox:1.37.0,sshbox:v1,mirror.gcr.io/docker:29.1.3-dind,quay.io/mudler/localagi:v2.6.0 image_update_map[0]="mirror.gcr.io/localai/localai:v3.8.0-aio-cpu,mirror.gcr.io/localai/localai:v3.8.0-aio-cpu" image_update_map[1]="quay.io/mudler/localrecall:v0.4.1,quay.io/mudler/localrecall:v0.4.1" image_update_map[2]="mirror.gcr.io/busybox:1.37.0,mirror.gcr.io/busybox:1.37.0" image_update_map[3]="sshbox:v1,sshbox:v1" image_update_map[4]="mirror.gcr.io/docker:29.1.3-dind,mirror.gcr.io/docker:29.1.3-dind" image_update_map[5]="quay.io/mudler/localagi:v2.6.0,quay.io/mudler/localagi:v2.6.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function buildImageLocalAISSHBoxV1() { set +e sudo rm -fr $HSHQ_BUILD_DIR/localaisshbox mkdir $HSHQ_BUILD_DIR/localaisshbox # This Dockerfile is copied directly from https://github.com/mudler/LocalAGI/blob/main/Dockerfile.sshbox # Since it is so small and unlikely to change, we'll just # avoid the git clone and thus decrease pof. cat < $HSHQ_BUILD_DIR/localaisshbox/Dockerfile # Final stage FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive # Install runtime dependencies RUN apt-get update && apt-get install -y \ ca-certificates \ tzdata \ docker.io \ bash \ wget \ curl \ openssh-server \ sudo # Configure SSH RUN mkdir /var/run/sshd RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config # Create startup script RUN echo '#!/bin/bash\n\ if [ -n "\$SSH_USER" ]; then\n\ if [ "\$SSH_USER" = "root" ]; then\n\ echo "PermitRootLogin yes" >> /etc/ssh/sshd_config\n\ if [ -n "\$SSH_PASSWORD" ]; then\n\ echo "root:\$SSH_PASSWORD" | chpasswd\n\ fi\n\ else\n\ echo "PermitRootLogin no" >> /etc/ssh/sshd_config\n\ useradd -m -s /bin/bash \$SSH_USER\n\ if [ -n "\$SSH_PASSWORD" ]; then\n\ echo "\$SSH_USER:\$SSH_PASSWORD" | chpasswd\n\ fi\n\ if [ -n "\$SUDO_ACCESS" ] && [ "\$SUDO_ACCESS" = "true" ]; then\n\ echo "\$SSH_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/\$SSH_USER\n\ fi\n\ fi\n\ fi\n\ /usr/sbin/sshd -D' > /start.sh RUN chmod +x /start.sh EXPOSE 22 CMD ["/start.sh"] EOFMT docker build -t hshq/sshbox:v1 $HSHQ_BUILD_DIR/localaisshbox retVal=$? sudo rm -fr $HSHQ_BUILD_DIR/localaisshbox return $retVal } # ComfyUI function installComfyUI() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory comfyui "ComfyUI" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName comfyui-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/comfyui mkdir $HSHQ_STACKS_DIR/comfyui/input mkdir $HSHQ_STACKS_DIR/comfyui/output mkdir $HSHQ_STACKS_DIR/comfyui/custom_nodes mkdir $HSHQ_STACKS_DIR/comfyui/user mkdir -p $HSHQ_NONBACKUP_DIR/comfyui/cache mkdir -p $HSHQ_AIMODELS_DIR/comfyui initServicesCredentials set +e outputConfigComfyUI installStack comfyui comfyui-app "To see the GUI go to" $HOME/comfyui.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$COMFYUI_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_COMFYUI_APP API Info" -b "If accessing ComfyUI externally, you will need the following info: \n\nComfyUI URL: https://$SUB_COMFYUI_APP.$HOMESERVER_DOMAIN\nComfyUI API Key: $COMFYUI_API_KEY\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" COMFYUI_INIT_ENV=true updateConfigVar COMFYUI_INIT_ENV $COMFYUI_INIT_ENV fi sleep 3 if [ -z "$FMLNAME_COMFYUI_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_COMFYUI_API.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>@auth_bearer {\n" inner_block=$inner_block">>>>>>header Authorization \"Bearer $COMFYUI_API_KEY\"\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>@auth_query {\n" inner_block=$inner_block">>>>>>query api-key=$COMFYUI_API_KEY\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>@auth_header {\n" inner_block=$inner_block">>>>>>header X-API-Key $COMFYUI_API_KEY\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>handle @auth_bearer {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://comfyui-app:8188 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>handle @auth_query {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://comfyui-app:8188 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>handle @auth_header {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://comfyui-app:8188 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>respond 403 {\n" inner_block=$inner_block">>>>>>>>body \"Forbidden: Invalid API Key\"\n" inner_block=$inner_block">>>>>>>>close\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_COMFYUI_API $MANAGETLS_COMFYUI_API "$is_integrate_hshq" $NETDEFAULT_COMFYUI_API "$inner_block" insertSubAuthelia $SUB_COMFYUI_API.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_COMFYUI_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://comfyui-app:8188 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_COMFYUI_APP $MANAGETLS_COMFYUI_APP "$is_integrate_hshq" $NETDEFAULT_COMFYUI_APP "$inner_block" insertSubAuthelia $SUB_COMFYUI_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcUptimeKuma comfyui "$FMLNAME_COMFYUI_API" $USERTYPE_COMFYUI_API "https://$SUB_COMFYUI_API.$HOMESERVER_DOMAIN/api/tags?api-key=$COMFYUI_API_KEY" insertEnableSvcAll comfyui "$FMLNAME_COMFYUI_APP" $USERTYPE_COMFYUI_APP "https://$SUB_COMFYUI_APP.$HOMESERVER_DOMAIN" "comfyui.png" "$(getHeimdallOrderFromSub $SUB_COMFYUI_APP $USERTYPE_COMFYUI_APP)" restartAllCaddyContainers fi } function outputConfigComfyUI() { cat < $HOME/comfyui-compose.yml $STACK_VERSION_PREFIX comfyui $(getScriptStackVersion comfyui) services: comfyui-app: image: $(getScriptImageByContainerName comfyui-app) container_name: comfyui-app hostname: comfyui-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-comfyui-models:/opt/ComfyUI/models - v-comfyui-input:/opt/ComfyUI/input - v-comfyui-output:/opt/ComfyUI/output - v-comfyui-user:/opt/ComfyUI/user - v-comfyui-custom_nodes:/opt/ComfyUI/custom_nodes - v-comfyui-cache:/opt/ComfyUI/.cache volumes: v-comfyui-models: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/comfyui v-comfyui-input: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/comfyui/input v-comfyui-output: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/comfyui/output v-comfyui-user: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/comfyui/user v-comfyui-custom_nodes: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/comfyui/custom_nodes v-comfyui-cache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/comfyui/cache networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true EOFMT cat < $HOME/comfyui.env TZ=\${PORTAINER_TZ} COMFYUI_API_KEY=$COMFYUI_API_KEY EOFMT } function performUpdateComfyUI() { perform_stack_name=comfyui prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/corundex/comfyui-rocm:comfyui_0.3.43__rocm6.4.1_ubuntu24.04_py3.12_pytorch_2.6.0 image_update_map[0]="mirror.gcr.io/corundex/comfyui-rocm:comfyui_0.3.43__rocm6.4.1_ubuntu24.04_py3.12_pytorch_2.6.0,mirror.gcr.io/corundex/comfyui-rocm:comfyui_0.3.43__rocm6.4.1_ubuntu24.04_py3.12_pytorch_2.6.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Langflow function installLangflow() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory langflow "Langflow" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName langflow-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName langflow-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/langflow mkdir $HSHQ_STACKS_DIR/langflow/config mkdir $HSHQ_STACKS_DIR/langflow/data mkdir $HSHQ_STACKS_DIR/langflow/db mkdir $HSHQ_STACKS_DIR/langflow/dbexport chmod 777 $HSHQ_STACKS_DIR/langflow/dbexport initServicesCredentials set +e addUserMailu alias $LANGFLOW_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS LANGFLOW_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $LANGFLOW_ADMIN_PASSWORD | tr -d ':\n') outputConfigLangflow installStack langflow langflow-app "Welcome to Langflow" $HOME/langflow.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$LANGFLOW_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_LANGFLOW_APP Admin Login Info" -b "$FMLNAME_LANGFLOW_APP Admin Username: $LANGFLOW_ADMIN_USERNAME\n$FMLNAME_LANGFLOW_APP Admin Email: $LANGFLOW_ADMIN_EMAIL_ADDRESS\n$FMLNAME_LANGFLOW_APP Admin Password: $LANGFLOW_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" LANGFLOW_INIT_ENV=true updateConfigVar LANGFLOW_INIT_ENV $LANGFLOW_INIT_ENV fi sleep 3 performIntegrationLangflow addReadOnlyUserToDatabase Langflow postgres langflow-db $LANGFLOW_DATABASE_NAME $LANGFLOW_DATABASE_USER $LANGFLOW_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/langflow/dbexport $LANGFLOW_DATABASE_READONLYUSER $LANGFLOW_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_LANGFLOW_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_LANGFLOW_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://langflow-app:7860 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_LANGFLOW_APP $MANAGETLS_LANGFLOW_APP "$is_integrate_hshq" $NETDEFAULT_LANGFLOW_APP "$inner_block" insertSubAuthelia $SUB_LANGFLOW_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll langflow "$FMLNAME_LANGFLOW_APP" $USERTYPE_LANGFLOW_APP "https://$SUB_LANGFLOW_APP.$HOMESERVER_DOMAIN" "langflow.png" "$(getHeimdallOrderFromSub $SUB_LANGFLOW_APP $USERTYPE_LANGFLOW_APP)" restartAllCaddyContainers checkAddDBConnection true langflow "$FMLNAME_LANGFLOW_APP" postgres langflow-db $LANGFLOW_DATABASE_NAME $LANGFLOW_DATABASE_USER $LANGFLOW_DATABASE_USER_PASSWORD fi } function outputConfigLangflow() { cat < $HOME/langflow-compose.yml $STACK_VERSION_PREFIX langflow $(getScriptStackVersion langflow) services: langflow-db: image: $(getScriptImageByContainerName langflow-db) container_name: langflow-db hostname: langflow-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-langflow-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/langflow/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/langflow/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.langflow-hourly-db.schedule=@every 1h" - "ofelia.job-exec.langflow-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.langflow-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.langflow-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.langflow-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.langflow-hourly-db.email-from=Langflow Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.langflow-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.langflow-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.langflow-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.langflow-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.langflow-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.langflow-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.langflow-monthly-db.email-from=Langflow Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.langflow-monthly-db.mail-only-on-error=false" langflow-app: image: $(getScriptImageByContainerName langflow-app) container_name: langflow-app hostname: langflow-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - langflow-db networks: - int-langflow-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-langflow-data:/app/langflow volumes: v-langflow-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/langflow/data networks: dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-langflow-net: driver: bridge internal: true ipam: driver: default EOFMT outputEnvLangflow } function outputEnvLangflow() { cat < $HOME/langflow.env TZ=\${PORTAINER_TZ} DO_NOT_TRACK=true POSTGRES_DB=$LANGFLOW_DATABASE_NAME POSTGRES_USER=$LANGFLOW_DATABASE_USER POSTGRES_PASSWORD=$LANGFLOW_DATABASE_USER_PASSWORD LANGFLOW_CONFIG_DIR=/app/langflow LANGFLOW_DATABASE_URL=postgresql://$LANGFLOW_DATABASE_USER:$LANGFLOW_DATABASE_USER_PASSWORD@langflow-db:5432/$LANGFLOW_DATABASE_NAME LANGFLOW_AUTO_LOGIN=False LANGFLOW_NEW_USER_IS_ACTIVE=False LANGFLOW_SUPERUSER=$LANGFLOW_ADMIN_USERNAME LANGFLOW_SUPERUSER_PASSWORD=$LANGFLOW_ADMIN_PASSWORD LANGFLOW_API_KEY_SOURCE=env LANGFLOW_API_KEY=$LANGFLOW_API_KEY LANGFLOW_SECRET_KEY=$LANGFLOW_SECRET_KEY EOFMT } function performIntegrationLangflow() { return } function performUpdateLangflow() { perform_stack_name=langflow prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/langflowai/langflow:1.7.1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/langflowai/langflow:1.7.1,mirror.gcr.io/langflowai/langflow:1.8.1" ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/langflowai/langflow:1.8.1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/langflowai/langflow:1.8.1,mirror.gcr.io/langflowai/langflow:1.8.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # AnythingLLM function installAnythingLLM() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory anythingllm "AnythingLLM" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName anythingllm-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/anythingllm mkdir $HSHQ_STACKS_DIR/anythingllm/data initServicesCredentials set +e addUserMailu alias $ANYTHINGLLM_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigAnythingLLM installStack anythingllm anythingllm-app "Primary server in HTTP mode listening" $HOME/anythingllm.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$ANYTHINGLLM_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_ANYTHINGLLM_APP Admin Login Info" -b "$FMLNAME_ANYTHINGLLM_APP Admin Username: $ANYTHINGLLM_ADMIN_USERNAME\n$FMLNAME_ANYTHINGLLM_APP Admin Password: $ANYTHINGLLM_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" ANYTHINGLLM_INIT_ENV=true updateConfigVar ANYTHINGLLM_INIT_ENV $ANYTHINGLLM_INIT_ENV fi sleep 3 performDBConfigAnythingLLM if [ -z "$FMLNAME_ANYTHINGLLM_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://anythingllm-app:3001 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_ANYTHINGLLM_APP $MANAGETLS_ANYTHINGLLM_APP "$is_integrate_hshq" $NETDEFAULT_ANYTHINGLLM_APP "$inner_block" insertSubAuthelia $SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll anythingllm "$FMLNAME_ANYTHINGLLM_APP" $USERTYPE_ANYTHINGLLM_APP "https://$SUB_ANYTHINGLLM_APP.$HOMESERVER_DOMAIN" "anythingllm.png" "$(getHeimdallOrderFromSub $SUB_ANYTHINGLLM_APP $USERTYPE_ANYTHINGLLM_APP)" restartAllCaddyContainers fi } function outputConfigAnythingLLM() { touch $HSHQ_STACKS_DIR/anythingllm/.env cat < $HOME/anythingllm-compose.yml $STACK_VERSION_PREFIX anythingllm $(getScriptStackVersion anythingllm) services: anythingllm-app: image: $(getScriptImageByContainerName anythingllm-app) container_name: anythingllm-app hostname: anythingllm-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true cap_add: - SYS_ADMIN networks: - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-anythingllm-data:/app/server/storage - \${PORTAINER_HSHQ_STACKS_DIR}/anythingllm/.env:/app/server/.env volumes: v-anythingllm-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/anythingllm/data networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true EOFMT outputEnvAnythingLLM } function outputEnvAnythingLLM() { cat < $HOME/anythingllm.env TZ=\${PORTAINER_TZ} NODE_ENV=production NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt DISABLE_TELEMETRY=true STORAGE_DIR=/app/server/storage SIG_KEY=$ANYTHINGLLM_SIG_KEY SIG_SALT=$ANYTHINGLLM_SIG_SALT JWT_SECRET=$ANYTHINGLLM_JWT_SECRET EOFMT } function performDBConfigAnythingLLM() { return } function performUpdateAnythingLLM() { perform_stack_name=anythingllm prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/mintplexlabs/anythingllm:1.9.1 image_update_map[0]="mirror.gcr.io/mintplexlabs/anythingllm:1.9.1,mirror.gcr.io/mintplexlabs/anythingllm:1.9.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Perplexica function installPerplexica() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory perplexica "Perplexica" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName perplexica-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/perplexica mkdir $HSHQ_STACKS_DIR/perplexica/data set +e outputConfigPerplexica performIntegrationPerplexica installStack perplexica perplexica-app "Ready in" $HOME/perplexica.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 3 if [ -z "$FMLNAME_PERPLEXICA_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_PERPLEXICA_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://perplexica-app:3000 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_PERPLEXICA_APP $MANAGETLS_PERPLEXICA_APP "$is_integrate_hshq" $NETDEFAULT_PERPLEXICA_APP "$inner_block" insertSubAuthelia $SUB_PERPLEXICA_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll perplexica "$FMLNAME_PERPLEXICA_APP" $USERTYPE_PERPLEXICA_APP "https://$SUB_PERPLEXICA_APP.$HOMESERVER_DOMAIN" "perplexica.png" "$(getHeimdallOrderFromSub $SUB_PERPLEXICA_APP $USERTYPE_PERPLEXICA_APP)" restartAllCaddyContainers fi } function outputConfigPerplexica() { cat < $HOME/perplexica-compose.yml $STACK_VERSION_PREFIX perplexica $(getScriptStackVersion perplexica) services: perplexica-app: image: $(getScriptImageByContainerName perplexica-app) container_name: perplexica-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-perplexica-data:/home/perplexica/data volumes: v-perplexica-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/perplexica/data networks: dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true EOFMT cat < $HOME/perplexica.env TZ=\${PORTAINER_TZ} SEARXNG_API_URL=http://searxng-app:8080 HOSTNAME=0.0.0.0 EOFMT } function performIntegrationPerplexica() { return } function performUpdatePerplexica() { perform_stack_name=perplexica prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/itzcrazykns1337/perplexica:slim-v1.12.0 image_update_map[0]="mirror.gcr.io/itzcrazykns1337/perplexica:slim-v1.12.0,mirror.gcr.io/itzcrazykns1337/perplexica:slim-v1.12.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Firecrawl function installFirecrawl() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory firecrawl "Firecrawl" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firecrawl-db) if [ $? -ne 0 ]; then buildOrPullImage hshq/firecrawl-db:v1 fi pullImage $(getScriptImageByContainerName firecrawl-api) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firecrawl-playwright) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firecrawl-api) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firecrawl-rabbitmq) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName firecrawl-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/firecrawl mkdir $HSHQ_STACKS_DIR/firecrawl/config mkdir $HSHQ_STACKS_DIR/firecrawl/data mkdir $HSHQ_STACKS_DIR/firecrawl/rabbitmq mkdir $HSHQ_STACKS_DIR/firecrawl/db mkdir $HSHQ_STACKS_DIR/firecrawl/dbexport chmod 777 $HSHQ_STACKS_DIR/firecrawl/dbexport mkdir $HSHQ_NONBACKUP_DIR/firecrawl mkdir $HSHQ_NONBACKUP_DIR/firecrawl/redis initServicesCredentials set +e outputConfigFirecrawl installStack firecrawl firecrawl-api "All services running" $HOME/firecrawl.env 3 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$FIRECRAWL_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_FIRECRAWL_API Admin Login Info" -b "$FMLNAME_FIRECRAWL_API Admin Password: $FIRECRAWL_BULL_AUTH_KEY\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" FIRECRAWL_INIT_ENV=true updateConfigVar FIRECRAWL_INIT_ENV $FIRECRAWL_INIT_ENV fi sleep 3 if [ -z "$FMLNAME_FIRECRAWL_API" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_FIRECRAWL_API.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://firecrawl-api:3002 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_FIRECRAWL_API $MANAGETLS_FIRECRAWL_API "$is_integrate_hshq" $NETDEFAULT_FIRECRAWL_API "$inner_block" insertSubAuthelia $SUB_FIRECRAWL_API.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcUptimeKuma firecrawl "$FMLNAME_FIRECRAWL_API" admin "https://$SUB_FIRECRAWL_API.$HOMESERVER_DOMAIN" true restartAllCaddyContainers fi } function outputConfigFirecrawl() { cat < $HOME/firecrawl-compose.yml $STACK_VERSION_PREFIX firecrawl $(getScriptStackVersion firecrawl) services: firecrawl-db: image: $(getScriptImageByContainerName firecrawl-db) container_name: firecrawl-db hostname: firecrawl-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-firecrawl-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro firecrawl-playwright: image: $(getScriptImageByContainerName firecrawl-playwright) container_name: firecrawl-playwright hostname: firecrawl-playwright restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true mem_limit: 4G memswap_limit: 4G tmpfs: - /tmp/.cache:noexec,nosuid,size=1g networks: - int-firecrawl-net - dock-aipriv-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro environment: - PORT=3000 firecrawl-api: image: $(getScriptImageByContainerName firecrawl-api) container_name: firecrawl-api hostname: firecrawl-api restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true mem_limit: 4G memswap_limit: 4G depends_on: - firecrawl-db - firecrawl-playwright - firecrawl-redis - firecrawl-rabbitmq networks: - int-firecrawl-net - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro environment: - HOST=0.0.0.0 - PORT=3002 firecrawl-rabbitmq: image: $(getScriptImageByContainerName firecrawl-rabbitmq) container_name: firecrawl-rabbitmq hostname: firecrawl-rabbitmq restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-firecrawl-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro firecrawl-redis: image: $(getScriptImageByContainerName firecrawl-redis) container_name: firecrawl-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $FIRECRAWL_REDIS_PASSWORD --appendonly no networks: - int-firecrawl-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-firecrawl-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/firecrawl.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$FIRECRAWL_DATABASE_NAME POSTGRES_USER=$FIRECRAWL_DATABASE_USER POSTGRES_PASSWORD=$FIRECRAWL_DATABASE_USER_PASSWORD REDIS_URL=redis://:$FIRECRAWL_REDIS_PASSWORD@firecrawl-redis:6379 REDIS_RATE_LIMIT_URL=redis://:$FIRECRAWL_REDIS_PASSWORD@firecrawl-redis:6379 PLAYWRIGHT_MICROSERVICE_URL=http://firecrawl-playwright:3000/scrape POSTGRES_HOST=firecrawl-db POSTGRES_PORT=5432 USE_DB_AUTHENTICATION=false NUM_WORKERS_PER_QUEUE=8 CRAWL_CONCURRENT_REQUESTS=10 MAX_CONCURRENT_JOBS=5 BROWSER_POOL_SIZE=5 MAX_CONCURRENT_PAGES=10 OPENAI_API_KEY= OPENAI_BASE_URL= MODEL_NAME= MODEL_EMBEDDING_NAME= OLLAMA_BASE_URL= SLACK_WEBHOOK_URL= BULL_AUTH_KEY=$FIRECRAWL_BULL_AUTH_KEY TEST_API_KEY=$FIRECRAWL_API_KEY ALLOW_LOCAL_WEBHOOKS=true SEARXNG_ENDPOINT=http://searxng-app:8080 EXTRACT_WORKER_PORT=3004 WORKER_PORT=3005 RABBITMQ_DEFAULT_USER=$FIRECRAWL_RABBITMQ_USERNAME RABBITMQ_DEFAULT_PASS=$FIRECRAWL_RABBITMQ_PASSWORD NUQ_RABBITMQ_URL=amqp://$FIRECRAWL_RABBITMQ_USERNAME:$FIRECRAWL_RABBITMQ_PASSWORD@firecrawl-rabbitmq:5672 ENV=local EOFMT } function performUpdateFirecrawl() { perform_stack_name=firecrawl prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=hshq/firecrawl-db:v1,ghcr.io/firecrawl/playwright-service:latest,ghcr.io/firecrawl/firecrawl:latest,mirror.gcr.io/rabbitmq:4.1.4-management-alpine,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="hshq/firecrawl-db:v1,hshq/firecrawl-db:v1" image_update_map[1]="ghcr.io/firecrawl/playwright-service:latest,ghcr.io/firecrawl/playwright-service:latest" image_update_map[2]="ghcr.io/firecrawl/firecrawl:latest,ghcr.io/firecrawl/firecrawl:latest" image_update_map[3]="mirror.gcr.io/rabbitmq:4.1.4-management-alpine,mirror.gcr.io/rabbitmq:4.1.4-management-alpine" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function buildImageFirecrawlDBV1() { set +e sudo rm -fr $HSHQ_BUILD_DIR/firecrawldb mkdir $HSHQ_BUILD_DIR/firecrawldb cd $HSHQ_BUILD_DIR/firecrawldb # This Dockerfile is copied directly from https://github.com/firecrawl/firecrawl/blob/main/apps/nuq-postgres/Dockerfile # Since it is so small and unlikely to change, we'll just # avoid the git clone and thus decrease pof. cat < $HSHQ_BUILD_DIR/firecrawldb/Dockerfile # Build a Postgres image that runs nuq.sql during initdb ARG PG_MAJOR=17 FROM postgres:\${PG_MAJOR} # Install pg_cron for the specified Postgres major version RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ postgresql-\${PG_MAJOR}-cron; \ rm -rf /var/lib/apt/lists/* # Ensure pg_cron is preloaded on first startup by modifying the initdb template # This must be set before the first server start (init scripts run after start) RUN set -eux; \ conf_sample="/usr/share/postgresql/\${PG_MAJOR}/postgresql.conf.sample"; \ sed -ri "s/^#?shared_preload_libraries\s*=.*/shared_preload_libraries = 'pg_cron'/" "\$conf_sample"; \ printf "\n# Added for pg_cron\ncron.database_name = '$FIRECRAWL_DATABASE_NAME'\n" >> "\$conf_sample" # Copy nuq.sql so it is executed as part of the initdb sequence COPY nuq.sql /docker-entrypoint-initdb.d/010-nuq.sql EOFMT cat < $HSHQ_BUILD_DIR/firecrawldb/nuq.sql CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pg_cron; CREATE SCHEMA IF NOT EXISTS nuq; DO \$\$ BEGIN CREATE TYPE nuq.job_status AS ENUM ('queued', 'active', 'completed', 'failed'); EXCEPTION WHEN duplicate_object THEN null; END \$\$; DO \$\$ BEGIN CREATE TYPE nuq.group_status AS ENUM ('active', 'completed', 'cancelled'); EXCEPTION WHEN duplicate_object THEN null; END \$\$; CREATE TABLE IF NOT EXISTS nuq.queue_scrape ( id uuid NOT NULL DEFAULT gen_random_uuid(), status nuq.job_status NOT NULL DEFAULT 'queued'::nuq.job_status, data jsonb, created_at timestamp with time zone NOT NULL DEFAULT now(), priority int NOT NULL DEFAULT 0, lock uuid, locked_at timestamp with time zone, stalls integer, finished_at timestamp with time zone, listen_channel_id text, -- for listenable jobs over rabbitmq returnvalue jsonb, -- only for selfhost failedreason text, -- only for selfhost owner_id uuid, group_id uuid, CONSTRAINT queue_scrape_pkey PRIMARY KEY (id) ); ALTER TABLE nuq.queue_scrape SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.01, autovacuum_vacuum_cost_limit = 2000, autovacuum_vacuum_cost_delay = 2); CREATE INDEX IF NOT EXISTS queue_scrape_active_locked_at_idx ON nuq.queue_scrape USING btree (locked_at) WHERE (status = 'active'::nuq.job_status); CREATE INDEX IF NOT EXISTS nuq_queue_scrape_queued_optimal_2_idx ON nuq.queue_scrape (priority ASC, created_at ASC, id) WHERE (status = 'queued'::nuq.job_status); CREATE INDEX IF NOT EXISTS nuq_queue_scrape_failed_created_at_idx ON nuq.queue_scrape USING btree (created_at) WHERE (status = 'failed'::nuq.job_status); CREATE INDEX IF NOT EXISTS nuq_queue_scrape_completed_created_at_idx ON nuq.queue_scrape USING btree (created_at) WHERE (status = 'completed'::nuq.job_status); -- Indexes for crawl-status.ts queries -- For getGroupAnyJob: query by group_id, owner_id, and data->>'mode' = 'single_urls' CREATE INDEX IF NOT EXISTS nuq_queue_scrape_group_owner_mode_idx ON nuq.queue_scrape (group_id, owner_id) WHERE ((data->>'mode') = 'single_urls'); -- For getGroupNumericStats: query by group_id and data->>'mode', grouped by status CREATE INDEX IF NOT EXISTS nuq_queue_scrape_group_mode_status_idx ON nuq.queue_scrape (group_id, status) WHERE ((data->>'mode') = 'single_urls'); -- For getCrawlJobsForListing: query by group_id, status='completed', data->>'mode', ordered by finished_at, created_at CREATE INDEX IF NOT EXISTS nuq_queue_scrape_group_completed_listing_idx ON nuq.queue_scrape (group_id, finished_at ASC, created_at ASC) WHERE (status = 'completed'::nuq.job_status AND (data->>'mode') = 'single_urls'); -- For group finish cron CREATE INDEX IF NOT EXISTS idx_queue_scrape_group_status ON nuq.queue_scrape (group_id, status) WHERE status IN ('active', 'queued'); CREATE TABLE IF NOT EXISTS nuq.queue_scrape_backlog ( id uuid NOT NULL DEFAULT gen_random_uuid(), data jsonb, created_at timestamp with time zone NOT NULL DEFAULT now(), priority int NOT NULL DEFAULT 0, listen_channel_id text, -- for listenable jobs over rabbitmq owner_id uuid, group_id uuid, times_out_at timestamptz, CONSTRAINT queue_scrape_backlog_pkey PRIMARY KEY (id) ); -- For getGroupNumericStats backlog query: query by group_id and data->>'mode' on backlog table CREATE INDEX IF NOT EXISTS nuq_queue_scrape_backlog_group_mode_idx ON nuq.queue_scrape_backlog (group_id) WHERE ((data->>'mode') = 'single_urls'); SELECT cron.schedule('nuq_queue_scrape_clean_completed', '*/5 * * * *', \$\$ DELETE FROM nuq.queue_scrape WHERE nuq.queue_scrape.status = 'completed'::nuq.job_status AND nuq.queue_scrape.created_at < now() - interval '1 hour' AND group_id IS NULL; \$\$); SELECT cron.schedule('nuq_queue_scrape_clean_failed', '*/5 * * * *', \$\$ DELETE FROM nuq.queue_scrape WHERE nuq.queue_scrape.status = 'failed'::nuq.job_status AND nuq.queue_scrape.created_at < now() - interval '6 hours' AND group_id IS NULL; \$\$); SELECT cron.schedule('nuq_queue_scrape_lock_reaper', '15 seconds', \$\$ UPDATE nuq.queue_scrape SET status = 'queued'::nuq.job_status, lock = null, locked_at = null, stalls = COALESCE(stalls, 0) + 1 WHERE nuq.queue_scrape.locked_at <= now() - interval '1 minute' AND nuq.queue_scrape.status = 'active'::nuq.job_status AND COALESCE(nuq.queue_scrape.stalls, 0) < 9; WITH stallfail AS (UPDATE nuq.queue_scrape SET status = 'failed'::nuq.job_status, lock = null, locked_at = null, stalls = COALESCE(stalls, 0) + 1 WHERE nuq.queue_scrape.locked_at <= now() - interval '1 minute' AND nuq.queue_scrape.status = 'active'::nuq.job_status AND COALESCE(nuq.queue_scrape.stalls, 0) >= 9 RETURNING id) SELECT pg_notify('nuq.queue_scrape', (id::text || '|' || 'failed'::text)) FROM stallfail; \$\$); SELECT cron.schedule('nuq_queue_scrape_backlog_reaper', '* * * * *', \$\$ DELETE FROM nuq.queue_scrape_backlog WHERE nuq.queue_scrape_backlog.times_out_at < now(); \$\$); SELECT cron.schedule('nuq_queue_scrape_reindex', '0 9 * * *', \$\$ REINDEX TABLE CONCURRENTLY nuq.queue_scrape; \$\$); CREATE TABLE IF NOT EXISTS nuq.queue_crawl_finished ( id uuid NOT NULL DEFAULT gen_random_uuid(), status nuq.job_status NOT NULL DEFAULT 'queued'::nuq.job_status, data jsonb, created_at timestamp with time zone NOT NULL DEFAULT now(), priority int NOT NULL DEFAULT 0, lock uuid, locked_at timestamp with time zone, stalls integer, finished_at timestamp with time zone, listen_channel_id text, -- for listenable jobs over rabbitmq returnvalue jsonb, -- only for selfhost failedreason text, -- only for selfhost owner_id uuid, group_id uuid, CONSTRAINT queue_crawl_finished_pkey PRIMARY KEY (id) ); ALTER TABLE nuq.queue_crawl_finished SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.01, autovacuum_vacuum_cost_limit = 2000, autovacuum_vacuum_cost_delay = 2); CREATE INDEX IF NOT EXISTS queue_crawl_finished_active_locked_at_idx ON nuq.queue_crawl_finished USING btree (locked_at) WHERE (status = 'active'::nuq.job_status); CREATE INDEX IF NOT EXISTS nuq_queue_crawl_finished_queued_optimal_2_idx ON nuq.queue_crawl_finished (priority ASC, created_at ASC, id) WHERE (status = 'queued'::nuq.job_status); CREATE INDEX IF NOT EXISTS nuq_queue_crawl_finished_failed_created_at_idx ON nuq.queue_crawl_finished USING btree (created_at) WHERE (status = 'failed'::nuq.job_status); CREATE INDEX IF NOT EXISTS nuq_queue_crawl_finished_completed_created_at_idx ON nuq.queue_crawl_finished USING btree (created_at) WHERE (status = 'completed'::nuq.job_status); SELECT cron.schedule('nuq_queue_crawl_finished_clean_completed', '*/5 * * * *', \$\$ DELETE FROM nuq.queue_crawl_finished WHERE nuq.queue_crawl_finished.status = 'completed'::nuq.job_status AND nuq.queue_crawl_finished.created_at < now() - interval '1 hour' AND group_id IS NULL; \$\$); SELECT cron.schedule('nuq_queue_crawl_finished_clean_failed', '*/5 * * * *', \$\$ DELETE FROM nuq.queue_crawl_finished WHERE nuq.queue_crawl_finished.status = 'failed'::nuq.job_status AND nuq.queue_crawl_finished.created_at < now() - interval '6 hours' AND group_id IS NULL; \$\$); SELECT cron.schedule('nuq_queue_crawl_finished_lock_reaper', '15 seconds', \$\$ UPDATE nuq.queue_crawl_finished SET status = 'queued'::nuq.job_status, lock = null, locked_at = null, stalls = COALESCE(stalls, 0) + 1 WHERE nuq.queue_crawl_finished.locked_at <= now() - interval '1 minute' AND nuq.queue_crawl_finished.status = 'active'::nuq.job_status AND COALESCE(nuq.queue_crawl_finished.stalls, 0) < 9; WITH stallfail AS (UPDATE nuq.queue_crawl_finished SET status = 'failed'::nuq.job_status, lock = null, locked_at = null, stalls = COALESCE(stalls, 0) + 1 WHERE nuq.queue_crawl_finished.locked_at <= now() - interval '1 minute' AND nuq.queue_crawl_finished.status = 'active'::nuq.job_status AND COALESCE(nuq.queue_crawl_finished.stalls, 0) >= 9 RETURNING id) SELECT pg_notify('nuq.queue_crawl_finished', (id::text || '|' || 'failed'::text)) FROM stallfail; \$\$); SELECT cron.schedule('nuq_queue_crawl_finished_reindex', '0 9 * * *', \$\$ REINDEX TABLE CONCURRENTLY nuq.queue_crawl_finished; \$\$); CREATE TABLE IF NOT EXISTS nuq.group_crawl ( id uuid NOT NULL, status nuq.group_status NOT NULL DEFAULT 'active'::nuq.group_status, created_at timestamptz NOT NULL DEFAULT now(), owner_id uuid NOT NULL, ttl int8 NOT NULL DEFAULT 86400000, expires_at timestamptz, CONSTRAINT group_crawl_pkey PRIMARY KEY (id) ); -- Index for group finish cron to find active groups CREATE INDEX IF NOT EXISTS idx_group_crawl_status ON nuq.group_crawl (status) WHERE status = 'active'::nuq.group_status; -- Index for backlog group_id lookups CREATE INDEX IF NOT EXISTS idx_queue_scrape_backlog_group_id ON nuq.queue_scrape_backlog (group_id); SELECT cron.schedule('nuq_group_crawl_finished', '15 seconds', \$\$ WITH finished_groups AS ( UPDATE nuq.group_crawl SET status = 'completed'::nuq.group_status, expires_at = now() + MAKE_INTERVAL(secs => nuq.group_crawl.ttl / 1000) WHERE status = 'active'::nuq.group_status AND NOT EXISTS ( SELECT 1 FROM nuq.queue_scrape WHERE nuq.queue_scrape.status IN ('active', 'queued') AND nuq.queue_scrape.group_id = nuq.group_crawl.id ) AND NOT EXISTS ( SELECT 1 FROM nuq.queue_scrape_backlog WHERE nuq.queue_scrape_backlog.group_id = nuq.group_crawl.id ) RETURNING id, owner_id ) INSERT INTO nuq.queue_crawl_finished (data, owner_id, group_id) SELECT '{}'::jsonb, finished_groups.owner_id, finished_groups.id FROM finished_groups; \$\$); SELECT cron.schedule('nuq_group_crawl_clean', '*/5 * * * *', \$\$ WITH cleaned_groups AS ( DELETE FROM nuq.group_crawl WHERE nuq.group_crawl.status = 'completed'::nuq.group_status AND nuq.group_crawl.expires_at < now() RETURNING * ), cleaned_jobs_queue_scrape AS ( DELETE FROM nuq.queue_scrape WHERE nuq.queue_scrape.group_id IN (SELECT id FROM cleaned_groups) ), cleaned_jobs_queue_scrape_backlog AS ( DELETE FROM nuq.queue_scrape_backlog WHERE nuq.queue_scrape_backlog.group_id IN (SELECT id FROM cleaned_groups) ), cleaned_jobs_crawl_finished AS ( DELETE FROM nuq.queue_crawl_finished WHERE nuq.queue_crawl_finished.group_id IN (SELECT id FROM cleaned_groups) ) SELECT 1; \$\$); EOFMT docker build -t hshq/firecrawl-db:v1 $HSHQ_BUILD_DIR/firecrawldb retVal=$? cd return $retVal } # LibreChat function installLibreChat() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory librechat "LibreChat" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName librechat-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName librechat-mongodb) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName librechat-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName librechat-meilisearch) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName librechat-ragapi) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName librechat-redis) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/librechat mkdir $HSHQ_STACKS_DIR/librechat/config mkdir $HSHQ_STACKS_DIR/librechat/data mkdir $HSHQ_STACKS_DIR/librechat/images mkdir $HSHQ_STACKS_DIR/librechat/uploads mkdir $HSHQ_STACKS_DIR/librechat/logs mkdir $HSHQ_STACKS_DIR/librechat/mongodata mkdir $HSHQ_STACKS_DIR/librechat/mongoexport mkdir $HSHQ_STACKS_DIR/librechat/db mkdir $HSHQ_STACKS_DIR/librechat/dbexport chmod 777 $HSHQ_STACKS_DIR/librechat/dbexport initServicesCredentials set +e addUserMailu alias $LIBRECHAT_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS LIBRECHAT_OIDC_CLIENT_SECRET_HASH=$(docker run --rm $IMG_AUTHELIA authelia crypto hash generate pbkdf2 --variant sha512 --password $LIBRECHAT_OIDC_CLIENT_SECRET | cut -d" " -f2) outputConfigLibreChat oidcBlock=$(cat $HOME/librechat.oidc) rm -f $HOME/librechat.oidc insertOIDCClientAuthelia librechat "$oidcBlock" installStack librechat librechat-app "Server listening on all interfaces" $HOME/librechat.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$LIBRECHAT_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_LIBRECHAT_APP Admin Login Info" -b "$FMLNAME_LIBRECHAT_APP Admin Email: $LIBRECHAT_ADMIN_EMAIL_ADDRESS\n$FMLNAME_LIBRECHAT_APP Admin Password: $LIBRECHAT_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" LIBRECHAT_INIT_ENV=true updateConfigVar LIBRECHAT_INIT_ENV $LIBRECHAT_INIT_ENV fi sleep 3 docker exec librechat-app sh -c "yes | npm run create-user $LIBRECHAT_ADMIN_EMAIL_ADDRESS \"LibreChat HSHQ Admin\" $LIBRECHAT_ADMIN_USERNAME $LIBRECHAT_ADMIN_PASSWORD --email-verified=true" > /dev/null 2>&1 addReadOnlyUserToDatabase LibreChat postgres librechat-db $LIBRECHAT_DATABASE_NAME $LIBRECHAT_DATABASE_USER $LIBRECHAT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/librechat/dbexport $LIBRECHAT_DATABASE_READONLYUSER $LIBRECHAT_DATABASE_READONLYUSER_PASSWORD addReadOnlyUserToDatabase LibreChat mongodb librechat-mongodb $LIBRECHAT_MONGODB_DATABASE $LIBRECHAT_MONGODB_USER $LIBRECHAT_MONGODB_USER_PASSWORD $HSHQ_STACKS_DIR/librechat/mongoexport $LIBRECHAT_MONGODB_READONLYUSER $LIBRECHAT_MONGODB_READONLYUSER_PASSWORD if [ -z "$FMLNAME_LIBRECHAT_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://librechat-app:3080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_LIBRECHAT_APP $MANAGETLS_LIBRECHAT_APP "$is_integrate_hshq" $NETDEFAULT_LIBRECHAT_APP "$inner_block" insertSubAuthelia $SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll librechat "$FMLNAME_LIBRECHAT_APP" $USERTYPE_LIBRECHAT_APP "https://$SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN" "librechat.png" "$(getHeimdallOrderFromSub $SUB_LIBRECHAT_APP $USERTYPE_LIBRECHAT_APP)" restartAllCaddyContainers checkAddDBConnection true librechat "$FMLNAME_LIBRECHAT_APP" postgres librechat-db $LIBRECHAT_DATABASE_NAME $LIBRECHAT_DATABASE_USER $LIBRECHAT_DATABASE_USER_PASSWORD checkAddDBConnection true librechat-mongodb "${FMLNAME_LIBRECHAT_APP}-MongoDB" mongo librechat-mongodb $LIBRECHAT_MONGODB_DATABASE $LIBRECHAT_MONGODB_USER $LIBRECHAT_MONGODB_USER_PASSWORD fi } function outputConfigLibreChat() { cat < $HOME/librechat-compose.yml $STACK_VERSION_PREFIX librechat $(getScriptStackVersion librechat) services: librechat-db: image: $(getScriptImageByContainerName librechat-db) container_name: librechat-db hostname: librechat-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-librechat-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.librechat-hourly-db.schedule=@every 1h" - "ofelia.job-exec.librechat-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.librechat-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.librechat-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.librechat-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.librechat-hourly-db.email-from=LibreChat Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.librechat-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.librechat-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.librechat-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.librechat-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.librechat-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.librechat-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.librechat-monthly-db.email-from=LibreChat Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.librechat-monthly-db.mail-only-on-error=false" librechat-mongodb: image: $(getScriptImageByContainerName librechat-mongodb) container_name: librechat-mongodb hostname: librechat-mongodb restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mongod --oplogSize 128 --quiet --auth networks: - int-librechat-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-librechat-mongodata:/data/db - v-librechat-mongoexport:/dump librechat-app: image: $(getScriptImageByContainerName librechat-app) container_name: librechat-app hostname: librechat-app user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - librechat-db - librechat-mongodb - librechat-ragapi - librechat-redis networks: - int-librechat-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/config/env:/app/.env - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/config/librechat.yaml:/app/librechat.yaml - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/images:/app/client/public/images - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/uploads:/app/uploads - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/logs:/app/logs librechat-meilisearch: image: $(getScriptImageByContainerName librechat-meilisearch) container_name: librechat-meilisearch hostname: librechat-meilisearch restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-librechat-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/librechat/meilidata:/meili_data librechat-ragapi: image: $(getScriptImageByContainerName librechat-ragapi) container_name: librechat-ragapi hostname: librechat-ragapi restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - librechat-db networks: - int-librechat-net - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro librechat-redis: image: $(getScriptImageByContainerName librechat-redis) container_name: librechat-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $LIBRECHAT_REDIS_PASSWORD --appendonly no networks: - int-librechat-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-librechat-mongodata: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/librechat/mongodata v-librechat-mongoexport: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/librechat/mongoexport networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-librechat-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/librechat.env TZ=\${PORTAINER_TZ} PYTHON_VER=python3.12 UID=$USERID GID=$GROUPID POSTGRES_DB=$LIBRECHAT_DATABASE_NAME POSTGRES_USER=$LIBRECHAT_DATABASE_USER POSTGRES_PASSWORD=$LIBRECHAT_DATABASE_USER_PASSWORD HOST=0.0.0.0 PORT=3080 MONGO_INITDB_ROOT_USERNAME=$LIBRECHAT_MONGODB_USER MONGO_INITDB_ROOT_PASSWORD=$LIBRECHAT_MONGODB_USER_PASSWORD MONGO_URI=mongodb://$LIBRECHAT_MONGODB_USER:$LIBRECHAT_MONGODB_USER_PASSWORD@librechat-mongodb:27017/${LIBRECHAT_MONGODB_DATABASE}?authSource=admin MEILI_HOST=http://librechat-meilisearch:7700 MEILI_NO_ANALYTICS=true MEILI_MASTER_KEY=$LIBRECHAT_MEILI_MASTER_KEY SEARCH=true RAG_PORT=8000 RAG_API_URL=http://librechat-ragapi:8000 DB_HOST=librechat-db CREDS_KEY=$LIBRECHAT_CREDS_KEY CREDS_IV=$LIBRECHAT_CREDS_IV DOMAIN_CLIENT=https://$SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN DOMAIN_SERVER=https://$SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN CONFIG_BYPASS_VALIDATION=false TITLE_CONVO=true ALLOW_SOCIAL_LOGIN=true OPENID_BUTTON_LABEL=Log in with Authelia OPENID_ISSUER=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/.well-known/openid-configuration OPENID_CLIENT_ID=librechat OPENID_CLIENT_SECRET=$LIBRECHAT_OIDC_CLIENT_SECRET OPENID_SESSION_SECRET=$(pwgen -c -n 32 1) OPENID_CALLBACK_URL=/oauth/openid/callback OPENID_SCOPE=openid profile email OPENID_IMAGE_URL=https://www.authelia.com/images/branding/logo-cropped.png OPENID_USE_END_SESSION_ENDPOINT=true JWT_SECRET=$LIBRECHAT_JWT_SECRET JWT_REFRESH_SECRET=$LIBRECHAT_JWT_REFRESH_SECRET EMAIL_HOST=$SMTP_HOSTNAME EMAIL_PORT=$SMTP_HOSTPORT EMAIL_ENCRYPTION=starttls EMAIL_ALLOW_SELFSIGNED=true EMAIL_FROM_NAME=LibreChat $(getAdminEmailName) EMAIL_FROM=$EMAIL_ADMIN_EMAIL_ADDRESS APP_TITLE=LibreChat USE_REDIS=true REDIS_URI=redis://:$LIBRECHAT_REDIS_PASSWORD@librechat-redis:6379 ENDPOINTS=custom,agents OPENAI_API_KEY=user_provided RAG_OPENAI_API_KEY=$LITELLM_AUTH_KEY RAG_OPENAI_BASEURL=http://litellm-proxy:4000 EMBEDDINGS_PROVIDER=openai EMBEDDINGS_MODEL=text-embedding-ada-002 NODE_ENV=production NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt SEARXNG_INSTANCE_URL=http://searxng-app:8080 SEARXNG_API_KEY=nokey FIRECRAWL_API_URL=http://firecrawl-api:3002 FIRECRAWL_API_KEY=$FIRECRAWL_API_KEY FIRECRAWL_VERSION=v2 JINA_API_KEY=$LITELLM_AUTH_KEY JINA_API_URL=http://litellm-proxy:4000/v1/rerank EOFMT cp $HOME/librechat.env $HSHQ_STACKS_DIR/librechat/config/env cat < $HOME/librechat.oidc # Authelia OIDC Client librechat BEGIN - client_id: librechat client_name: LibreChat client_secret: $LIBRECHAT_OIDC_CLIENT_SECRET_HASH public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth redirect_uris: - https://$SUB_LIBRECHAT_APP.$HOMESERVER_DOMAIN/oauth/openid/callback scopes: - openid - profile - email - offline_access response_types: - code grant_types: - authorization_code - refresh_token access_token_signed_response_alg: none userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_post # Authelia OIDC Client librechat END EOFIM cat < $HSHQ_STACKS_DIR/librechat/config/librechat.yaml version: 1.2.8 cache: true interface: mcpServers: placeholder: "MCP Servers" use: true create: true share: true trustCheckbox: label: "I trust this server" subLabel: "Only enable servers you trust" modelSelect: true parameters: true sidePanel: true presets: true prompts: true bookmarks: true multiConvo: true agents: true customWelcome: "Hey {{user.name}}! Welcome to LibreChat" runCode: true webSearch: true fileSearch: true fileCitations: true peoplePicker: users: true groups: true roles: true marketplace: use: true endpoints: custom: - name: "LiteLLM" apiKey: "$LITELLM_AUTH_KEY" baseURL: "http://litellm-proxy:4000/v1" models: default: - "Chat" - "Fast" - "Thinking" - "LongContext" - "Vision" fetch: true titleConvo: true titleModel: "current_model" summarize: true forcePrompt: false modelDisplayLabel: "LiteLLM" webSearch: searxngInstanceUrl: "\${SEARXNG_INSTANCE_URL}" searxngApiKey: "\${SEARXNG_API_KEY}" searchProvider: "searxng" firecrawlApiKey: "\${FIRECRAWL_API_KEY}" firecrawlApiUrl: "\${FIRECRAWL_API_URL}" firecrawlVersion: "\${FIRECRAWL_VERSION}" scraperProvider: "firecrawl" jinaApiKey: "\${JINA_API_KEY}" jinaApiUrl: "\${JINA_API_URL}" rerankerType: "jina" scraperTimeout: 5000 safeSearch: 1 EOFIM } function performUpdateLibreChat() { perform_stack_name=librechat prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/pgvector/pgvector:0.8.0-pg15-trixie,mirror.gcr.io/mongo:8.0.17,ghcr.io/danny-avila/librechat:v0.8.1,mirror.gcr.io/getmeili/meilisearch:v1.31.0,ghcr.io/danny-avila/librechat-rag-api-dev-lite:v0.7.0,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/pgvector/pgvector:0.8.0-pg15-trixie,mirror.gcr.io/pgvector/pgvector:0.8.0-pg15-trixie" image_update_map[1]="mirror.gcr.io/mongo:8.0.17,mirror.gcr.io/mongo:8.0.17" image_update_map[2]="ghcr.io/danny-avila/librechat:v0.8.1,ghcr.io/danny-avila/librechat:v0.8.1" image_update_map[3]="mirror.gcr.io/getmeili/meilisearch:v1.31.0,mirror.gcr.io/getmeili/meilisearch:v1.31.0" image_update_map[4]="ghcr.io/danny-avila/librechat-rag-api-dev-lite:v0.7.0,ghcr.io/danny-avila/librechat-rag-api-dev-lite:v0.7.0" image_update_map[5]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # Crawl4AI function installCrawl4AI() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory crawl4ai "Crawl4AI" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName crawl4ai-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName crawl4ai-proxy) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/crawl4ai initServicesCredentials set +e outputConfigCrawl4AI # If we need a string to monitor, its this: "Application startup complete" installStack crawl4ai crawl4ai-app "" $HOME/crawl4ai.env 1 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 3 if [ -z "$FMLNAME_CRAWL4AI_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_CRAWL4AI_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_BASEHEADER\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>header {\n" inner_block=$inner_block">>>>>>Content-Security-Policy \"default-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdnjs.cloudflare.com cdn.tailwindcss.com; font-src 'self' fonts.gstatic.com; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' cdnjs.cloudflare.com fonts.googleapis.com; img-src 'self' *.$HOMESERVER_DOMAIN img.shields.io data:; frame-src 'self' *.$HOMESERVER_DOMAIN data: blob:; connect-src 'self' *.$HOMESERVER_DOMAIN data:; object-src 'none'; frame-ancestors 'self' *.$HOMESERVER_DOMAIN; upgrade-insecure-requests;\"\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://crawl4ai-app:11235 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_CRAWL4AI_APP $MANAGETLS_CRAWL4AI_APP "$is_integrate_hshq" $NETDEFAULT_CRAWL4AI_APP "$inner_block" insertSubAuthelia $SUB_CRAWL4AI_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll crawl4ai "$FMLNAME_CRAWL4AI_APP" $USERTYPE_CRAWL4AI_APP "https://$SUB_CRAWL4AI_APP.$HOMESERVER_DOMAIN/playground/" "crawl4ai.png" "$(getHeimdallOrderFromSub $SUB_CRAWL4AI_APP $USERTYPE_CRAWL4AI_APP)" restartAllCaddyContainers fi } function outputConfigCrawl4AI() { cat < $HOME/crawl4ai-compose.yml $STACK_VERSION_PREFIX crawl4ai $(getScriptStackVersion crawl4ai) services: crawl4ai-app: image: $(getScriptImageByContainerName crawl4ai-app) container_name: crawl4ai-app hostname: crawl4ai-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net healthcheck: test: ["CMD", "curl", "-f", "http://localhost:11235/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /dev/shm:/dev/shm crawl4ai-proxy: image: $(getScriptImageByContainerName crawl4ai-proxy) container_name: crawl4ai-proxy hostname: crawl4ai-proxy restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro networks: dock-proxy-net: name: dock-proxy external: true dock-ext-net: name: dock-ext external: true dock-aipriv-net: name: dock-aipriv external: true EOFMT cat < $HOME/crawl4ai.env TZ=\${PORTAINER_TZ} OPENAI_BASE_URL=http://litellm-proxy:4000 OPENAI_API_KEY=$LITELLM_AUTH_KEY LLM_PROVIDER=Chat LLM_TEMPERATURE=0.5 LISTEN_PORT=8000 CRAWL4AI_ENDPOINT=http://crawl4ai-app:11235/crawl EOFMT } function performUpdateCrawl4AI() { perform_stack_name=crawl4ai prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/unclecode/crawl4ai:0.7.8,ghcr.io/lennyerik/crawl4ai-proxy:latest image_update_map[0]="mirror.gcr.io/unclecode/crawl4ai:0.7.8,mirror.gcr.io/unclecode/crawl4ai:0.8.6" image_update_map[1]="ghcr.io/lennyerik/crawl4ai-proxy:latest,ghcr.io/lennyerik/crawl4ai-proxy:latest" ;; 2) newVer=v3 curImageList=mirror.gcr.io/unclecode/crawl4ai:0.8.6,ghcr.io/lennyerik/crawl4ai-proxy:latest image_update_map[0]="mirror.gcr.io/unclecode/crawl4ai:0.8.6,ghcr.io/homeserverhq/crawl4ai:v0.9.5" image_update_map[1]="ghcr.io/lennyerik/crawl4ai-proxy:latest,ghcr.io/homeserverhq/crawl4ai-proxy:v0.1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateCrawl4AIV3 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 3) newVer=v3 curImageList=ghcr.io/homeserverhq/crawl4ai:v0.9.5,ghcr.io/homeserverhq/crawl4ai-proxy:v0.1 image_update_map[0]="ghcr.io/homeserverhq/crawl4ai:v0.9.5,ghcr.io/homeserverhq/crawl4ai:v0.9.5" image_update_map[1]="ghcr.io/homeserverhq/crawl4ai-proxy:v0.1,ghcr.io/homeserverhq/crawl4ai-proxy:v0.1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfUpdateCrawl4AIV3() { rm -f $HOME/crawl4ai.env cat < $HOME/crawl4ai.env TZ=\${PORTAINER_TZ} OPENAI_BASE_URL=http://litellm-proxy:4000 OPENAI_API_KEY=$LITELLM_AUTH_KEY LLM_PROVIDER=Chat LLM_TEMPERATURE=0.5 CRAWL4AI_URL=http://crawl4ai-app:11235 EOFMT } # Ollama function installOllama() { set +e if [ -d $HSHQ_STACKS_DIR/aistack ]; then echo "ERROR: You must uninstall the aistack in order to continue. It has been deprecated and will cause conflicts with this stack." return 1 fi is_integrate_hshq=$1 checkDeleteStackAndDirectory ollama "Ollama" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ollama-server) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/ollama mkdir $HSHQ_STACKS_DIR/ollama/code mkdir -p $HSHQ_AIMODELS_DIR/ollama initServicesCredentials set +e outputConfigOllama installStack ollama ollama-server "Listening on" $HOME/ollama.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$OLLAMA_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_OLLAMA_SERVER API Key Info" -b "If accessing Ollama externally, you will need the following info: \n\nOllama URL: https://$SUB_OLLAMA_SERVER.$HOMESERVER_DOMAIN\nOllama API Key: $OLLAMA_API_KEY\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" OLLAMA_INIT_ENV=true updateConfigVar OLLAMA_INIT_ENV $OLLAMA_INIT_ENV fi sleep 3 if [ -z "$FMLNAME_OLLAMA_SERVER" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e # Thanks to https://github.com/g1ibby/ollama-auth for the idea on how to set up # an API key for Ollama (specifically in this case when accessed externally). inner_block="" inner_block=$inner_block">>https://$SUB_OLLAMA_SERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>@auth_bearer {\n" inner_block=$inner_block">>>>>>header Authorization \"Bearer $OLLAMA_API_KEY\"\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>@auth_query {\n" inner_block=$inner_block">>>>>>query api-key=$OLLAMA_API_KEY\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>@auth_header {\n" inner_block=$inner_block">>>>>>header X-API-Key $OLLAMA_API_KEY\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>handle @auth_bearer {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://ollama-server:11434 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>handle @auth_query {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://ollama-server:11434 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>handle @auth_header {\n" inner_block=$inner_block">>>>>>>>reverse_proxy http://ollama-server:11434 {\n" inner_block=$inner_block">>>>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>>>}\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>>>respond 403 {\n" inner_block=$inner_block">>>>>>>>body \"Forbidden: Invalid API Key\"\n" inner_block=$inner_block">>>>>>>>close\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OLLAMA_SERVER $MANAGETLS_OLLAMA_SERVER "$is_integrate_hshq" $NETDEFAULT_OLLAMA_SERVER "$inner_block" insertSubAuthelia $SUB_OLLAMA_SERVER.$HOMESERVER_DOMAIN bypass if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcUptimeKuma ollama "$FMLNAME_OLLAMA_SERVER" $USERTYPE_OLLAMA_SERVER "https://$SUB_OLLAMA_SERVER.$HOMESERVER_DOMAIN/api/tags?api-key=$OLLAMA_API_KEY" true restartAllCaddyContainers fi } function outputConfigOllama() { cat < $HOME/ollama-compose.yml $STACK_VERSION_PREFIX ollama $(getScriptStackVersion ollama) services: ollama-server: image: $(getScriptImageByContainerName ollama-server) container_name: ollama-server hostname: ollama-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ollama/code:/code - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/ollama:/root/.ollama networks: dock-proxy-net: name: dock-proxy external: true dock-aipriv-net: name: dock-aipriv external: true dock-ext-net: name: dock-ext external: true EOFMT cat < $HOME/ollama.env TZ=\${PORTAINER_TZ} OLLAMA_HOST=0.0.0.0 OLLAMA_NUM_GPU=999 OLLAMA_API_KEY=$OLLAMA_API_KEY EOFMT } function performUpdateOllama() { perform_stack_name=ollama prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v3 curImageList=mirror.gcr.io/ollama/ollama:0.13.5 image_update_map[0]="mirror.gcr.io/ollama/ollama:0.13.5,mirror.gcr.io/ollama/ollama:0.20.5" ;; 2) newVer=v3 curImageList=mirror.gcr.io/ollama/ollama:0.17.7 image_update_map[0]="mirror.gcr.io/ollama/ollama:0.17.7,mirror.gcr.io/ollama/ollama:0.20.5" ;; 3) newVer=v3 curImageList=mirror.gcr.io/ollama/ollama:0.20.5 image_update_map[0]="mirror.gcr.io/ollama/ollama:0.20.5,mirror.gcr.io/ollama/ollama:0.20.5" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # OpenWebUI function installOpenWebUI() { set +e if [ -d $HSHQ_STACKS_DIR/aistack ]; then echo "ERROR: You must uninstall the aistack in order to continue. It has been deprecated and will cause conflicts with this stack." return 1 fi is_integrate_hshq=$1 checkDeleteStackAndDirectory openwebui "OpenWebUI" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openwebui-db) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openwebui-app) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openwebui-redis) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openwebui-qdrant) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openwebui-openterminal) if [ $? -ne 0 ]; then return 1 fi buildOrPullImage $(getScriptImageByContainerName openwebui-mcpo) if [ $? -ne 0 ]; then return 1 fi #buildOrPullImage $(getScriptImageByContainerName openwebui-pipelines) #if [ $? -ne 0 ]; then # return 1 #fi buildOrPullImage $(getScriptImageByContainerName openwebui-mcp) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/openwebui mkdir $HSHQ_STACKS_DIR/openwebui/config mkdir $HSHQ_STACKS_DIR/openwebui/data mkdir $HSHQ_STACKS_DIR/openwebui/db mkdir $HSHQ_STACKS_DIR/openwebui/dbexport mkdir $HSHQ_STACKS_DIR/openwebui/qdrant mkdir $HSHQ_STACKS_DIR/openwebui/openterminal mkdir $HSHQ_STACKS_DIR/openwebui/mcpo mkdir $HSHQ_STACKS_DIR/openwebui/pipelines chmod 777 $HSHQ_STACKS_DIR/openwebui/dbexport initServicesCredentials set +e addUserMailu alias $OPENWEBUI_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS OPENWEBUI_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $OPENWEBUI_ADMIN_PASSWORD | tr -d ':\n') outputConfigOpenWebUI oidcBlock=$(cat $HOME/openwebui.oidc) rm -f $HOME/openwebui.oidc insertOIDCClientAuthelia openwebui "$oidcBlock" installStack openwebui openwebui-app "building the best AI user interface" $HOME/openwebui.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$OPENWEBUI_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_OPENWEBUI_APP Admin Login Info" -b "$FMLNAME_OPENWEBUI_APP Admin Email: $OPENWEBUI_ADMIN_EMAIL_ADDRESS\n$FMLNAME_OPENWEBUI_APP Admin Password: $OPENWEBUI_ADMIN_PASSWORD\n$FMLNAME_OPENWEBUI_QDRANT Password: $OPENWEBUI_QDRANT_API_KEY\nOpenWebUI MCPO API Key: $OPENWEBUI_MCPO_API_KEY\nOpenWebUI Pipelines API Key: $OPENWEBUI_PIPELINES_API_KEY" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" OPENWEBUI_INIT_ENV=true updateConfigVar OPENWEBUI_INIT_ENV $OPENWEBUI_INIT_ENV fi sleep 3 set +e addReadOnlyUserToDatabase OpenWebUI postgres openwebui-db $OPENWEBUI_DATABASE_NAME $OPENWEBUI_DATABASE_USER $OPENWEBUI_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/openwebui/dbexport $OPENWEBUI_DATABASE_READONLYUSER $OPENWEBUI_DATABASE_READONLYUSER_PASSWORD performDBConfigOpenWebUI addMCPServerLiteLLM "openwebui" "owui" "http://openwebui-mcp:8001/mcp" http none "" if [ -z "$FMLNAME_OPENWEBUI_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_BASEHEADER\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n" inner_block=$inner_block">>>>@owuiFiles path /api/v1/files/*/content\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>#OWUI_FILES_REPLACE_LINE_1\n" inner_block=$inner_block">>>>#OWUI_FILES_REPLACE_LINE_2\n" inner_block=$inner_block">>>>#OWUI_FILES_REPLACE_LINE_3\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://openwebui-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENWEBUI_APP $MANAGETLS_OPENWEBUI_APP "$is_integrate_hshq" $NETDEFAULT_OPENWEBUI_APP "$inner_block" # Special Case Replacement sed -i "s/#OWUI_FILES_REPLACE_LINE_1/header @owuiFiles >Content-Type \"text\/plain; charset=utf-8\"/" $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip sed -i "s/#OWUI_FILES_REPLACE_LINE_2/header @owuiFiles >Content-Disposition \"inline\"/" $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip sed -i "s/#OWUI_FILES_REPLACE_LINE_3/header @owuiFiles >Cache-Control \"no-store, no-cache, must-revalidate, max-age=0\"/" $HSHQ_STACKS_DIR/caddy-common/snippets/svcs.snip insertSubAuthelia $SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://openwebui-qdrant:6333 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENWEBUI_QDRANT $MANAGETLS_OPENWEBUI_QDRANT "$is_integrate_hshq" $NETDEFAULT_OPENWEBUI_QDRANT "$inner_block" insertSubAuthelia $SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll openwebui "$FMLNAME_OPENWEBUI_APP" $USERTYPE_OPENWEBUI_APP "https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN" "openwebui.png" "$(getHeimdallOrderFromSub $SUB_OPENWEBUI_APP $USERTYPE_OPENWEBUI_APP)" insertEnableSvcAll openwebui "$FMLNAME_OPENWEBUI_QDRANT" $USERTYPE_OPENWEBUI_QDRANT "https://$SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN/dashboard" "qdrant.png" "$(getHeimdallOrderFromSub $SUB_OPENWEBUI_QDRANT $USERTYPE_OPENWEBUI_QDRANT)" restartAllCaddyContainers checkAddDBConnection true openwebui "$FMLNAME_OPENWEBUI_APP" postgres openwebui-db $OPENWEBUI_DATABASE_NAME $OPENWEBUI_DATABASE_USER $OPENWEBUI_DATABASE_USER_PASSWORD fi importConfigsOpenWebUI } function outputConfigOpenWebUI() { outputComposeOpenWebUI outputEnvOpenWebUI OPENWEBUI_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $OPENWEBUI_OIDC_CLIENT_SECRET | tr -d ':\n') cat < $HOME/openwebui.oidc # Authelia OIDC Client openwebui BEGIN - client_id: $OPENWEBUI_OIDC_CLIENT_ID client_name: OpenWebUI client_secret: '$OPENWEBUI_OIDC_CLIENT_SECRET_HASH' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth redirect_uris: - https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/oauth/oidc/callback scopes: - openid - profile - email - groups userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client openwebui END EOFIM cat < $HSHQ_STACKS_DIR/openwebui/mcpo/config.json { "mcpServers": { "placeholder": { "command": "echo", "args": ["MCP proxy running"] } } } EOFOW } function outputComposeOpenWebUI() { cat < $HOME/openwebui-compose.yml $STACK_VERSION_PREFIX openwebui $(getScriptStackVersion openwebui) services: openwebui-db: image: $(getScriptImageByContainerName openwebui-db) container_name: openwebui-db hostname: openwebui-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-openwebui-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.openwebui-hourly-db.schedule=@every 1h" - "ofelia.job-exec.openwebui-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.openwebui-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openwebui-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openwebui-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openwebui-hourly-db.email-from=OpenWebUI Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openwebui-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.openwebui-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.openwebui-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.openwebui-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openwebui-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openwebui-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openwebui-monthly-db.email-from=OpenWebUI Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openwebui-monthly-db.mail-only-on-error=false" openwebui-qdrant: image: $(getScriptImageByContainerName openwebui-qdrant) container_name: openwebui-qdrant hostname: openwebui-qdrant restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openwebui-net - dock-dbs-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/qdrant:/qdrant/storage openwebui-app: image: $(getScriptImageByContainerName openwebui-app) container_name: openwebui-app hostname: openwebui-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - openwebui-db networks: - int-openwebui-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/data:/app/backend/data openwebui-openterminal: image: $(getScriptImageByContainerName openwebui-openterminal) container_name: openwebui-openterminal hostname: openwebui-openterminal restart: unless-stopped env_file: stack.env networks: - int-openwebui-net - dock-aipriv-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openwebui-openterminal:/home openwebui-mcpo: image: $(getScriptImageByContainerName openwebui-mcpo) container_name: openwebui-mcpo hostname: openwebui-mcpo restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: --config /app/config/config.json --api-key "$OPENWEBUI_MCPO_API_KEY" --hot-reload networks: - int-openwebui-net - dock-aipriv-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/mcpo:/app/config # openwebui-pipelines: # image: $(getScriptImageByContainerName openwebui-pipelines) # container_name: openwebui-pipelines # hostname: openwebui-pipelines # restart: unless-stopped # env_file: stack.env # security_opt: # - no-new-privileges:true # networks: # - int-openwebui-net # - dock-aipriv-net # volumes: # - /etc/localtime:/etc/localtime:ro # - /etc/timezone:/etc/timezone:ro # - /etc/ssl/certs:/etc/ssl/certs:ro # - /usr/share/ca-certificates:/usr/share/ca-certificates:ro # - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro # - v-openwebui-pipelines:/app/pipelines openwebui-mcp: image: $(getScriptImageByContainerName openwebui-mcp) container_name: openwebui-mcp hostname: openwebui-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openwebui-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro openwebui-redis: image: $(getScriptImageByContainerName openwebui-redis) container_name: openwebui-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $OPENWEBUI_REDIS_PASSWORD --appendonly no networks: - int-openwebui-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-openwebui-openterminal: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/openterminal v-openwebui-pipelines: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/pipelines networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-openwebui-net: driver: bridge internal: true ipam: driver: default EOFMT } function outputEnvOpenWebUI() { cat < $HOME/openwebui.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$OPENWEBUI_DATABASE_NAME POSTGRES_USER=$OPENWEBUI_DATABASE_USER POSTGRES_PASSWORD=$OPENWEBUI_DATABASE_USER_PASSWORD DATABASE_URL=postgresql://$OPENWEBUI_DATABASE_USER:$OPENWEBUI_DATABASE_USER_PASSWORD@openwebui-db:5432/$OPENWEBUI_DATABASE_NAME WEBUI_AUTH=True ENABLE_SIGNUP=False ENABLE_LOGIN_FORM=True DEFAULT_USER_ROLE=user WEBUI_URL=https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN WEBUI_SECRET_KEY=$OPENWEBUI_SECRET_KEY ENABLE_OAUTH_SIGNUP=true ADMIN_EMAIL=$OPENWEBUI_ADMIN_EMAIL_ADDRESS ENV=prod RAG_EMBEDDING_ENGINE=openai RAG_EMBEDDING_MODEL=Embed ENABLE_KB_EXEC=true OPENAI_API_BASE_URL=http://litellm-proxy:4000/v1 OPENAI_API_KEY=$LITELLM_AUTH_KEY OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true OAUTH_PROVIDER_NAME=Authelia OPENID_PROVIDER_URL=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN/.well-known/openid-configuration OAUTH_CLIENT_ID=$OPENWEBUI_OIDC_CLIENT_ID OAUTH_CLIENT_SECRET=$OPENWEBUI_OIDC_CLIENT_SECRET OAUTH_SCOPES=openid groups email profile OPENID_REDIRECT_URI=https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN/oauth/oidc/callback ENABLE_OAUTH_GROUP_MANAGEMENT=true OAUTH_GROUP_CLAIM=groups ENABLE_OAUTH_GROUP_CREATION=true REDIS_HOST=openwebui-redis REDIS_PORT=6379 REDIS_PASSWORD=$OPENWEBUI_REDIS_PASSWORD REDIS_CONNECTION_STRING=redis://:$OPENWEBUI_REDIS_PASSWORD@openwebui-redis:6379/0 PYTHON_VER=python3.12 DEFAULT_LOCALE=en GLOBAL_LOG_LEVEL=INFO ENABLE_FS_SKILLS=true QDRANT__SERVICE__API_KEY=$OPENWEBUI_QDRANT_API_KEY QDRANT__SERVICE__ENABLE_API_KEY_AUTHORIZATION=true ENABLE_API_KEYS=true USER_PERMISSIONS_FEATURES_API_KEYS=true RAG_SYSTEM_CONTEXT=False ENABLE_RAG_LOCAL_WEB_FETCH=True VECTOR_DB=qdrant QDRANT_URI=http://openwebui-qdrant:6333 QDRANT_API_KEY=$OPENWEBUI_QDRANT_API_KEY QDRANT__TELEMETRY_DISABLED=true OPEN_TERMINAL_MULTI_USER=true OPEN_TERMINAL_API_KEY=$OPENWEBUI_OPENTERMINAL_API_KEY OPENWEBUI_MCPO_API_KEY=$OPENWEBUI_MCPO_API_KEY PIPELINES_API_KEY=$OPENWEBUI_PIPELINES_API_KEY OPENWEBUI_URL=http://openwebui-app:8080 MCP_TRANSPORT=http MCP_HTTP_PORT=8001 OPENWEBUI_ALLOWED_HOSTS=* CORS_ALLOWED_ORIGINS=* ALLOW_IFRAME_EMBEDDING=true TRUST_PROXY=true PROXY_URL=https://$SUB_OPENWEBUI_APP.$HOMESERVER_DOMAIN OPENWEBUI_BASE_URL=http://openwebui-app:8080 MCP_SERVER_PORT=80 ALLOW_ALL_AGGREGATE=false IS_STATEFUL=false EOFMT } function performDBConfigOpenWebUI() { return } function importConfigsOpenWebUI() { return } function performUpdateOpenWebUI() { perform_stack_name=openwebui prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/open-webui/open-webui:0.6.43,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/open-webui/open-webui:0.6.43,hshq/openwebui-unstupid:v1" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfOpenWebUIUpdateV2 perform_update_report="${perform_update_report}$stack_upgrade_report" performV2IntegrationsOpenWebUI return ;; 2) newVer=v3 curImageList=mirror.gcr.io/postgres:16.9-bookworm,hshq/openwebui-unstupid:v1,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/pipelines:main,hshq/openwebui-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="hshq/openwebui-unstupid:v1,hshq/openwebui-app:v2" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged" image_update_map[4]="ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/open-terminal:0.11.34" image_update_map[5]="ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/mcpo:main" image_update_map[6]="ghcr.io/open-webui/pipelines:main,ghcr.io/open-webui/pipelines:main" image_update_map[7]="hshq/openwebui-mcp:v1,hshq/openwebui-mcp:v1" ;; 3) newVer=v4 curImageList=mirror.gcr.io/postgres:16.9-bookworm,hshq/openwebui-app:v2,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/pipelines:main,hshq/openwebui-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="hshq/openwebui-app:v2,hshq/openwebui-app:v3" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged" image_update_map[4]="ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/open-terminal:0.11.34" image_update_map[5]="ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/mcpo:main" image_update_map[6]="ghcr.io/open-webui/pipelines:main,ghcr.io/open-webui/pipelines:main" image_update_map[7]="hshq/openwebui-mcp:v1,hshq/openwebui-mcp:v1" ;; 4) newVer=v4 curImageList=mirror.gcr.io/postgres:16.9-bookworm,hshq/openwebui-app:v3,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/pipelines:main,hshq/openwebui-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="hshq/openwebui-app:v3,hshq/openwebui-app:v3" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged" image_update_map[4]="ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/open-terminal:0.11.34" image_update_map[5]="ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/mcpo:main" image_update_map[6]="ghcr.io/open-webui/pipelines:main,ghcr.io/open-webui/pipelines:main" image_update_map[7]="hshq/openwebui-mcp:v1,hshq/openwebui-mcp:v1" ;; 5) newVer=v6 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/homeserverhq/open-webui:v0.11.0,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/pipelines:main,hshq/openwebui-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/homeserverhq/open-webui:v0.11.0,ghcr.io/homeserverhq/open-webui:v0.12.0" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged" image_update_map[4]="ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/open-terminal:0.11.34" image_update_map[5]="ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/mcpo:main" image_update_map[6]="ghcr.io/open-webui/pipelines:main,ghcr.io/open-webui/pipelines:main" image_update_map[7]="hshq/openwebui-mcp:v1,hshq/openwebui-mcp:v1" ;; 6) newVer=v7 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/homeserverhq/open-webui:v0.12.0,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/pipelines:main,hshq/openwebui-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/homeserverhq/open-webui:v0.12.0,ghcr.io/homeserverhq/open-webui:v0.13.0" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged,mirror.gcr.io/qdrant/qdrant:v1.20.0-unprivileged" image_update_map[4]="ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/open-terminal:0.17.4" image_update_map[5]="ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/mcpo:main" image_update_map[6]="ghcr.io/open-webui/pipelines:main,ghcr.io/open-webui/pipelines:main" image_update_map[7]="hshq/openwebui-mcp:v1,hshq/openwebui-mcp:v1" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateOpenWebUIV7 perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 7) newVer=v7 curImageList=mirror.gcr.io/postgres:16.9-bookworm,ghcr.io/homeserverhq/open-webui:v0.13.0,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/qdrant/qdrant:v1.20.0-unprivileged,ghcr.io/open-webui/open-terminal:0.11.34,ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/pipelines:main,hshq/openwebui-mcp:v1 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="ghcr.io/homeserverhq/open-webui:v0.13.0,ghcr.io/homeserverhq/open-webui:v0.13.0" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/qdrant/qdrant:v1.20.0-unprivileged,mirror.gcr.io/qdrant/qdrant:v1.20.0-unprivileged" image_update_map[4]="ghcr.io/open-webui/open-terminal:0.17.4,ghcr.io/open-webui/open-terminal:0.17.4" image_update_map[5]="ghcr.io/open-webui/mcpo:main,ghcr.io/open-webui/mcpo:main" image_update_map[6]="hshq/openwebui-mcp:v1,hshq/openwebui-mcp:v1" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function outputComposeV2OpenWebUI() { cat < $HOME/openwebui-compose.yml $STACK_VERSION_PREFIX openwebui v2 services: openwebui-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: openwebui-db hostname: openwebui-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-openwebui-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.openwebui-hourly-db.schedule=@every 1h" - "ofelia.job-exec.openwebui-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.openwebui-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openwebui-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openwebui-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openwebui-hourly-db.email-from=OpenWebUI Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openwebui-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.openwebui-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.openwebui-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.openwebui-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openwebui-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openwebui-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openwebui-monthly-db.email-from=OpenWebUI Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openwebui-monthly-db.mail-only-on-error=false" openwebui-qdrant: image: mirror.gcr.io/qdrant/qdrant:v1.17.1-unprivileged container_name: openwebui-qdrant hostname: openwebui-qdrant restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openwebui-net - dock-dbs-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/qdrant:/qdrant/storage openwebui-app: image: hshq/openwebui-unstupid:v1 container_name: openwebui-app hostname: openwebui-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - openwebui-db networks: - int-openwebui-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/data:/app/backend/data openwebui-openterminal: image: ghcr.io/open-webui/open-terminal:0.11.34 container_name: openwebui-openterminal hostname: openwebui-openterminal restart: unless-stopped env_file: stack.env networks: - int-openwebui-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openwebui-openterminal:/home openwebui-mcpo: image: ghcr.io/open-webui/mcpo:main container_name: openwebui-mcpo hostname: openwebui-mcpo restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: --config /app/config/config.json --api-key "$OPENWEBUI_MCPO_API_KEY" --hot-reload networks: - int-openwebui-net - dock-aipriv-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/mcpo:/app/config openwebui-pipelines: image: ghcr.io/open-webui/pipelines:main container_name: openwebui-pipelines hostname: openwebui-pipelines restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openwebui-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openwebui-pipelines:/app/pipelines openwebui-mcp: image: hshq/openwebui-mcp:v1 container_name: openwebui-mcp hostname: openwebui-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openwebui-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro openwebui-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: openwebui-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $OPENWEBUI_REDIS_PASSWORD --appendonly no networks: - int-openwebui-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-openwebui-openterminal: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/openterminal v-openwebui-pipelines: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/pipelines networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-openwebui-net: driver: bridge internal: true ipam: driver: default EOFMT } function mfOpenWebUIUpdateV2() { set +e pullImage $(getScriptImageByContainerName openwebui-qdrant) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName openwebui-openterminal) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName openwebui-mcpo) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName openwebui-pipelines) if [ $? -ne 0 ]; then return 1 fi buildImageOpenWebUIMCPV1 if [ $? -ne 0 ]; then return 1 fi mkdir -p $HSHQ_STACKS_DIR/openwebui/qdrant mkdir -p $HSHQ_STACKS_DIR/openwebui/openterminal mkdir -p $HSHQ_STACKS_DIR/openwebui/mcpo mkdir -p $HSHQ_STACKS_DIR/openwebui/pipelines initServicesCredentials outputComposeV2OpenWebUI grep -q "QDRANT_URI" $HOME/openwebui.env if [ $? -ne 0 ]; then echo "QDRANT__SERVICE__API_KEY=$OPENWEBUI_QDRANT_API_KEY" >> $HOME/openwebui.env echo "QDRANT__SERVICE__ENABLE_API_KEY_AUTHORIZATION=true" >> $HOME/openwebui.env echo "ENABLE_API_KEYS=true" >> $HOME/openwebui.env echo "USER_PERMISSIONS_FEATURES_API_KEYS=true" >> $HOME/openwebui.env echo "RAG_SYSTEM_CONTEXT=False" >> $HOME/openwebui.env echo "ENABLE_RAG_LOCAL_WEB_FETCH=True" >> $HOME/openwebui.env echo "VECTOR_DB=qdrant" >> $HOME/openwebui.env echo "QDRANT_URI=http://openwebui-qdrant:6333" >> $HOME/openwebui.env echo "QDRANT_API_KEY=$OPENWEBUI_QDRANT_API_KEY" >> $HOME/openwebui.env echo "OPEN_TERMINAL_MULTI_USER=true" >> $HOME/openwebui.env echo "OPEN_TERMINAL_API_KEY=$OPENWEBUI_OPENTERMINAL_API_KEY" >> $HOME/openwebui.env echo "OPENWEBUI_MCPO_API_KEY=$OPENWEBUI_MCPO_API_KEY" >> $HOME/openwebui.env echo "PIPELINES_API_KEY=$OPENWEBUI_PIPELINES_API_KEY" >> $HOME/openwebui.env echo "OPENWEBUI_URL=http://openwebui-app:8080" >> $HOME/openwebui.env echo "MCP_TRANSPORT=http" >> $HOME/openwebui.env echo "MCP_HTTP_PORT=80" >> $HOME/openwebui.env fi cat < $HSHQ_STACKS_DIR/openwebui/mcpo/config.json { "mcpServers": { "placeholder": { "command": "echo", "args": ["MCP proxy running"] } } } EOFOW addMCPServerLiteLLM "openwebui" "owui" "http://openwebui-mcp:8001/mcp" http none "" inner_block="" inner_block=$inner_block">>https://$SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://openwebui-qdrant:6333 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_OPENWEBUI_QDRANT $MANAGETLS_OPENWEBUI_QDRANT "$is_integrate_hshq" $NETDEFAULT_OPENWEBUI_QDRANT "$inner_block" insertSubAuthelia $SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} restartAllCaddyContainers insertEnableSvcAll openwebui "$FMLNAME_OPENWEBUI_QDRANT" $USERTYPE_OPENWEBUI_QDRANT "https://$SUB_OPENWEBUI_QDRANT.$HOMESERVER_DOMAIN/dashboard" "qdrant.png" "$(getHeimdallOrderFromSub $SUB_OPENWEBUI_QDRANT $USERTYPE_OPENWEBUI_QDRANT)" sendEmail -s "$FMLNAME_OPENWEBUI_QDRANT Login Info" -b "$FMLNAME_OPENWEBUI_QDRANT Password: $OPENWEBUI_QDRANT_API_KEY\nOpenWebUI MCPO API Key: $OPENWEBUI_MCPO_API_KEY\nOpenWebUI Pipelines API Key: $OPENWEBUI_PIPELINES_API_KEY" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" } function performV2IntegrationsOpenWebUI() { return } function buildImageOpenWebUIAppV1() { set +e echo -e "\n========================================================================" echo -e " The OpenWebUI image is being built. It can take a very long time" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/open-webui cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 --branch v0.8.12 https://github.com/open-webui/open-webui.git addImageButtonOpenWebUIApp docker image build -t hshq/openwebui-unstupid:v1 . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/open-webui return $rtval } function buildImageOpenWebUIAppV2() { set +e echo -e "\n========================================================================" echo -e " The OpenWebUI image is being built. It can take a very long time" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/open-webui cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 --branch v0.9.5 https://github.com/open-webui/open-webui.git addImageButtonOpenWebUIApp docker image build -t hshq/openwebui-app:v2 . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/open-webui return $rtval } function buildImageOpenWebUIAppV3() { set +e echo -e "\n========================================================================" echo -e " The OpenWebUI image is being built. It can take a very long time" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/open-webui cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 --branch v0.10.2 https://github.com/open-webui/open-webui.git addImageButtonOpenWebUIApp addPerUserMCPVaultOpenWebUIApp docker image build -t hshq/openwebui-app:v3 . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/open-webui return $rtval } function addImageButtonOpenWebUIApp() { cd $HSHQ_BUILD_DIR/open-webui # OpenWebUI is one of the most phenomenal projects on the planet. # It plugs into everything AI, tons of features/integrations, its absolutely amazing. # That being said, this is probably the most insane feature removal ever. (https://github.com/open-webui/open-webui/commit/f0829ba6e6fd200702fb76efc43dd785cf87fec9#diff-da06c64d08f7e5601cda6df17831c7fe957533b532077a02e485e6c53350e26d) # The tool "replacement" is not even close... sed -i "/let loadingSpeech = false;/a\ let generatingImage = false;" src/lib/components/chat/Messages/ResponseMessage.svelte addBlock=$(cat << EOFAB const generateImage = async (message: MessageType) => { generatingImage = true; const prompt = getOutputText(message?.output) || removeAllDetails(message.content ?? '') || message.content; const res = await imageGenerations(localStorage.token, prompt).catch((error) => { toast.error(\`\${error}\`); }); console.log(res); if (res) { const files = res.map((image) => ({ type: 'image', url: \`\${image.url}\` })); saveMessage(message.id, { ...message, files: files }); } generatingImage = false; }; EOFAB ) addBlock=$(echo "$addBlock" | sed ':a;N;$!ba;s/\n/\\n/g') sed -i "/let feedbackLoading = false;/i\ $addBlock" src/lib/components/chat/Messages/ResponseMessage.svelte addBlock=$(cat << EOFAB {#if \$config?.features.enable_image_generation && (\$user?.role === 'admin' || \$user?.permissions?.features?.image_generation) && !readOnly} {/if} EOFAB ) addBlock=$(echo "$addBlock" | sed ':a;N;$!ba;s/\n/\\n/g') sed -i "/{#if message.usage}/i\ $addBlock" src/lib/components/chat/Messages/ResponseMessage.svelte } function addPerUserMCPVaultOpenWebUIApp() { sed -i '/^import aiohttp$/a import httpx' backend/open_webui/utils/tools.py cat < /tmp/mcpkeyvault_fn.py async def _resolve_mcpkeyvault_placeholders( headers: dict, request ) -> dict: """Replace __USER_{SERVICE}_API_KEY__ patterns with per-user valve values. Fetches the calling user's MCPKeyVault valves via the OWUI API using the user's session JWT. Skips resolution if no placeholders are found or if the user has no session token. """ import os pattern = re.compile(r"__USER_(\w+)_API_KEY__") if not any(pattern.search(str(v)) for v in headers.values()): return headers token = getattr(request.state, "token", None) if not token: return headers owui_url = os.environ.get( "OPENWEBUI_URL", str(request.base_url).rstrip("/") ) url = owui_url + "/api/v1/tools/id/mcpkeyvault/valves/user" async with httpx.AsyncClient() as client: try: resp = await client.get( url, headers={"Authorization": f"Bearer {token.credentials}"}, timeout=5.0, ) if resp.status_code != 200: return headers valves = resp.json() except Exception: return headers result = {} for key, value in headers.items(): if isinstance(value, str): for match in pattern.finditer(value): service_name = match.group(1).lower() resolved = valves.get(f"{service_name}_api_key", "") value = value.replace(match.group(0), resolved) result[key] = value return result PYEOF sed -i '/^ return headers, cookies$/r /tmp/mcpkeyvault_fn.py' backend/open_webui/utils/tools.py rm /tmp/mcpkeyvault_fn.py sed -i '/^ headers.update(get_custom_headers(connection_headers, user, metadata))$/a\ \ # Resolve __USER_{SERVICE}_API_KEY__ placeholders via MCPKeyVault\ headers = await _resolve_mcpkeyvault_placeholders(headers, request)' backend/open_webui/utils/tools.py } function buildImageOpenWebUIMCPV1() { set +e echo -e "\n========================================================================" echo -e " The OpenWebUI MCP image is being built. It can take a bit" echo -e " for the process to complete, so please be patient." echo -e "========================================================================\n" sudo rm -fr $HSHQ_BUILD_DIR/open-webui-mcp-server cd $HSHQ_BUILD_DIR git -c advice.detachedHead=false clone --depth 1 https://github.com/troylar/open-webui-mcp-server.git cd $HSHQ_BUILD_DIR/open-webui-mcp-server docker image build -t hshq/openwebui-mcp:v1 . rtval=$? cd sudo rm -fr $HSHQ_BUILD_DIR/open-webui-mcp-server return $rtval } function mfUpdateOpenWebUIV7() { set +e grep -q "QDRANT__TELEMETRY_DISABLED" $HOME/openwebui.env if [ $? -ne 0 ]; then echo "QDRANT__TELEMETRY_DISABLED=true" >> $HOME/openwebui.env fi cat < $HOME/openwebui-compose.yml $STACK_VERSION_PREFIX openwebui v7 services: openwebui-db: image: mirror.gcr.io/postgres:16.9-bookworm container_name: openwebui-db hostname: openwebui-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-openwebui-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.openwebui-hourly-db.schedule=@every 1h" - "ofelia.job-exec.openwebui-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.openwebui-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openwebui-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openwebui-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openwebui-hourly-db.email-from=OpenWebUI Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openwebui-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.openwebui-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.openwebui-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.openwebui-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.openwebui-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.openwebui-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.openwebui-monthly-db.email-from=OpenWebUI Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.openwebui-monthly-db.mail-only-on-error=false" openwebui-qdrant: image: mirror.gcr.io/qdrant/qdrant:v1.20.0-unprivileged container_name: openwebui-qdrant hostname: openwebui-qdrant restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openwebui-net - dock-dbs-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/qdrant:/qdrant/storage openwebui-app: image: ghcr.io/homeserverhq/open-webui:v0.13.0 container_name: openwebui-app hostname: openwebui-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - openwebui-db networks: - int-openwebui-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/data:/app/backend/data openwebui-openterminal: image: ghcr.io/open-webui/open-terminal:0.17.4 container_name: openwebui-openterminal hostname: openwebui-openterminal restart: unless-stopped env_file: stack.env networks: - int-openwebui-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-openwebui-openterminal:/home openwebui-mcpo: image: ghcr.io/open-webui/mcpo:main container_name: openwebui-mcpo hostname: openwebui-mcpo restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: --config /app/config/config.json --api-key "$OPENWEBUI_MCPO_API_KEY" --hot-reload networks: - int-openwebui-net - dock-aipriv-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/mcpo:/app/config openwebui-mcp: image: hshq/openwebui-mcp:v1 container_name: openwebui-mcp hostname: openwebui-mcp restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-openwebui-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro openwebui-redis: image: mirror.gcr.io/valkey/valkey:alpine3.23 container_name: openwebui-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $OPENWEBUI_REDIS_PASSWORD --appendonly no networks: - int-openwebui-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-openwebui-openterminal: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/openterminal v-openwebui-pipelines: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/openwebui/pipelines networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-openwebui-net: driver: bridge internal: true ipam: driver: default EOFMT } # Khoj function installKhoj() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory khoj "Khoj" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName khoj-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName khoj-sandbox) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName khoj-computer) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName khoj-server) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/khoj mkdir $HSHQ_STACKS_DIR/khoj/config mkdir $HSHQ_STACKS_DIR/khoj/computer mkdir $HSHQ_STACKS_DIR/khoj/db mkdir $HSHQ_STACKS_DIR/khoj/dbexport chmod 777 $HSHQ_STACKS_DIR/khoj/dbexport sudo chown 1001:1001 $HSHQ_STACKS_DIR/khoj/computer mkdir -p $HSHQ_AIMODELS_DIR/khoj initServicesCredentials set +e addUserMailu alias $KHOJ_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigKhoj installStack khoj khoj-server "Uvicorn running on" $HOME/khoj.env 5 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$KHOJ_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_KHOJ_SERVER Admin Login Info" -b "$FMLNAME_KHOJ_SERVER Admin Email: $KHOJ_ADMIN_EMAIL_ADDRESS\n$FMLNAME_KHOJ_SERVER Admin Password: $KHOJ_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" KHOJ_INIT_ENV=true updateConfigVar KHOJ_INIT_ENV $KHOJ_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase Khoj postgres khoj-db $KHOJ_DATABASE_NAME $KHOJ_DATABASE_USER $KHOJ_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/khoj/dbexport $KHOJ_DATABASE_READONLYUSER $KHOJ_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_KHOJ_SERVER" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://khoj-server:42110 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_KHOJ_SERVER $MANAGETLS_KHOJ_SERVER "$is_integrate_hshq" $NETDEFAULT_KHOJ_SERVER "$inner_block" insertSubAuthelia $SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll khoj "$FMLNAME_KHOJ_SERVER" $USERTYPE_KHOJ_SERVER "https://$SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN" "khoj.png" "$(getHeimdallOrderFromSub $SUB_KHOJ_SERVER $USERTYPE_KHOJ_SERVER)" restartAllCaddyContainers checkAddDBConnection true khoj "$FMLNAME_KHOJ_SERVER" postgres khoj-db $KHOJ_DATABASE_NAME $KHOJ_DATABASE_USER $KHOJ_DATABASE_USER_PASSWORD fi } function outputConfigKhoj() { cat < $HOME/khoj-compose.yml $STACK_VERSION_PREFIX khoj $(getScriptStackVersion khoj) services: khoj-db: image: $(getScriptImageByContainerName khoj-db) container_name: khoj-db hostname: khoj-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-khoj-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/khoj/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/khoj/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.khoj-hourly-db.schedule=@every 1h" - "ofelia.job-exec.khoj-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.khoj-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.khoj-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.khoj-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.khoj-hourly-db.email-from=Khoj Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.khoj-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.khoj-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.khoj-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.khoj-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.khoj-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.khoj-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.khoj-monthly-db.email-from=Khoj Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.khoj-monthly-db.mail-only-on-error=false" khoj-sandbox: image: $(getScriptImageByContainerName khoj-sandbox) container_name: khoj-sandbox hostname: khoj-sandbox restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - khoj-db networks: - int-khoj-net - dock-aipriv-net - dock-ext-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro khoj-computer: image: $(getScriptImageByContainerName khoj-computer) container_name: khoj-computer hostname: khoj-computer user: "1001:1001" restart: unless-stopped env_file: stack.env privileged: true depends_on: - khoj-db ports: - $KHOJ_VNC_PORT:5900 networks: - int-khoj-net - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-khoj-computer:/home/operator khoj-server: image: $(getScriptImageByContainerName khoj-server) container_name: khoj-server hostname: khoj-server restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true working_dir: /app command: --host="0.0.0.0" --port=42110 -vv --anonymous-mode --non-interactive depends_on: - khoj-db networks: - int-khoj-net - dock-ext-net - dock-aipriv-net volumes: # Not sure about this. User can enable if they want. #- /var/run/docker.sock:/var/run/docker.sock - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/khoj:/root/.cache - v-khoj-config:/root/.khoj volumes: v-khoj-config: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/khoj/config v-khoj-computer: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/khoj/computer networks: dock-proxy-net: name: dock-proxy external: true dock-aipriv-net: name: dock-aipriv external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-khoj-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/khoj.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$KHOJ_DATABASE_NAME POSTGRES_USER=$KHOJ_DATABASE_USER POSTGRES_PASSWORD=$KHOJ_DATABASE_USER_PASSWORD POSTGRES_HOST=khoj-db POSTGRES_PORT=5432 KHOJ_DJANGO_SECRET_KEY=$KHOJ_DJANGO_SECRET_KEY SEARXNG_BASE_URL=http://searxng-app:8080/ KHOJ_DEBUG=False KHOJ_ADMIN_EMAIL=$KHOJ_ADMIN_EMAIL_ADDRESS KHOJ_ADMIN_PASSWORD=$KHOJ_ADMIN_PASSWORD KHOJ_TERRARIUM_URL=http://khoj-sandbox:8080 OPENAI_BASE_URL=http://litellm-proxy:4000/v1 OPENAI_API_KEY=$LITELLM_AUTH_KEY KHOJ_DEFAULT_CHAT_MODEL=Chat KHOJ_OPERATOR_ENABLED=True KHOJ_NO_HTTPS=True KHOJ_DOMAIN=$SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN KHOJ_ALLOWED_DOMAIN=$SUB_KHOJ_SERVER.$HOMESERVER_DOMAIN KHOJ_TELEMETRY_DISABLE=True EOFMT } function performUpdateKhoj() { perform_stack_name=khoj prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=mirror.gcr.io/pgvector/pgvector:0.8.1-pg16-bookworm,ghcr.io/khoj-ai/terrarium:latest,ghcr.io/khoj-ai/khoj-computer:1.42.0,ghcr.io/khoj-ai/khoj:1.42.10 image_update_map[0]="mirror.gcr.io/pgvector/pgvector:0.8.1-pg16-bookworm,mirror.gcr.io/pgvector/pgvector:0.8.1-pg16-bookworm" image_update_map[1]="ghcr.io/khoj-ai/terrarium:latest,ghcr.io/khoj-ai/terrarium:latest" image_update_map[2]="ghcr.io/khoj-ai/khoj-computer:1.42.0,ghcr.io/khoj-ai/khoj-computer:1.42.0" image_update_map[3]="ghcr.io/khoj-ai/khoj:1.42.10,ghcr.io/khoj-ai/khoj:1.42.10" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # LobeChat function installLobeChat() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory lobechat "LobeChat" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName lobechat-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName lobechat-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName lobechat-redis) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName lobechat-minio) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/lobechat mkdir $HSHQ_STACKS_DIR/lobechat/minio mkdir $HSHQ_STACKS_DIR/lobechat/db mkdir $HSHQ_STACKS_DIR/lobechat/dbexport chmod 777 $HSHQ_STACKS_DIR/lobechat/dbexport initServicesCredentials set +e addUserMailu alias $LOBECHAT_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS LOBECHAT_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $LOBECHAT_ADMIN_PASSWORD | tr -d ':\n') outputConfigLobeChat oidcBlock=$(cat $HOME/lobechat.oidc) rm -f $HOME/lobechat.oidc insertOIDCClientAuthelia lobechat "$oidcBlock" installStack lobechat lobechat-app "Ready in" $HOME/lobechat.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$LOBECHAT_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_LOBECHAT_APP Admin Login Info" -b "$FMLNAME_LOBECHAT_APP Admin Username: $LOBECHAT_ADMIN_USERNAME\n$FMLNAME_LOBECHAT_APP Admin Email: $LOBECHAT_ADMIN_EMAIL_ADDRESS\n$FMLNAME_LOBECHAT_APP Admin Password: $LOBECHAT_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" LOBECHAT_INIT_ENV=true updateConfigVar LOBECHAT_INIT_ENV $LOBECHAT_INIT_ENV fi sleep 3 addReadOnlyUserToDatabase LobeChat postgres lobechat-db $LOBECHAT_DATABASE_NAME $LOBECHAT_DATABASE_USER $LOBECHAT_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/lobechat/dbexport $LOBECHAT_DATABASE_READONLYUSER $LOBECHAT_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_LOBECHAT_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://lobechat-app:3210 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_LOBECHAT_APP $MANAGETLS_LOBECHAT_APP "$is_integrate_hshq" $NETDEFAULT_LOBECHAT_APP "$inner_block" insertSubAuthelia $SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll lobechat "$FMLNAME_LOBECHAT_APP" $USERTYPE_LOBECHAT_APP "https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN" "lobechat.png" "$(getHeimdallOrderFromSub $SUB_LOBECHAT_APP $USERTYPE_LOBECHAT_APP)" restartAllCaddyContainers checkAddDBConnection true lobechat "$FMLNAME_LOBECHAT_APP" postgres lobechat-db $LOBECHAT_DATABASE_NAME $LOBECHAT_DATABASE_USER $LOBECHAT_DATABASE_USER_PASSWORD fi } function outputConfigLobeChat() { cat < $HOME/lobechat-compose.yml $STACK_VERSION_PREFIX lobechat $(getScriptStackVersion lobechat) services: lobechat-db: image: $(getScriptImageByContainerName lobechat-db) container_name: lobechat-db hostname: lobechat-db user: "\${PORTAINER_UID}:\${PORTAINER_GID}" restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true shm_size: 256mb networks: - int-lobechat-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/lobechat/db:/var/lib/postgresql/data - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/lobechat/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.lobechat-hourly-db.schedule=@every 1h" - "ofelia.job-exec.lobechat-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.lobechat-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.lobechat-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.lobechat-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.lobechat-hourly-db.email-from=LobeChat Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.lobechat-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.lobechat-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.lobechat-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.lobechat-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.lobechat-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.lobechat-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.lobechat-monthly-db.email-from=LobeChat Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.lobechat-monthly-db.mail-only-on-error=false" lobechat-app: image: $(getScriptImageByContainerName lobechat-app) container_name: lobechat-app hostname: lobechat-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - lobechat-db - lobechat-minio - lobechat-redis networks: - int-lobechat-net - dock-ext-net - dock-aipriv-net entrypoint: > /bin/sh -c " /bin/node /app/startServer.js & LOBE_PID=\\\$! sleep 3 wait \\\$LOBE_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro lobechat-redis: image: $(getScriptImageByContainerName lobechat-redis) container_name: lobechat-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $LOBECHAT_REDIS_PASSWORD --appendonly no networks: - int-lobechat-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro lobechat-minio: image: $(getScriptImageByContainerName lobechat-minio) container_name: lobechat-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - int-lobechat-net entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\\\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 mc alias set minio http://localhost:9000 ${LOBECHAT_MINIO_KEY} ${LOBECHAT_MINIO_SECRET} echo 'Creating bucket lobechat' mc mb minio/lobechat wait \\\$MINIO_PID " volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/lobechat/minio:/data:/data networks: dock-proxy-net: name: dock-proxy external: true dock-aipriv-net: name: dock-aipriv external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true int-lobechat-net: driver: bridge internal: true ipam: driver: default EOFMT cat < $HOME/lobechat.env TZ=\${PORTAINER_TZ} POSTGRES_DB=$LOBECHAT_DATABASE_NAME POSTGRES_USER=$LOBECHAT_DATABASE_USER POSTGRES_PASSWORD=$LOBECHAT_DATABASE_USER_PASSWORD APP_URL=https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN AUTH_URL=https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN/api/auth LOBE_DB_NAME=$LOBECHAT_DATABASE_NAME MINIO_ROOT_USER=$LOBECHAT_MINIO_KEY MINIO_ROOT_PASSWORD=$LOBECHAT_MINIO_SECRET S3_PUBLIC_DOMAIN=http://lobechat-minio:9000 S3_ENDPOINT=http://lobechat-minio:9000 MINIO_LOBE_BUCKET=lobechat OPENAI_API_KEY=$LITELLM_AUTH_KEY OPENAI_PROXY_URL=http://litellm-proxy:4000/v1 KEY_VAULTS_SECRET=$LOBECHAT_KEYVAULTS_SECRET NEXT_AUTH_SECRET=$LOBECHAT_NEXTAUTH_SECRET NEXT_AUTH_SSO_PROVIDERS=authelia AUTH_AUTHELIA_ID=$LOBECHAT_OIDC_CLIENT_ID AUTH_AUTHELIA_SECRET=$LOBECHAT_OIDC_CLIENT_SECRET AUTH_AUTHELIA_ISSUER=https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN NEXTAUTH_URL=https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN/api/auth NODE_ENV=production NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt DATABASE_URL=postgresql://$LOBECHAT_DATABASE_USER:$LOBECHAT_DATABASE_USER_PASSWORD@lobechat-db:5432/$LOBECHAT_DATABASE_NAME S3_BUCKET=lobechat S3_ENABLE_PATH_STYLE=1 S3_ACCESS_KEY=$LOBECHAT_MINIO_KEY S3_ACCESS_KEY_ID=$LOBECHAT_MINIO_KEY S3_SECRET_ACCESS_KEY=$LOBECHAT_MINIO_SECRET LLM_VISION_IMAGE_USE_BASE64=1 S3_SET_ACL=0 SEARXNG_URL=http://searxng-app:8080 REDIS_URL=redis://:$LOBECHAT_REDIS_PASSWORD@lobechat-redis:6379/0 REDIS_PREFIX=lobechat REDIS_TLS=0 EOFMT LOBECHAT_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $LOBECHAT_OIDC_CLIENT_SECRET | tr -d ':\n') cat < $HOME/lobechat.oidc # Authelia OIDC Client lobechat BEGIN - client_id: $LOBECHAT_OIDC_CLIENT_ID client_name: LobeChat client_secret: '$LOBECHAT_OIDC_CLIENT_SECRET_HASH' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth redirect_uris: - https://$SUB_LOBECHAT_APP.$HOMESERVER_DOMAIN/api/auth/callback/authelia scopes: - openid - profile - email - groups userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_basic # Authelia OIDC Client lobechat END EOFIM } function performUpdateLobeChat() { perform_stack_name=lobechat prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/pgvector/pgvector:pg17,mirror.gcr.io/lobehub/lobe-chat-database:1.143.3,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image_update_map[0]="mirror.gcr.io/pgvector/pgvector:pg17,mirror.gcr.io/pgvector/pgvector:pg17" image_update_map[1]="mirror.gcr.io/lobehub/lobe-chat-database:1.143.3,mirror.gcr.io/lobehub/lobe-chat-database:1.143.3" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" ;; 2) newVer=v2 curImageList=mirror.gcr.io/pgvector/pgvector:pg17,mirror.gcr.io/lobehub/lobe-chat-database:1.143.3,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/minio:v1.0 image_update_map[0]="mirror.gcr.io/pgvector/pgvector:pg17,mirror.gcr.io/pgvector/pgvector:pg17" image_update_map[1]="mirror.gcr.io/lobehub/lobe-chat-database:1.143.3,mirror.gcr.io/lobehub/lobe-chat-database:1.143.3" image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # InvokeAI function installInvokeAI() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory invokeai "InvokeAI" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName invokeai-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/invokeai mkdir $HSHQ_STACKS_DIR/invokeai/data mkdir -p $HSHQ_AIMODELS_DIR/invokeai/models mkdir -p $HSHQ_AIMODELS_DIR/invokeai/cache/{download,convert} initServicesCredentials set +e outputConfigInvokeAI installStack invokeai invokeai-app "Invoke running on" $HOME/invokeai.env 3 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi sleep 3 if [ -z "$FMLNAME_INVOKEAI_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_INVOKEAI_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://invokeai-app:9090 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_INVOKEAI_APP $MANAGETLS_INVOKEAI_APP "$is_integrate_hshq" $NETDEFAULT_INVOKEAI_APP "$inner_block" insertSubAuthelia $SUB_INVOKEAI_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll invokeai "$FMLNAME_INVOKEAI_APP" $USERTYPE_INVOKEAI_APP "https://$SUB_INVOKEAI_APP.$HOMESERVER_DOMAIN" "invokeai.png" "$(getHeimdallOrderFromSub $SUB_INVOKEAI_APP $USERTYPE_INVOKEAI_APP)" restartAllCaddyContainers fi } function outputConfigInvokeAI() { cat < $HOME/invokeai-compose.yml $STACK_VERSION_PREFIX invokeai $(getScriptStackVersion invokeai) services: invokeai-app: image: $(getScriptImageByContainerName invokeai-app) container_name: invokeai-app hostname: invokeai-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true networks: - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-invokeai-data:/invokeai - v-invokeai-models:/models - v-invokeai-cache:/iaicache volumes: v-invokeai-data: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/invokeai/data v-invokeai-models: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/invokeai v-invokeai-cache: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/aimodels/invokeai/cache networks: dock-proxy-net: name: dock-proxy external: true dock-aipriv-net: name: dock-aipriv external: true dock-ext-net: name: dock-ext external: true EOFMT cat < $HOME/invokeai.env TZ=\${PORTAINER_TZ} EOFMT } function performUpdateInvokeAI() { perform_stack_name=invokeai prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v1 curImageList=ghcr.io/invoke-ai/invokeai:6.12-cpu image_update_map[0]="ghcr.io/invoke-ai/invokeai:6.12-cpu,ghcr.io/invoke-ai/invokeai:6.12-cpu" ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } # RAGFlow function installRAGFlow() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory ragflow "RAGFlow" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ragflow-db) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ragflow-infinity) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ragflow-app) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ragflow-rustfs) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ragflow-kvrocks) if [ $? -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName ragflow-nats) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/ragflow mkdir $HSHQ_STACKS_DIR/ragflow/config mkdir $HSHQ_STACKS_DIR/ragflow/logs mkdir $HSHQ_STACKS_DIR/ragflow/nginx mkdir $HSHQ_STACKS_DIR/ragflow/infinity mkdir $HSHQ_STACKS_DIR/ragflow/rustfs mkdir $HSHQ_STACKS_DIR/ragflow/db mkdir $HSHQ_STACKS_DIR/ragflow/dbexport mkdir $HSHQ_STACKS_DIR/ragflow/misc mkdir $HSHQ_STACKS_DIR/ragflow/nats mkdir -p $HSHQ_NONBACKUP_DIR/ragflow/kvrocks sudo chown 10001:10001 $HSHQ_STACKS_DIR/ragflow/rustfs chmod 777 $HSHQ_STACKS_DIR/ragflow/dbexport initServicesCredentials set +e addUserMailu alias $RAGFLOW_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigRAGFlow oidcBlock=$(cat $HOME/ragflow.oidc) rm -f $HOME/ragflow.oidc insertOIDCClientAuthelia ragflow "$oidcBlock" installStack ragflow ragflow-infinity "Infinity is started in standalone mode" $HOME/ragflow.env 10 retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$RAGFLOW_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_RAGFLOW_APP Admin Login Info" -b "$FMLNAME_RAGFLOW_APP Admin Username: $RAGFLOW_ADMIN_EMAIL_ADDRESS\n$FMLNAME_RAGFLOW_APP Admin Password: $RAGFLOW_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" RAGFLOW_INIT_ENV=true updateConfigVar RAGFLOW_INIT_ENV $RAGFLOW_INIT_ENV fi sleep 3 startStopStack ragflow stop set +e ragflowImage=$(getScriptImageByContainerName ragflow-app) cid=$(docker create "$ragflowImage" 2>/dev/null) docker cp "$cid":/ragflow/conf/. $HSHQ_STACKS_DIR/ragflow/config/ 2>/dev/null docker rm -f "$cid" >/dev/null 2>&1 sudo mv -f $HSHQ_STACKS_DIR/ragflow/service_conf.yaml.template $HSHQ_STACKS_DIR/ragflow/config/service_conf.yaml.template startStopStack ragflow start local curR=0 local maxR=300 while [ $curR -lt $maxR ]; do if docker exec ragflow-app curl -fsS http://localhost:9380/api/v1/system/healthz >/dev/null 2>&1 && docker exec ragflow-app curl -fsS http://localhost:9381/api/v1/admin/ping >/dev/null 2>&1; then echo "RAGFlow is ready." break fi sleep 5 curR=$((curR+5)) done if [ $curR -ge $maxR ]; then echo "ERROR: ragflow-app did not become ready within ${maxR} seconds. Try: docker logs ragflow-app" return 1 fi sleep 3 set +e performIntegrationsRAGflow addReadOnlyUserToDatabase RAGFlow mysql ragflow-db $RAGFLOW_DATABASE_NAME root $RAGFLOW_DATABASE_ROOT_PASSWORD $HSHQ_STACKS_DIR/ragflow/dbexport $RAGFLOW_DATABASE_READONLYUSER $RAGFLOW_DATABASE_READONLYUSER_PASSWORD if [ -z "$FMLNAME_RAGFLOW_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERALLOWFRAME\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RELAXEDCSP\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://ragflow-app {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_RAGFLOW_APP $MANAGETLS_RAGFLOW_APP "$is_integrate_hshq" $NETDEFAULT_RAGFLOW_APP "$inner_block" insertSubAuthelia $SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME} inner_block="" inner_block=$inner_block">>https://$SUB_RAGFLOW_API.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://ragflow-app:9380 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_RAGFLOW_API $MANAGETLS_RAGFLOW_API "$is_integrate_hshq" $NETDEFAULT_RAGFLOW_API "$inner_block" insertSubAuthelia $SUB_RAGFLOW_API.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_RAGFLOW_MCP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://ragflow-app:9382 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_RAGFLOW_MCP $MANAGETLS_RAGFLOW_MCP "$is_integrate_hshq" $NETDEFAULT_RAGFLOW_MCP "$inner_block" insertSubAuthelia $SUB_RAGFLOW_MCP.$HOMESERVER_DOMAIN bypass inner_block="" inner_block=$inner_block">>https://$SUB_RAGFLOW_RUSTFS.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://ragflow-rustfs:9001 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_RAGFLOW_RUSTFS $MANAGETLS_RAGFLOW_RUSTFS "$is_integrate_hshq" $NETDEFAULT_RAGFLOW_RUSTFS "$inner_block" insertSubAuthelia $SUB_RAGFLOW_RUSTFS.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll ragflow "$FMLNAME_RAGFLOW_APP" $USERTYPE_RAGFLOW_APP "https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN" "ragflow.png" "$(getHeimdallOrderFromSub $SUB_RAGFLOW_APP $USERTYPE_RAGFLOW_APP)" insertEnableSvcAll ragflow "$FMLNAME_RAGFLOW_RUSTFS" $USERTYPE_RAGFLOW_RUSTFS "https://$SUB_RAGFLOW_RUSTFS.$HOMESERVER_DOMAIN" "rustfs.png" "$(getHeimdallOrderFromSub $SUB_RAGFLOW_RUSTFS $USERTYPE_RAGFLOW_RUSTFS)" restartAllCaddyContainers checkAddDBConnection true ragflow "$FMLNAME_RAGFLOW_APP" mysql ragflow-db $RAGFLOW_DATABASE_NAME $RAGFLOW_DATABASE_USER $RAGFLOW_DATABASE_USER_PASSWORD fi } function outputConfigRAGFlow() { set +e cat < $HOME/ragflow.env TZ=\${PORTAINER_TZ} TIMEZONE='UTC-6\tAmerica/Chicago' DB_TYPE=mysql DOC_ENGINE=infinity DEVICE=cpu COMPOSE_PROFILES=infinity,cpu MEM_LIMIT=8589934592 MYSQL_HOST=ragflow-db MYSQL_PORT=3306 MYSQL_USER=$RAGFLOW_DATABASE_USER MYSQL_PASSWORD=$RAGFLOW_DATABASE_USER_PASSWORD MYSQL_DATABASE=$RAGFLOW_DATABASE_NAME MYSQL_DBNAME=$RAGFLOW_DATABASE_NAME MARIADB_ROOT_PASSWORD=$RAGFLOW_DATABASE_ROOT_PASSWORD MARIADB_DATABASE=$RAGFLOW_DATABASE_NAME MARIADB_USER=$RAGFLOW_DATABASE_USER MARIADB_PASSWORD=$RAGFLOW_DATABASE_USER_PASSWORD MARIADB_ROOT_HOST=% RAGFLOW_DATABASE_NAME=$RAGFLOW_DATABASE_NAME RAGFLOW_DATABASE_HOST=ragflow-db RAGFLOW_DATABASE_USER=$RAGFLOW_DATABASE_USER RAGFLOW_DATABASE_USER_PASSWORD=$RAGFLOW_DATABASE_USER_PASSWORD RAGFLOW_DATABASE_ROOT_PASSWORD=$RAGFLOW_DATABASE_ROOT_PASSWORD INFINITY_HOST=ragflow-infinity INFINITY_THRIFT_PORT=23817 INFINITY_HTTP_PORT=23820 INFINITY_PSQL_PORT=5432 MINIO_HOST=ragflow-rustfs MINIO_CONSOLE_PORT=9001 MINIO_PORT=9000 MINIO_USER=$RAGFLOW_MINIO_KEY MINIO_PASSWORD=$RAGFLOW_MINIO_SECRET MINIO_ROOT_USER=$RAGFLOW_MINIO_KEY MINIO_ROOT_PASSWORD=$RAGFLOW_MINIO_SECRET RUSTFS_ACCESS_KEY=$RAGFLOW_MINIO_KEY RUSTFS_SECRET_KEY=$RAGFLOW_MINIO_SECRET RUSTFS_AUTO_CREATE_BUCKETS=ragflow KVROCKS_HOST=ragflow-kvrocks KVROCKS_PORT=6379 REDIS_PASSWORD=$RAGFLOW_REDIS_PASSWORD NATS_HOST=ragflow-nats NATS_PORT=4222 SVR_WEB_HTTP_PORT=80 SVR_WEB_HTTPS_PORT=443 SVR_HTTP_PORT=9380 ADMIN_SVR_HTTP_PORT=9381 SVR_MCP_PORT=9382 MAX_CONTENT_LENGTH=1073741824 DOC_BULK_SIZE=4 EMBEDDING_BATCH_SIZE=8 REGISTER_ENABLED=1 USE_DOCLING=true DOCLING_SERVER_URL=http://docling-app:5001 DOCLING_API_KEY=$DOCLING_API_KEY DOCLING_PICTURE_DESCRIPTION=1 DOCLING_PICTURE_DESCRIPTION_PRESET=external_vlm DOCLING_PICTURE_DESCRIPTION_AREA_THRESHOLD=0.025 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 PYTHON_VER=python3.13 RAGFLOW_SECRET_KEY=$RAGFLOW_SECRET_KEY SECRET_KEY=$RAGFLOW_SECRET_KEY DEFAULT_SUPERUSER_NICKNAME="RAGFlow $(getAdminEmailName)" DEFAULT_SUPERUSER_EMAIL=$RAGFLOW_ADMIN_EMAIL_ADDRESS DEFAULT_SUPERUSER_PASSWORD=$RAGFLOW_ADMIN_PASSWORD ADMIN_DEFAULT_PASSWORD=$RAGFLOW_ADMIN_PASSWORD RAGFLOW_MCP_ENABLED=true RAGFLOW_MCP_HOST=0.0.0.0 RAGFLOW_MCP_PORT=9382 RAGFLOW_MCP_LAUNCH_MODE=host RAGFLOW_MCP_TRANSPORT_STREAMABLE_ENABLED=true RAGFLOW_MCP_TRANSPORT_SSE_ENABLED=true RAGFLOW_MCP_JSON_RESPONSE=true ALLOW_ANY_HOST=true EOFMT cat < $HSHQ_STACKS_DIR/ragflow/nginx/nginx.conf user root; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' '\$status \$body_bytes_sent "\$http_referer" ' '"\$http_user_agent" "\$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; client_max_body_size 1024M; include /etc/nginx/conf.d/ragflow.conf; } EOFMT cat < $HSHQ_STACKS_DIR/ragflow/nginx/proxy.conf proxy_set_header Host \$host; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffer_size 1024k; proxy_buffers 16 1024k; proxy_busy_buffers_size 2048k; proxy_temp_file_write_size 2048k; EOFMT cat < $HSHQ_STACKS_DIR/ragflow/nginx/ragflow.conf server { listen 80; server_name _; root /ragflow/web/dist; gzip on; gzip_min_length 1k; gzip_comp_level 9; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; gzip_vary on; gzip_disable "MSIE [1-6]\."; location ~ ^/api/v1/admin { proxy_pass http://localhost:9381; include proxy.conf; } location ~ ^/(v1|api) { proxy_pass http://localhost:9380; include proxy.conf; } location / { index index.html; try_files \$uri \$uri/ /index.html; } location ~ ^/static/(css|js|media)/ { expires 10y; access_log off; } } EOFMT cat < $HSHQ_STACKS_DIR/ragflow/misc/infinity_conf.toml [general] version = "0.7.0" time_zone = "utc-8" [network] server_address = "0.0.0.0" postgres_port = 5432 http_port = 23820 client_port = 23817 connection_pool_size = 128 [log] log_filename = "infinity.log" log_dir = "/var/infinity/log" log_to_stdout = true log_file_max_size = "100MB" log_file_rotate_count = 10 log_level = "info" [storage] persistence_dir = "/var/infinity/persistence" data_dir = "/var/infinity/data" optimize_interval = "10s" cleanup_interval = "60s" compact_interval = "120s" storage_type = "local" mem_index_capacity = 65536 [buffer] buffer_manager_size = "8GB" lru_num = 7 temp_dir = "/var/infinity/tmp" result_cache = "off" memindex_memory_quota = "1GB" [wal] wal_dir = "/var/infinity/wal" [resource] EOFMT cat < $HSHQ_STACKS_DIR/ragflow/service_conf.yaml.template ragflow: host: 0.0.0.0 http_port: 9380 secret_key: '$RAGFLOW_SECRET_KEY' admin: host: 0.0.0.0 http_port: 9381 minio: user: '$RAGFLOW_MINIO_KEY' password: '$RAGFLOW_MINIO_SECRET' host: 'ragflow-rustfs:9000' bucket: '' prefix_path: '' infinity: uri: 'ragflow-infinity:23817' db_name: 'default_db' kvrocks: db: 1 username: '' password: '$RAGFLOW_REDIS_PASSWORD' host: 'ragflow-kvrocks:6379' nats: host: 'ragflow-nats' port: 4222 ingestor: mq_type: 'nats' mysql: name: '$RAGFLOW_DATABASE_NAME' user: '$RAGFLOW_DATABASE_USER' password: '$RAGFLOW_DATABASE_USER_PASSWORD' host: 'ragflow-db' port: 3306 max_connections: 100 stale_timeout: 30 oauth: oidc: display_name: "Authelia" client_id: "$RAGFLOW_OIDC_CLIENT_ID" client_secret: "$RAGFLOW_OIDC_CLIENT_SECRET" issuer: "https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN" scope: "openid email profile" redirect_uri: "https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN/api/v1/auth/oauth/oidc/callback" smtp: mail_server: $SMTP_HOSTNAME mail_port: $SMTP_HOSTPORT mail_use_ssl: false mail_use_tls: false mail_username: "" mail_password: "" mail_default_sender: - "RAGFlow $(getAdminEmailName)" - "$EMAIL_ADMIN_EMAIL_ADDRESS" mail_frontend_url: "https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN" EOFMT RAGFLOW_OIDC_CLIENT_SECRET_HASH=$(htpasswd -bnBC 10 "" $RAGFLOW_OIDC_CLIENT_SECRET | tr -d ':\n') cat < $HOME/ragflow.oidc # Authelia OIDC Client ragflow BEGIN - client_id: $RAGFLOW_OIDC_CLIENT_ID client_name: RAGFlow client_secret: '$RAGFLOW_OIDC_CLIENT_SECRET_HASH' public: false authorization_policy: ${LDAP_PRIMARY_USER_GROUP_NAME}_auth redirect_uris: - https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN/api/v1/auth/oauth/oidc/callback scopes: - openid - profile - email - groups userinfo_signed_response_alg: none token_endpoint_auth_method: client_secret_post # Authelia OIDC Client ragflow END EOFMT cat < $HOME/ragflow-compose.yml $STACK_VERSION_PREFIX ragflow $(getScriptStackVersion ragflow) services: ragflow-db: image: $(getScriptImageByContainerName ragflow-db) container_name: ragflow-db hostname: ragflow-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mariadbd --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 --skip-name-resolve networks: - int-ragflow-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ragflow-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMariaDB.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.ragflow-hourly-db.schedule=@every 1h" - "ofelia.job-exec.ragflow-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.ragflow-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ragflow-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ragflow-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ragflow-hourly-db.email-from=RAGFlow Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ragflow-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.ragflow-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.ragflow-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.ragflow-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ragflow-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ragflow-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ragflow-monthly-db.email-from=RAGFlow Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ragflow-monthly-db.mail-only-on-error=false" ragflow-infinity: image: $(getScriptImageByContainerName ragflow-infinity) container_name: ragflow-infinity hostname: ragflow-infinity restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ragflow-db command: ["-f", "/infinity_conf.toml"] mem_limit: 8g ulimits: nofile: soft: 500000 hard: 500000 networks: - int-ragflow-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ragflow-infinity:/var/infinity - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/misc/infinity_conf.toml:/infinity_conf.toml ragflow-app: image: $(getScriptImageByContainerName ragflow-app) container_name: ragflow-app hostname: ragflow-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ragflow-db - ragflow-infinity - ragflow-rustfs - ragflow-kvrocks - ragflow-nats command: - --enable-adminserver - --init-superuser networks: - int-ragflow-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-ragflow-config:/ragflow/conf - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/logs:/ragflow/logs - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/nginx/ragflow.conf:/etc/nginx/conf.d/ragflow.conf - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/nginx/proxy.conf:/etc/nginx/proxy.conf - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/nginx/nginx.conf:/etc/nginx/nginx.conf - /etc/ssl/certs/ca-certificates.crt:/ragflow/.venv/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:9380/api/v1/system/healthz"] interval: 60s timeout: 10s retries: 5 start_period: 60s ragflow-rustfs: image: $(getScriptImageByContainerName ragflow-rustfs) container_name: ragflow-rustfs hostname: ragflow-rustfs restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ragflow-db networks: - int-ragflow-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/rustfs:/data ragflow-kvrocks: image: $(getScriptImageByContainerName ragflow-kvrocks) container_name: ragflow-kvrocks hostname: ragflow-kvrocks restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true user: "0:0" entrypoint: - /bin/sh - -c - | mkdir -p /var/lib/kvrocks { echo "bind 0.0.0.0" echo "port 6379" echo "dir /var/lib/kvrocks" printf 'requirepass %s\n' "\$\$REDIS_PASSWORD" } > /var/lib/kvrocks/kvrocks.conf exec kvrocks -c /var/lib/kvrocks/kvrocks.conf --dir /var/lib/kvrocks networks: - int-ragflow-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ragflow-kvrocks:/var/lib/kvrocks healthcheck: test: ["CMD-SHELL", "redis-cli -a \"\$\$REDIS_PASSWORD\" ping | grep -q PONG"] interval: 10s timeout: 10s retries: 30 ragflow-nats: image: $(getScriptImageByContainerName ragflow-nats) container_name: ragflow-nats hostname: ragflow-nats restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: ["-js", "-sd", "/data", "--http_port", "8222"] networks: - int-ragflow-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ragflow-nats:/data healthcheck: test: ["CMD", "nats-server", "--health-check"] interval: 10s timeout: 10s retries: 30 volumes: v-ragflow-config: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/config v-ragflow-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/db v-ragflow-infinity: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/infinity v-ragflow-kvrocks: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_NONBACKUP_DIR}/ragflow/kvrocks v-ragflow-nats: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/nats networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-ragflow-net: driver: bridge internal: true ipam: driver: default EOFMT } function performIntegrationsRAGflow() { return } function performUpdateRAGFlow() { perform_stack_name=ragflow prepPerformUpdate if [ $? -ne 0 ]; then return 1; fi # The current version is included as a placeholder for when the next version arrives. case "$perform_stack_ver" in 1) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/infiniflow/infinity:v0.6.15,mirror.gcr.io/infiniflow/ragflow:v0.23.1,mirror.gcr.io/infiniflow/sandbox-executor-manager:latest,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/infiniflow/infinity:v0.6.15,mirror.gcr.io/infiniflow/infinity:v0.7.0-dev2" image_update_map[2]="mirror.gcr.io/infiniflow/ragflow:v0.23.1,mirror.gcr.io/infiniflow/ragflow:v0.24.0" image_update_map[3]="mirror.gcr.io/infiniflow/sandbox-executor-manager:latest,mirror.gcr.io/infiniflow/sandbox-executor-manager:latest" image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[5]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing true mfUpdateV2RAGflow perform_update_report="${perform_update_report}$stack_upgrade_report" return ;; 2) newVer=v2 curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/infiniflow/infinity:v0.7.0-dev2,mirror.gcr.io/infiniflow/ragflow:v0.24.0,mirror.gcr.io/infiniflow/sandbox-executor-manager:latest,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm" image_update_map[1]="mirror.gcr.io/infiniflow/infinity:v0.7.0-dev2,mirror.gcr.io/infiniflow/infinity:v0.7.0-dev2" image_update_map[2]="mirror.gcr.io/infiniflow/ragflow:v0.24.0,mirror.gcr.io/infiniflow/ragflow:v0.24.0" image_update_map[3]="mirror.gcr.io/infiniflow/sandbox-executor-manager:latest,mirror.gcr.io/infiniflow/sandbox-executor-manager:latest" image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" image_update_map[5]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of RAGFlow cannot be upgraded to the next version. There are significant changes which require a full re-install. Please backup/export your data, uninstall RAGFlow and perform a fresh installation and re-import your data. Sorry for the inconvenience." return ;; 3) newVer=v4 curImageList=mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3,ghcr.io/homeserverhq/ragflow:v0.27.1,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/mariadb:11.4.12" image_update_map[1]="mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3,mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3" image_update_map[2]="ghcr.io/homeserverhq/ragflow:v0.27.1,ghcr.io/homeserverhq/ragflow:v0.27.1" image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" ;; 4) newVer=v5 curImageList=mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3,ghcr.io/homeserverhq/ragflow:v0.27.1,ghcr.io/homeserverhq/minio:v1.0,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/mariadb:11.4.12" image_update_map[1]="mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3,mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3" image_update_map[2]="ghcr.io/homeserverhq/ragflow:v0.27.1,ghcr.io/homeserverhq/ragflow:v0.27.3.1" image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" ;; 5) newVer=v6 curImageList=mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3,ghcr.io/homeserverhq/ragflow:v0.27.3.1,ghcr.io/homeserverhq/minio:v1.0,mirror.gcr.io/valkey/valkey:alpine3.23 image_update_map[0]="mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/mariadb:11.4.12" image_update_map[1]="mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3,mirror.gcr.io/infiniflow/infinity:v0.7.3-x64-v3" image_update_map[2]="ghcr.io/homeserverhq/ragflow:v0.27.3.1,ghcr.io/homeserverhq/ragflow:v0.27.3.1" image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0" image_update_map[4]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of RAGFlow cannot be upgraded to the next version. You must first backup your data, uninstall and reinstall RAGFlow and import your data back in." return ;; 6) newVer=v6 curImageList=mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/infiniflow/infinity:v0.7.5-x64-v3,mirror.gcr.io/infiniflow/ragflow:v1.0.0,mirror.gcr.io/rustfs/rustfs:1.0.4,mirror.gcr.io/apache/kvrocks:2.16.0,mirror.gcr.io/nats:2.14.2 image_update_map[0]="mirror.gcr.io/mariadb:11.4.12,mirror.gcr.io/mariadb:11.4.12" image_update_map[1]="mirror.gcr.io/infiniflow/infinity:v0.7.5-x64-v3,mirror.gcr.io/infiniflow/infinity:v0.7.5-x64-v3" image_update_map[2]="mirror.gcr.io/infiniflow/ragflow:v1.0.0,mirror.gcr.io/infiniflow/ragflow:v1.0.0" image_update_map[3]="mirror.gcr.io/rustfs/rustfs:1.0.4,mirror.gcr.io/rustfs/rustfs:1.0.4" image_update_map[4]="mirror.gcr.io/apache/kvrocks:2.16.0,mirror.gcr.io/apache/kvrocks:2.16.0" image_update_map[5]="mirror.gcr.io/nats:2.14.2,mirror.gcr.io/nats:2.14.2" is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): This version of RAGFlow cannot be upgraded to the next version. You must first backup your data, uninstall and reinstall RAGFlow and import your data back in." return ;; *) is_upgrade_error=true perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)" return ;; esac upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false perform_update_report="${perform_update_report}$stack_upgrade_report" } function mfUpdateV2RAGflow() { startStopStack ragflow stop sudo rm -f $HSHQ_STACKS_DIR/ragflow/misc/infinity_conf.toml sudo rm -f $HSHQ_STACKS_DIR/ragflow/config/doc_meta_es_mapping.json sudo rm -f $HSHQ_STACKS_DIR/ragflow/config/doc_meta_infinity_mapping.json sudo rm -f $HSHQ_STACKS_DIR/ragflow/config/infinity_mapping.json sudo rm -f $HSHQ_STACKS_DIR/ragflow/config/system_settings.json sudo rm -f $HSHQ_STACKS_DIR/ragflow/config/service_conf.yaml.template sudo rm -f $HSHQ_STACKS_DIR/ragflow/misc/entrypoint.sh cat < $HSHQ_STACKS_DIR/ragflow/misc/infinity_conf.toml [general] version = "0.7.0" time_zone = "utc-8" [network] server_address = "0.0.0.0" postgres_port = 5432 http_port = 23820 client_port = 23817 connection_pool_size = 128 [log] log_filename = "infinity.log" log_dir = "/var/infinity/log" log_to_stdout = true log_file_max_size = "100MB" log_file_rotate_count = 10 # trace/debug/info/warning/error/critical 6 log levels, default: info log_level = "info" [storage] persistence_dir = "/var/infinity/persistence" data_dir = "/var/infinity/data" # periodically activates garbage collection: # 0 means real-time, # s means seconds, for example "60s", 60 seconds # m means minutes, for example "60m", 60 minutes # h means hours, for example "1h", 1 hour optimize_interval = "10s" cleanup_interval = "60s" compact_interval = "120s" storage_type = "local" # dump memory index entry when it reachs the capacity mem_index_capacity = 65536 # S3 storage config example: # [storage.object_storage] # url = "127.0.0.1:9000" # bucket_name = "infinity" # access_key = "minioadmin" # secret_key = "minioadmin" # enable_https = false [buffer] buffer_manager_size = "8GB" lru_num = 7 temp_dir = "/var/infinity/tmp" result_cache = "off" memindex_memory_quota = "1GB" [wal] wal_dir = "/var/infinity/wal" [resource] EOFMT cat < $HSHQ_STACKS_DIR/ragflow/doc_meta_es_mapping.json { "settings": { "index": { "number_of_shards": 2, "number_of_replicas": 0, "refresh_interval": "1000ms" } }, "mappings": { "_source": { "enabled": true }, "dynamic": "runtime", "properties": { "id": { "type": "keyword", "store": true }, "kb_id": { "type": "keyword", "store": true }, "meta_fields": { "type": "object", "dynamic": true } } } } EOFMT cat < $HSHQ_STACKS_DIR/ragflow/doc_meta_infinity_mapping.json { "id": {"type": "varchar", "default": ""}, "kb_id": {"type": "varchar", "default": ""}, "meta_fields": {"type": "json", "default": "{}"} } EOFMT cat < $HSHQ_STACKS_DIR/ragflow/infinity_mapping.json { "id": {"type": "varchar", "default": ""}, "doc_id": {"type": "varchar", "default": ""}, "kb_id": {"type": "varchar", "default": "", "index_type": {"type": "secondary", "cardinality": "low"}}, "mom_id": {"type": "varchar", "default": ""}, "create_time": {"type": "varchar", "default": ""}, "create_timestamp_flt": {"type": "float", "default": 0.0}, "img_id": {"type": "varchar", "default": ""}, "docnm": {"type": "varchar", "default": "", "analyzer": ["rag-coarse", "rag-fine"], "comment": "docnm_kwd, title_tks, title_sm_tks"}, "name_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "tag_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "important_kwd_empty_count": {"type": "integer", "default": 0}, "important_keywords": {"type": "varchar", "default": "", "analyzer": ["rag-coarse", "rag-fine"], "comment": "important_kwd, important_tks"}, "questions": {"type": "varchar", "default": "", "analyzer": ["rag-coarse", "rag-fine"], "comment": "question_kwd, question_tks"}, "content": {"type": "varchar", "default": "", "analyzer": ["rag-coarse", "rag-fine"], "comment": "content_with_weight, content_ltks, content_sm_ltks"}, "authors": {"type": "varchar", "default": "", "analyzer": ["rag-coarse", "rag-fine"], "comment": "authors_tks, authors_sm_tks"}, "page_num_int": {"type": "varchar", "default": ""}, "top_int": {"type": "varchar", "default": ""}, "position_int": {"type": "varchar", "default": ""}, "weight_int": {"type": "integer", "default": 0}, "weight_flt": {"type": "float", "default": 0.0}, "rank_int": {"type": "integer", "default": 0}, "rank_flt": {"type": "float", "default": 0}, "available_int": {"type": "integer", "default": 1, "index_type": {"type": "secondary", "cardinality": "low"}}, "knowledge_graph_kwd": {"type": "varchar", "default": ""}, "entities_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "pagerank_fea": {"type": "integer", "default": 0}, "tag_feas": {"type": "varchar", "default": "", "analyzer": "rankfeatures"}, "from_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "to_entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "entity_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "entity_type_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "source_id": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "n_hop_with_weight": {"type": "varchar", "default": ""}, "mom_with_weight": {"type": "varchar", "default": ""}, "removed_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "doc_type_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "toc_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"}, "raptor_kwd": {"type": "varchar", "default": "", "analyzer": "whitespace-#"} } EOFMT cat < $HSHQ_STACKS_DIR/ragflow/system_settings.json { "system_settings": [ { "name": "enable_whitelist", "source": "variable", "data_type": "bool", "value": "true" }, { "name": "default_role", "source": "variable", "data_type": "string", "value": "" }, { "name": "mail.server", "source": "variable", "data_type": "string", "value": "$SMTP_HOSTNAME" }, { "name": "mail.port", "source": "variable", "data_type": "integer", "value": "$SMTP_HOSTPORT" }, { "name": "mail.use_ssl", "source": "variable", "data_type": "bool", "value": "false" }, { "name": "mail.use_tls", "source": "variable", "data_type": "bool", "value": "false" }, { "name": "mail.username", "source": "variable", "data_type": "string", "value": "" }, { "name": "mail.password", "source": "variable", "data_type": "string", "value": "" }, { "name": "mail.timeout", "source": "variable", "data_type": "integer", "value": "10" }, { "name": "mail.default_sender", "source": "variable", "data_type": "string", "value": "RAGFlow $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>" }, { "name": "mail.frontend_url", "source": "variable", "data_type": "string", "value": "https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN" }, { "name": "sandbox.provider_type", "source": "variable", "data_type": "string", "value": "self_managed" }, { "name": "sandbox.self_managed", "source": "variable", "data_type": "json", "value": "{\"endpoint\": \"http://localhost:9385\", \"timeout\": 30, \"max_retries\": 3, \"pool_size\": 10}" }, { "name": "sandbox.aliyun_codeinterpreter", "source": "variable", "data_type": "json", "value": "{}" }, { "name": "sandbox.e2b", "source": "variable", "data_type": "json", "value": "{}" } ] } EOFMT cat < $HSHQ_STACKS_DIR/ragflow/service_conf.yaml.template ragflow: host: 0.0.0.0 http_port: 9380 secret_key: $RAGFLOW_SECRET_KEY admin: host: 0.0.0.0 http_port: 9381 minio: user: '$RAGFLOW_MINIO_KEY' password: '$RAGFLOW_MINIO_SECRET' host: 'ragflow-minio:9000' bucket: 'ragflow' prefix_path: '' infinity: uri: 'ragflow-infinity:23817' db_name: 'ragflow_db' redis: db: 1 username: '' password: '$RAGFLOW_REDIS_PASSWORD' host: 'ragflow-redis:6379' postgres: name: '$RAGFLOW_DATABASE_NAME' user: '$RAGFLOW_DATABASE_USER' password: '$RAGFLOW_DATABASE_USER_PASSWORD' host: 'ragflow-db' port: 5432 max_connections: 100 stale_timeout: 30 oauth: oidc: display_name: "Authelia" client_id: "$RAGFLOW_OIDC_CLIENT_ID" client_secret: "$RAGFLOW_OIDC_CLIENT_SECRET" issuer: "https://$SUB_AUTHELIA.$HOMESERVER_DOMAIN" scope: "openid email profile" redirect_uri: "https://$SUB_RAGFLOW_APP.$HOMESERVER_DOMAIN/v1/user/oauth/callback/oidc" EOFMT sudo mv $HSHQ_STACKS_DIR/ragflow/doc_meta_es_mapping.json $HSHQ_STACKS_DIR/ragflow/config/doc_meta_es_mapping.json sudo mv $HSHQ_STACKS_DIR/ragflow/doc_meta_infinity_mapping.json $HSHQ_STACKS_DIR/ragflow/config/doc_meta_infinity_mapping.json sudo mv $HSHQ_STACKS_DIR/ragflow/infinity_mapping.json $HSHQ_STACKS_DIR/ragflow/config/infinity_mapping.json sudo mv $HSHQ_STACKS_DIR/ragflow/system_settings.json $HSHQ_STACKS_DIR/ragflow/config/system_settings.json sudo mv $HSHQ_STACKS_DIR/ragflow/service_conf.yaml.template $HSHQ_STACKS_DIR/ragflow/config/service_conf.yaml.template cat < $HSHQ_STACKS_DIR/ragflow/misc/entrypoint.sh #!/usr/bin/env bash set -e # ----------------------------------------------------------------------------- # Usage and command-line argument parsing # ----------------------------------------------------------------------------- function usage() { echo "Usage: \$0 [--disable-webserver] [--disable-taskexecutor] [--disable-datasync] [--consumer-no-beg=] [--consumer-no-end=] [--workers=] [--host-id=]" echo echo " --disable-webserver Disables the web server (nginx + ragflow_server)." echo " --disable-taskexecutor Disables task executor workers." echo " --disable-datasync Disables synchronization of datasource workers." echo " --enable-mcpserver Enables the MCP server." echo " --enable-adminserver Enables the Admin server." echo " --init-superuser Initializes the superuser." echo " --consumer-no-beg= Start range for consumers (if using range-based)." echo " --consumer-no-end= End range for consumers (if using range-based)." echo " --workers= Number of task executors to run (if range is not used)." echo " --host-id= Unique ID for the host (defaults to hostname)." echo echo "Examples:" echo " \$0 --disable-taskexecutor" echo " \$0 --disable-webserver --consumer-no-beg=0 --consumer-no-end=5" echo " \$0 --disable-webserver --workers=2 --host-id=myhost123" echo " \$0 --enable-mcpserver" echo " \$0 --enable-adminserver" echo " \$0 --init-superuser" exit 1 } ENABLE_WEBSERVER=1 # Default to enable web server ENABLE_TASKEXECUTOR=1 # Default to enable task executor ENABLE_DATASYNC=1 ENABLE_MCP_SERVER=0 ENABLE_ADMIN_SERVER=0 # Default close admin server INIT_SUPERUSER_ARGS="" # Default to not initialize superuser CONSUMER_NO_BEG=0 CONSUMER_NO_END=0 WORKERS=1 MCP_HOST="127.0.0.1" MCP_PORT=9382 MCP_BASE_URL="http://127.0.0.1:9380" MCP_SCRIPT_PATH="/ragflow/mcp/server/server.py" MCP_MODE="self-host" MCP_HOST_API_KEY="$RAGFLOW_MCPSERVER_API_KEY" MCP_TRANSPORT_SSE_FLAG="--transport-sse-enabled" MCP_TRANSPORT_STREAMABLE_HTTP_FLAG="--transport-streamable-http-enabled" MCP_JSON_RESPONSE_FLAG="--json-response" # ----------------------------------------------------------------------------- # Host ID logic: # 1. By default, use the system hostname if length <= 32 # 2. Otherwise, use the full MD5 hash of the hostname (32 hex chars) # ----------------------------------------------------------------------------- CURRENT_HOSTNAME="\$(hostname)" if [ \${#CURRENT_HOSTNAME} -le 32 ]; then DEFAULT_HOST_ID="\$CURRENT_HOSTNAME" else DEFAULT_HOST_ID="\$(echo -n "\$CURRENT_HOSTNAME" | md5sum | cut -d ' ' -f 1)" fi HOST_ID="\$DEFAULT_HOST_ID" # Parse arguments for arg in "\$@"; do case \$arg in --disable-webserver) ENABLE_WEBSERVER=0 shift ;; --disable-taskexecutor) ENABLE_TASKEXECUTOR=0 shift ;; --disable-datasync) ENABLE_DATASYNC=0 shift ;; --enable-mcpserver) ENABLE_MCP_SERVER=1 shift ;; --enable-adminserver) ENABLE_ADMIN_SERVER=1 shift ;; --init-superuser) INIT_SUPERUSER_ARGS="--init-superuser" shift ;; --mcp-host=*) MCP_HOST="\${arg#*=}" shift ;; --mcp-port=*) MCP_PORT="\${arg#*=}" shift ;; --mcp-base-url=*) MCP_BASE_URL="\${arg#*=}" shift ;; --mcp-mode=*) MCP_MODE="\${arg#*=}" shift ;; --mcp-host-api-key=*) MCP_HOST_API_KEY="\${arg#*=}" shift ;; --mcp-script-path=*) MCP_SCRIPT_PATH="\${arg#*=}" shift ;; --no-transport-sse-enabled) MCP_TRANSPORT_SSE_FLAG="--no-transport-sse-enabled" shift ;; --no-transport-streamable-http-enabled) MCP_TRANSPORT_STREAMABLE_HTTP_FLAG="--no-transport-streamable-http-enabled" shift ;; --no-json-response) MCP_JSON_RESPONSE_FLAG="--no-json-response" shift ;; --consumer-no-beg=*) CONSUMER_NO_BEG="\${arg#*=}" shift ;; --consumer-no-end=*) CONSUMER_NO_END="\${arg#*=}" shift ;; --workers=*) WORKERS="\${arg#*=}" shift ;; --host-id=*) HOST_ID="\${arg#*=}" shift ;; *) usage ;; esac done # ----------------------------------------------------------------------------- # Replace env variables in the service_conf.yaml file # ----------------------------------------------------------------------------- CONF_DIR="/ragflow/conf" TEMPLATE_FILE="\${CONF_DIR}/service_conf.yaml.template" CONF_FILE="\${CONF_DIR}/service_conf.yaml" rm -f "\${CONF_FILE}" DEF_ENV_VALUE_PATTERN="\\$\{([^:]+):-([^}]+)\}" while IFS= read -r line || [[ -n "\$line" ]]; do if [[ "\$line" =~ DEF_ENV_VALUE_PATTERN ]]; then varname="\${BASH_REMATCH[1]}" default="\${BASH_REMATCH[2]}" if [ -n "\${!varname}" ]; then eval "echo \"\$line"\" >> "\${CONF_FILE}" else echo "\$line" | sed -E "s/\\\\$\{[^:]+:-([^}]+)\}/\1/g" >> "\${CONF_FILE}" fi else eval "echo \"\$line\"" >> "\${CONF_FILE}" fi done < "\${TEMPLATE_FILE}" export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu/" PY=python3 # ----------------------------------------------------------------------------- # Function(s) # ----------------------------------------------------------------------------- function task_exe() { local consumer_id="\$1" local host_id="\$2" JEMALLOC_PATH="\$(pkg-config --variable=libdir jemalloc)/libjemalloc.so" while true; do LD_PRELOAD="\$JEMALLOC_PATH" "\$PY" rag/svr/task_executor.py "\${host_id}_\${consumer_id}" & wait; sleep 1; done } function start_mcp_server() { echo "Starting MCP Server on \${MCP_HOST}:\${MCP_PORT} with base URL \${MCP_BASE_URL}..." "\$PY" "\${MCP_SCRIPT_PATH}" --host="\${MCP_HOST}" --port="\${MCP_PORT}" --base-url="\${MCP_BASE_URL}" --mode="\${MCP_MODE}" --api-key="\${MCP_HOST_API_KEY}" "\${MCP_TRANSPORT_SSE_FLAG}" "\${MCP_TRANSPORT_STREAMABLE_HTTP_FLAG}" "\${MCP_JSON_RESPONSE_FLAG}" & } function ensure_docling() { [[ "\${USE_DOCLING}" == "true" ]] || { echo "[docling] disabled by USE_DOCLING"; return 0; } DOCLING_PIN="\${DOCLING_VERSION:-==2.71.0}" "\$PY" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('docling') else 1)" || uv pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --extra-index-url https://pypi.org/simple --no-cache-dir "docling\${DOCLING_PIN}" } # ----------------------------------------------------------------------------- # Start components based on flags # ----------------------------------------------------------------------------- ensure_docling if [[ "\${ENABLE_WEBSERVER}" -eq 1 ]]; then echo "Starting nginx..." /usr/sbin/nginx echo "Starting ragflow_server..." while true; do "\$PY" api/ragflow_server.py \${INIT_SUPERUSER_ARGS} & wait; sleep 1; done & fi if [[ "\${ENABLE_DATASYNC}" -eq 1 ]]; then echo "Starting data sync..." while true; do "\$PY" rag/svr/sync_data_source.py & wait; sleep 1; done & fi if [[ "\${ENABLE_ADMIN_SERVER}" -eq 1 ]]; then echo "Starting admin_server..." while true; do "\$PY" admin/server/admin_server.py & wait; sleep 1; done & fi if [[ "\${ENABLE_MCP_SERVER}" -eq 1 ]]; then start_mcp_server fi if [[ "\${ENABLE_TASKEXECUTOR}" -eq 1 ]]; then if [[ "\${CONSUMER_NO_END}" -gt "\${CONSUMER_NO_BEG}" ]]; then echo "Starting task executors on host '\${HOST_ID}' for IDs in [\${CONSUMER_NO_BEG}, \${CONSUMER_NO_END})..." for (( i=CONSUMER_NO_BEG; i $HOME/ragflow-compose.yml $STACK_VERSION_PREFIX ragflow $(getScriptStackVersion ragflow) services: ragflow-db: image: $(getScriptImageByContainerName ragflow-db) container_name: ragflow-db hostname: ragflow-db restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: mariadbd --innodb-buffer-pool-size=128M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 --skip-name-resolve networks: - int-ragflow-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ragflow-db:/var/lib/mysql - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportMariaDB.sh:/exportDB.sh:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/dbexport:/dbexport labels: - "ofelia.enabled=true" - "ofelia.job-exec.ragflow-hourly-db.schedule=@every 1h" - "ofelia.job-exec.ragflow-hourly-db.command=/exportDB.sh" - "ofelia.job-exec.ragflow-hourly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ragflow-hourly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ragflow-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ragflow-hourly-db.email-from=RAGFlow Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ragflow-hourly-db.mail-only-on-error=true" - "ofelia.job-exec.ragflow-monthly-db.schedule=0 0 8 1 * *" - "ofelia.job-exec.ragflow-monthly-db.command=/exportDB.sh" - "ofelia.job-exec.ragflow-monthly-db.smtp-host=$SMTP_HOSTNAME" - "ofelia.job-exec.ragflow-monthly-db.smtp-port=$SMTP_HOSTPORT" - "ofelia.job-exec.ragflow-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS" - "ofelia.job-exec.ragflow-monthly-db.email-from=RAGFlow Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>" - "ofelia.job-exec.ragflow-monthly-db.mail-only-on-error=false" ragflow-infinity: image: $(getScriptImageByContainerName ragflow-infinity) container_name: ragflow-infinity hostname: ragflow-infinity restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ragflow-db command: ["-f", "/infinity_conf.toml"] mem_limit: 8g ulimits: nofile: soft: 500000 hard: 500000 networks: - int-ragflow-net - dock-dbs-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - v-ragflow-infinity:/var/infinity - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/misc/infinity_conf.toml:/infinity_conf.toml ragflow-app: image: $(getScriptImageByContainerName ragflow-app) container_name: ragflow-app hostname: ragflow-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ragflow-db - ragflow-infinity - ragflow-minio - ragflow-redis command: - --enable-adminserver - --init-model-provider-tables - --init-superuser networks: - int-ragflow-net - dock-ext-net - dock-internalmail-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - v-ragflow-config:/ragflow/conf - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/logs:/ragflow/logs - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/nginx/ragflow.conf:/etc/nginx/conf.d/ragflow.conf - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/nginx/proxy.conf:/etc/nginx/proxy.conf - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/nginx/nginx.conf:/etc/nginx/nginx.conf - /etc/ssl/certs/ca-certificates.crt:/ragflow/.venv/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:9380/api/v1/system/healthz"] interval: 60s timeout: 10s retries: 5 start_period: 60s ragflow-minio: image: $(getScriptImageByContainerName ragflow-minio) container_name: ragflow-minio hostname: ragflow-minio restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true depends_on: - ragflow-db entrypoint: > /bin/sh -c " minio server /data --address ':9000' --console-address ':9001' & MINIO_PID=\$\$! while ! curl -s http://localhost:9000/minio/health/live; do echo 'Waiting for MinIO to start...' sleep 1 done sleep 5 mc alias set minio http://localhost:9000 ${RAGFLOW_MINIO_KEY} ${RAGFLOW_MINIO_SECRET} echo 'Creating bucket ragflow' mc mb minio/ragflow || true wait \$\$MINIO_PID " networks: - int-ragflow-net - dock-proxy-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/minio:/data ragflow-redis: image: $(getScriptImageByContainerName ragflow-redis) container_name: ragflow-redis restart: unless-stopped security_opt: - no-new-privileges:true command: redis-server --requirepass $RAGFLOW_REDIS_PASSWORD --appendonly no --maxmemory 128mb --maxmemory-policy allkeys-lru networks: - int-ragflow-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro volumes: v-ragflow-config: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/config v-ragflow-db: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/db v-ragflow-infinity: driver: local driver_opts: type: none o: bind device: \${PORTAINER_HSHQ_STACKS_DIR}/ragflow/infinity networks: dock-proxy-net: name: dock-proxy external: true dock-internalmail-net: name: dock-internalmail external: true dock-ext-net: name: dock-ext external: true dock-dbs-net: name: dock-dbs external: true dock-aipriv-net: name: dock-aipriv external: true int-ragflow-net: driver: bridge internal: true ipam: driver: default EOFMT } # TabbyML function installTabbyML() { set +e is_integrate_hshq=$1 checkDeleteStackAndDirectory tabbyml "TabbyML" cdRes=$? if [ $cdRes -ne 0 ]; then return 1 fi pullImage $(getScriptImageByContainerName tabbyml-app) if [ $? -ne 0 ]; then return 1 fi set -e mkdir $HSHQ_STACKS_DIR/tabbyml mkdir $HSHQ_STACKS_DIR/tabbyml/data initServicesCredentials set +e addUserMailu alias $TABBYML_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS outputConfigTabbyML installStack tabbyml tabbyml-app "Listening at http" $HOME/tabbyml.env retVal=$? if [ $retVal -ne 0 ]; then return $retVal fi if ! [ "$TABBYML_INIT_ENV" = "true" ]; then sendEmail -s "$FMLNAME_TABBYML_APP Admin Login Info" -b "$FMLNAME_TABBYML_APP Admin Email: $TABBYML_ADMIN_EMAIL_ADDRESS\n$FMLNAME_TABBYML_APP Admin Password: $TABBYML_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" TABBYML_INIT_ENV=true updateConfigVar TABBYML_INIT_ENV $TABBYML_INIT_ENV fi sleep 3 bash $HOME/setupDBSettings.sh rm -f $HOME/setupDBSettings.sh docker container restart tabbyml-app > /dev/null 2>&1 if [ -z "$FMLNAME_TABBYML_APP" ]; then set +e echo "ERROR: Formal name is empty, returning..." return 1 fi set -e inner_block="" inner_block=$inner_block">>https://$SUB_TABBYML_APP.$HOMESERVER_DOMAIN {\n" inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n" inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n" inner_block=$inner_block">>>>handle @subnet {\n" inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n" inner_block=$inner_block">>>>>>reverse_proxy http://tabbyml-app:8080 {\n" inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n" inner_block=$inner_block">>>>>>}\n" inner_block=$inner_block">>>>}\n" inner_block=$inner_block">>>>respond 404\n" inner_block=$inner_block">>}" updateCaddyBlocks $SUB_TABBYML_APP $MANAGETLS_TABBYML_APP "$is_integrate_hshq" $NETDEFAULT_TABBYML_APP "$inner_block" insertSubAuthelia $SUB_TABBYML_APP.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME} if ! [ "$is_integrate_hshq" = "false" ]; then insertEnableSvcAll tabbyml "$FMLNAME_TABBYML_APP" $USERTYPE_TABBYML_APP "https://$SUB_TABBYML_APP.$HOMESERVER_DOMAIN" "tabbyml.png" "$(getHeimdallOrderFromSub $SUB_TABBYML_APP $USERTYPE_TABBYML_APP)" restartAllCaddyContainers fi } function outputConfigTabbyML() { cat < $HOME/tabbyml-compose.yml $STACK_VERSION_PREFIX tabbyml $(getScriptStackVersion tabbyml) services: tabbyml-app: image: $(getScriptImageByContainerName tabbyml-app) container_name: tabbyml-app hostname: tabbyml-app restart: unless-stopped env_file: stack.env security_opt: - no-new-privileges:true command: serve networks: - dock-ext-net - dock-aipriv-net volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl/certs:/etc/ssl/certs:ro - /usr/share/ca-certificates:/usr/share/ca-certificates:ro - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - \${PORTAINER_HSHQ_STACKS_DIR}/tabbyml/data:/data networks: dock-aipriv-net: name: dock-aipriv external: true dock-ext-net: name: dock-ext external: true EOFMT cat < $HOME/tabbyml.env TZ=\${PORTAINER_TZ} TABBY_WEBSERVER_JWT_TOKEN_SECRET=$TABBYML_JWT_SECRET EOFMT cat < $HSHQ_STACKS_DIR/tabbyml/data/config.toml [model.completion.http] kind = "openai/completion" model_name = "Chat" api_endpoint = "http://litellm-proxy:4000/v1" api_key = "$LITELLM_AUTH_KEY" prompt_template = "
 {prefix} {suffix} "

[model.chat.http]
kind = "openai/chat"
model_name = "Chat"
api_key = "$LITELLM_AUTH_KEY"
api_endpoint = "http://litellm-proxy:4000/v1"
chat_template = "{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if message['role'] == 'user' %}{{ '[INST] ' + message['content'] + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ message['content'] + ' ' }}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}"

[model.embedding.http]
kind = "openai/embedding"
model_name = "Embed"
api_endpoint = "http://litellm-proxy:4000/v1"
api_key = "$LITELLM_AUTH_KEY"
EOFMT
  cat < $HOME/setupDBSettings.sh
#!/bin/bash
tabbyml_db=$HSHQ_STACKS_DIR/tabbyml/data/ee/db.sqlite
uuid=\$(uuidgen | tr -d '-')
uuid2=\$(uuidgen | tr -d '-')
encoded_id=\$(echo -n "0" | md5sum | cut -d' ' -f1 | cut -c1-6)
token="\${encoded_id}\${uuid}"
dtnow=\$(date -u '+%Y-%m-%d %H:%M:%S')
TABBYML_ADMIN_PASSWORD_HASH=\$(echo -n "$TABBYML_ADMIN_PASSWORD" | argon2 \$(pwgen -c -n 16 1) -id -e -t 2 -m 15 -p 1 -l 32)
sudo sqlite3 \$tabbyml_db "INSERT INTO users(id,email,is_admin,created_at,updated_at,auth_token,active,password_encrypted,avatar,name) VALUES(1,'$TABBYML_ADMIN_EMAIL_ADDRESS',1,'\$dtnow','\$dtnow','auth_\$uuid2',1,'\$TABBYML_ADMIN_PASSWORD_HASH',NULL,'TabbyML HSHQ Admin');"
sudo sqlite3 \$tabbyml_db "INSERT INTO refresh_tokens(id,user_id,token,expires_at,created_at) VALUES(1,1,'\$token','\$dtnow','\$dtnow');"
sudo sqlite3 \$tabbyml_db "INSERT INTO sqlite_sequence(name,seq) VALUES('users',1);"
sudo sqlite3 \$tabbyml_db "INSERT INTO sqlite_sequence(name,seq) VALUES('refresh_tokens',1);"
sudo sqlite3 \$tabbyml_db "INSERT INTO server_setting(id,security_allowed_register_domain_list,security_disable_client_side_telemetry,network_external_url,billing_enterprise_license,security_disable_password_login,branding_logo,branding_icon) VALUES(1,NULL,0,'http://localhost:8080',NULL,0,NULL,NULL);"

EOFDS
}

function performUpdateTabbyML()
{
  perform_stack_name=tabbyml
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v1
      curImageList=mirror.gcr.io/tabbyml/tabby:0.31.2
      image_update_map[0]="mirror.gcr.io/tabbyml/tabby:0.31.2,mirror.gcr.io/tabbyml/tabby:0.31.2"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

# DeepWikiOpen
function installDeepWikiOpen()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory deepwikiopen "DeepWikiOpen"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName deepwikiopen-app)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/deepwikiopen
  mkdir $HSHQ_STACKS_DIR/deepwikiopen/config
  mkdir $HSHQ_STACKS_DIR/deepwikiopen/adalflow
  initServicesCredentials
  set +e
  outputConfigDeepWikiOpen
  installStack deepwikiopen deepwikiopen-app "Application startup complete" $HOME/deepwikiopen.env
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$DEEPWIKI_OPEN_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_DEEPWIKI_OPEN_APP Auth Code" -b "$FMLNAME_DEEPWIKI_OPEN_APP Auth Code: $DEEPWIKI_OPEN_AUTH_CODE\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    DEEPWIKI_OPEN_INIT_ENV=true
    updateConfigVar DEEPWIKI_OPEN_INIT_ENV $DEEPWIKI_OPEN_INIT_ENV
  fi
  sleep 3
  if [ -z "$FMLNAME_DEEPWIKI_OPEN_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_DEEPWIKI_OPEN_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://deepwikiopen-app:3000 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_DEEPWIKI_OPEN_APP $MANAGETLS_DEEPWIKI_OPEN_APP "$is_integrate_hshq" $NETDEFAULT_DEEPWIKI_OPEN_APP "$inner_block"
  insertSubAuthelia $SUB_DEEPWIKI_OPEN_APP.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME}

  inner_block=""
  inner_block=$inner_block">>https://$SUB_DEEPWIKI_OPEN_API.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://deepwikiopen-app:8001 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_DEEPWIKI_OPEN_API $MANAGETLS_DEEPWIKI_OPEN_API "$is_integrate_hshq" $NETDEFAULT_DEEPWIKI_OPEN_API "$inner_block"
  insertSubAuthelia $SUB_DEEPWIKI_OPEN_API.$HOMESERVER_DOMAIN bypass

  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll deepwikiopen "$FMLNAME_DEEPWIKI_OPEN_APP" $USERTYPE_DEEPWIKI_OPEN_APP "https://$SUB_DEEPWIKI_OPEN_APP.$HOMESERVER_DOMAIN" "deepwikiopen.png" "$(getHeimdallOrderFromSub $SUB_DEEPWIKI_OPEN_APP $USERTYPE_DEEPWIKI_OPEN_APP)"
    restartAllCaddyContainers
  fi
}

function outputConfigDeepWikiOpen()
{
  cat < $HOME/deepwikiopen-compose.yml
$STACK_VERSION_PREFIX deepwikiopen $(getScriptStackVersion deepwikiopen)

services:
  deepwikiopen-app:
    image: $(getScriptImageByContainerName deepwikiopen-app)
    container_name: deepwikiopen-app
    hostname: deepwikiopen-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - dock-ext-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/deepwikiopen/adalflow:/root/.adalflow
      - \${PORTAINER_HSHQ_STACKS_DIR}/deepwikiopen/config/embedder.json:/app/api/config/embedder.json
      - \${PORTAINER_HSHQ_STACKS_DIR}/deepwikiopen/config/generator.json:/app/api/config/generator.json

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  dock-ext-net:
    name: dock-ext
    external: true

EOFMT
  cat < $HOME/deepwikiopen.env
TZ=\${PORTAINER_TZ}
SERVER_BASE_URL=https://$SUB_DEEPWIKI_OPEN_API.$HOMESERVER_DOMAIN
DEEPWIKI_AUTH_MODE=1
DEEPWIKI_AUTH_CODE=$DEEPWIKI_OPEN_AUTH_CODE
OPENAI_BASE_URL=http://litellm-proxy:4000/v1
OPENAI_API_BASE_URLS=http://litellm-proxy:4000/v1
OPENAI_API_KEY=$LITELLM_AUTH_KEY
OPENAI_API_KEYS=$LITELLM_AUTH_KEY
ENABLE_OPENAI_API=true
NODE_ENV=production
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
EOFMT
  cat < $HSHQ_STACKS_DIR/deepwikiopen/config/embedder.json
{
  "embedder": {
    "client_class": "OpenAIClient",
    "model_kwargs": {
      "model": "Embed"
    }
  },
  "retriever": {
    "top_k": 20
  },
  "text_splitter": {
    "split_by": "word",
    "chunk_size": 350,
    "chunk_overlap": 100
  }
}
EOFMT
  cat < $HSHQ_STACKS_DIR/deepwikiopen/config/generator.json
{
  "default_provider": "openai",
  "providers": {
    "openai": {
      "default_model": "LongContext",
      "supportsCustomModel": true,
      "models": {
        "LongContext": {
          "options": {
            "temperature": 0.7,
            "top_p": 0.8,
            "num_ctx": 131072
          }
        },
        "Thinking": {
          "options": {
            "temperature": 0.7,
            "top_p": 0.8,
            "num_ctx": 131072
          }
        },
        "Vision": {
          "options": {
            "temperature": 0.7,
            "top_p": 0.8,
            "num_ctx": 32768
          }
        },
        "Fast": {
          "options": {
            "temperature": 0.7,
            "top_p": 0.8,
            "num_ctx": 32768
          }
        }
      }
    }
  }
}
EOFMT
}

function performUpdateDeepWikiOpen()
{
  perform_stack_name=deepwikiopen
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v1
      curImageList=ghcr.io/asyncfuncai/deepwiki-open:sha-d48f5bc
      image_update_map[0]="ghcr.io/asyncfuncai/deepwiki-open:sha-d48f5bc,ghcr.io/asyncfuncai/deepwiki-open:sha-d48f5bc"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

# Docling
function installDocling()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory docling "Docling"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  buildOrPullImage $(getScriptImageByContainerName docling-app)
  if [ $? -ne 0 ]; then
    return 1
  fi
  buildOrPullImage $(getScriptImageByContainerName docling-redis)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/docling
  mkdir $HSHQ_STACKS_DIR/docling/config
  mkdir $HSHQ_STACKS_DIR/docling/data
  initServicesCredentials
  set +e
  outputConfigDocling
  # If we need a string to monitor, its this: "Application startup complete"
  installStack docling docling-app "" $HOME/docling.env 1
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  sleep 3
  if [ -z "$FMLNAME_DOCLING_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_DOCLING_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://docling-app:5001 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_DOCLING_APP $MANAGETLS_DOCLING_APP "$is_integrate_hshq" $NETDEFAULT_DOCLING_APP "$inner_block"
  insertSubAuthelia $SUB_DOCLING_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll docling "$FMLNAME_DOCLING_APP" $USERTYPE_DOCLING_APP "https://$SUB_DOCLING_APP.$HOMESERVER_DOMAIN/ui/" "docling.png" "$(getHeimdallOrderFromSub $SUB_DOCLING_APP $USERTYPE_DOCLING_APP)"
    restartAllCaddyContainers
  fi
}

function outputConfigDocling()
{
  cat < $HOME/docling-compose.yml
$STACK_VERSION_PREFIX docling $(getScriptStackVersion docling)

services:
  docling-app:
    image: $(getScriptImageByContainerName docling-app)
    container_name: docling-app
    hostname: docling-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-docling-net
      - dock-ext-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  docling-redis:
    image: $(getScriptImageByContainerName docling-redis)
    container_name: docling-redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command: redis-server
      --requirepass $DOCLING_REDIS_PASSWORD
      --appendonly no
    networks:
      - int-docling-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  int-docling-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  cat < $HOME/docling.env
TZ=\${PORTAINER_TZ}
DOCLING_SERVE_ENABLE_UI=true
DOCLING_SERVE_ENG_RQ_REDIS_URL=redis://:$DOCLING_REDIS_PASSWORD@docling-redis:6379
EOFMT
}

function performUpdateDocling()
{
  perform_stack_name=docling
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v3
      curImageList=hshq/docling:v1,mirror.gcr.io/valkey/valkey:alpine3.23
      image_update_map[0]="hshq/docling:v1,hshq/docling-serve:v1.38.1"
      image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
    ;;
    2)
      newVer=v3
      curImageList=hshq/docling-serve:v1.31.0,mirror.gcr.io/valkey/valkey:alpine3.23
      image_update_map[0]="hshq/docling-serve:v1.31.0,hshq/docling-serve:v1.38.1"
      image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
    ;;
    3)
      newVer=v4
      curImageList=hshq/docling-serve:v1.38.1,mirror.gcr.io/valkey/valkey:alpine3.23
      image_update_map[0]="hshq/docling-serve:v1.38.1,hshq/docling-serve:v1.38.1.1"
      image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
    ;;
    4)
      newVer=v4
      curImageList=hshq/docling-serve:v1.38.1.1,mirror.gcr.io/valkey/valkey:alpine3.23
      image_update_map[0]="hshq/docling-serve:v1.38.1.1,hshq/docling-serve:v1.38.1.1"
      image_update_map[1]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

function buildImageDoclingV1()
{
  set +e
  curDoclingVersion=v1.14.3
  echo -e "\n========================================================================"
  echo -e "  The Docling image is being built. It can take awhile for the process"
  echo -e "  to complete, so please be patient."
  echo -e "========================================================================\n"
  sudo rm -fr $HSHQ_BUILD_DIR/docling-serve
  cd $HSHQ_BUILD_DIR
  git -c advice.detachedHead=false clone --depth 1 --branch $curDoclingVersion https://github.com/docling-project/docling-serve.git
  cd docling-serve
  sed -i "s/TAG=.*/TAG=$curDoclingVersion/g" Makefile
  make docling-serve-cpu-image
  rtval=$?
  cd
  sudo rm -fr $HSHQ_BUILD_DIR/docling-serve
  docker tag ghcr.io/docling-project/docling-serve-cpu:$curDoclingVersion hshq/docling:v1
  return $rtval
}

function buildImageDoclingV2()
{
  buildImageDoclingHSHQ v1.31.0
}

function buildImageDoclingV3()
{
  buildImageDoclingHSHQ v1.38.1
}

function buildImageDoclingV4()
{
  buildImageDoclingHSHQ v1.38.1.1
}

function buildImageDoclingHSHQ()
{
  set +e
  curDoclingVersion="$1"
  echo -e "\n========================================================================"
  echo -e "  The Docling image is being built. It can take awhile for the process"
  echo -e "  to complete, so please be patient."
  echo -e "========================================================================\n"
  sudo rm -fr $HSHQ_BUILD_DIR/docling-serve
  cd $HSHQ_BUILD_DIR
  git -c advice.detachedHead=false clone --depth 1 --branch $curDoclingVersion https://github.com/homeserverhq/docling-serve.git
  cd docling-serve
  make docling-serve-cpu-image TAG=$curDoclingVersion
  rtval=$?
  cd
  sudo rm -fr $HSHQ_BUILD_DIR/docling-serve
  return $rtval
}

# Dify
function installDify()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory dify "Dify"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-db)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-vectordb)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-ssrf)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-api)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-plugind)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-sandbox)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-web)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-redis)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-nginx)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName dify-minio)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/dify
  mkdir $HSHQ_STACKS_DIR/dify/config
  mkdir $HSHQ_STACKS_DIR/dify/qdrant
  mkdir $HSHQ_STACKS_DIR/dify/storage
  mkdir $HSHQ_STACKS_DIR/dify/storage/privkeys
  mkdir $HSHQ_STACKS_DIR/dify/plugind
  mkdir $HSHQ_STACKS_DIR/dify/sandbox
  mkdir $HSHQ_STACKS_DIR/dify/sandbox/conf
  mkdir $HSHQ_STACKS_DIR/dify/sandbox/dependencies
  mkdir $HSHQ_STACKS_DIR/dify/nginx
  mkdir $HSHQ_STACKS_DIR/dify/nginx/conf.d
  mkdir $HSHQ_STACKS_DIR/dify/minio
  mkdir $HSHQ_STACKS_DIR/dify/db
  mkdir $HSHQ_STACKS_DIR/dify/dbexport
  chmod 777 $HSHQ_STACKS_DIR/dify/dbexport
  initServicesCredentials
  set +e
  addUserMailu alias $DIFY_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS
  outputConfigDify
  installStack dify dify-api "Listening at" $HOME/dify.env 5
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  waitForContainerLogString dify-beat 5 300 "beat: Starting..."
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  waitForContainerLogString dify-db 5 300 "database system is ready to accept connections"
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  waitForContainerLogString dify-plugind 5 300 "listening on: tcp://0.0.0.0:5003"
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  waitForContainerLogString dify-sandbox 5 300 "python dependencies sandbox initialized"
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  waitForContainerLogString dify-ssrf 5 300 "Accepting HTTP Socket connections at "
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  waitForContainerLogString dify-vectordb 5 300 "Qdrant HTTP listening on"
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  waitForContainerLogString dify-worker 5 300 "celery@dify-worker ready"
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$DIFY_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_DIFY_APP Admin Login Info" -b "$FMLNAME_DIFY_APP Admin Email: $DIFY_ADMIN_EMAIL_ADDRESS\n$FMLNAME_DIFY_APP Admin Password: $DIFY_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    DIFY_INIT_ENV=true
    updateConfigVar DIFY_INIT_ENV $DIFY_INIT_ENV
  fi
  echo "Dify installed, sleeping for 30 seconds..."
  sleep 30
  docker exec dify-db /dbexport/setupDBSettings.sh > /dev/null 2>&1
  rm -f $HSHQ_STACKS_DIR/dify/dbexport/setupDBSettings.sh
  addReadOnlyUserToDatabase Dify postgres dify-db $DIFY_DATABASE_NAME $DIFY_DATABASE_USER $DIFY_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/dify/dbexport $DIFY_DATABASE_READONLYUSER $DIFY_DATABASE_READONLYUSER_PASSWORD
  sleep 10
  startStopStack dify stop
  echo "Restarting dify stack..."
  sleep 5
  startStopStack dify start
  if [ -z "$FMLNAME_DIFY_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADERCORSAUTOMATED\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://dify-nginx {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_DIFY_APP $MANAGETLS_DIFY_APP "$is_integrate_hshq" $NETDEFAULT_DIFY_APP "$inner_block"
  insertSubAuthelia $SUB_DIFY_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll dify "$FMLNAME_DIFY_APP" $USERTYPE_DIFY_APP "https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN" "dify.png" "$(getHeimdallOrderFromSub $SUB_DIFY_APP $USERTYPE_DIFY_APP)"
    restartAllCaddyContainers
    checkAddDBConnection true dify "$FMLNAME_DIFY_APP" postgres dify-db $DIFY_DATABASE_NAME $DIFY_DATABASE_USER $DIFY_DATABASE_USER_PASSWORD
  fi
}

function outputConfigDify()
{
  cat < $HOME/dify-compose.yml
$STACK_VERSION_PREFIX dify $(getScriptStackVersion dify)

services:
  dify-db:
    image: $(getScriptImageByContainerName dify-db)
    container_name: dify-db
    hostname: dify-db
    user: "\${PORTAINER_UID}:\${PORTAINER_GID}"
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    shm_size: 512mb
    networks:
      - int-dify-net
      - dock-dbs-net
    command: >
      postgres -c 'max_connections=100'
               -c 'shared_buffers=128MB'
               -c 'work_mem=4MB'
               -c 'maintenance_work_mem=64MB'
               -c 'effective_cache_size=4096MB'
               -c 'statement_timeout=0'
               -c 'idle_in_transaction_session_timeout=0'
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/db:/var/lib/postgresql/data
      - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/dbexport:/dbexport
    healthcheck:
      test:
        [
          "CMD",
          "pg_isready",
          "-h",
          "dify-db",
          "-U",
          "$DIFY_DATABASE_USER",
          "-d",
          "$DIFY_DATABASE_NAME",
        ]
      interval: 1s
      timeout: 3s
      retries: 60
    labels:
      - "ofelia.enabled=true"
      - "ofelia.job-exec.dify-hourly-db.schedule=@every 1h"
      - "ofelia.job-exec.dify-hourly-db.command=/exportDB.sh"
      - "ofelia.job-exec.dify-hourly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.dify-hourly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.dify-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.dify-hourly-db.email-from=Dify Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.dify-hourly-db.mail-only-on-error=true"
      - "ofelia.job-exec.dify-monthly-db.schedule=0 0 8 1 * *"
      - "ofelia.job-exec.dify-monthly-db.command=/exportDB.sh"
      - "ofelia.job-exec.dify-monthly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.dify-monthly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.dify-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.dify-monthly-db.email-from=Dify Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.dify-monthly-db.mail-only-on-error=false"

  dify-vectordb:
    image: $(getScriptImageByContainerName dify-vectordb)
    container_name: dify-vectordb
    hostname: dify-vectordb
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-dify-net
      - dock-dbs-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/qdrant:/qdrant/storage

  dify-ssrf:
    image: $(getScriptImageByContainerName dify-ssrf)
    container_name: dify-ssrf
    hostname: dify-ssrf
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-dify-net
      - dock-aipriv-net
    entrypoint:
      [
        "sh",
        "-c",
        "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r\$\$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh",
      ]
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/config/squid.conf.template:/etc/squid/squid.conf.template
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/config/docker-entrypoint.sh:/docker-entrypoint-mount.sh
    environment:
      - HTTP_PORT=3128
      - COREDUMP_DIR=/var/spool/squid
      - REVERSE_PROXY_PORT=8194
      - SANDBOX_HOST=dify-sandbox
      - SANDBOX_PORT=8194

  dify-api:
    image: $(getScriptImageByContainerName dify-api)
    container_name: dify-api
    hostname: dify-api
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      dify-db:
        condition: service_healthy
      dify-vectordb:
        condition: service_started
      dify-redis:
        condition: service_started
    networks:
      - int-dify-net
      - dock-ext-net
      - dock-internalmail-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/storage:/app/api/storage
    environment:
      - MODE=api

  dify-worker:
    image: $(getScriptImageByContainerName dify-worker)
    container_name: dify-worker
    hostname: dify-worker
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      dify-db:
        condition: service_healthy
      dify-vectordb:
        condition: service_started
      dify-redis:
        condition: service_started
    networks:
      - int-dify-net
      - dock-ext-net
      - dock-internalmail-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/storage:/app/api/storage
    environment:
      - MODE=worker
      - PLUGIN_MAX_PACKAGE_SIZE=52428800
      - INNER_API_KEY_FOR_PLUGIN=$DIFY_PLUGIN_DIFY_INNER_API_KEY

  dify-beat:
    image: $(getScriptImageByContainerName dify-beat)
    container_name: dify-beat
    hostname: dify-beat
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      dify-db:
        condition: service_healthy
      dify-vectordb:
        condition: service_started
      dify-redis:
        condition: service_started
    networks:
      - int-dify-net
      - dock-ext-net
      - dock-internalmail-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
    environment:
      - MODE=beat

  dify-plugind:
    image: $(getScriptImageByContainerName dify-plugind)
    container_name: dify-plugind
    hostname: dify-plugind
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      dify-db:
        condition: service_healthy
      dify-vectordb:
        condition: service_started
      dify-redis:
        condition: service_started
    networks:
      - int-dify-net
      - dock-ext-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/plugind:/app/storage
    environment:
      - DB_DATABASE=dify_plugindb
      - SERVER_PORT=5002
      - SERVER_KEY=$DIFY_PLUGIN_DAEMON_KEY
      - MAX_PLUGIN_PACKAGE_SIZE=52428800
      - PPROF_ENABLED=false
      - DIFY_INNER_API_URL=http://dify-api:5001
      - DIFY_INNER_API_KEY=$DIFY_PLUGIN_DIFY_INNER_API_KEY
      - PLUGIN_REMOTE_INSTALLING_HOST=0.0.0.0
      - PLUGIN_REMOTE_INSTALLING_PORT=5003
      - PLUGIN_WORKING_PATH=/app/storage/cwd
      - FORCE_VERIFYING_SIGNATURE=true
      - PYTHON_ENV_INIT_TIMEOUT=120
      - PLUGIN_MAX_EXECUTION_TIMEOUT=600
      - PLUGIN_STDIO_BUFFER_SIZE=1024
      - PLUGIN_STDIO_MAX_BUFFER_SIZE=5242880
      - PLUGIN_STORAGE_TYPE=local
      - PLUGIN_STORAGE_LOCAL_ROOT=/app/storage
      - PLUGIN_INSTALLED_PATH=plugin
      - PLUGIN_PACKAGE_CACHE_PATH=plugin_packages
      - PLUGIN_MEDIA_CACHE_PATH=assets
      - S3_USE_AWS_MANAGED_IAM=false
      - S3_USE_AWS=false

  dify-sandbox:
    image: $(getScriptImageByContainerName dify-sandbox)
    container_name: dify-sandbox
    hostname: dify-sandbox
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-dify-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/sandbox/conf:/conf
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/sandbox/dependencies:/dependencies
    environment:
      - API_KEY=$DIFY_SANDBOX_API_KEY
      - GIN_MODE=release
      - WORKER_TIMEOUT=15
      - ENABLE_NETWORK=true
      - HTTP_PROXY=http://dify-ssrf:3128
      - HTTPS_PROXY=http://dify-ssrf:3128
      - SANDBOX_PORT=8194

  dify-web:
    image: $(getScriptImageByContainerName dify-web)
    container_name: dify-web
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    depends_on:
      dify-db:
        condition: service_healthy
      dify-vectordb:
        condition: service_started
      dify-redis:
        condition: service_started
      dify-api:
        condition: service_started
      dify-plugind:
        condition: service_started
    networks:
      - int-dify-net
      - dock-ext-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
    environment:
      - CONSOLE_API_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
      - APP_API_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
      - HOSTNAME=
      - SECRET_KEY=$DIFY_SECRET_KEY
      - AMPLITUDE_API_KEY=
      - NEXT_PUBLIC_COOKIE_DOMAIN=
      - SENTRY_DSN=
      - NEXT_TELEMETRY_DISABLED=1
      - TEXT_GENERATION_TIMEOUT_MS=60000
      - CSP_WHITELIST=
      - ALLOW_EMBED=false
      - ALLOW_UNSAFE_DATA_SCHEME=false
      - MARKETPLACE_API_URL=https://marketplace.dify.ai
      - MARKETPLACE_URL=https://marketplace.dify.ai
      - TOP_K_MAX_VALUE=
      - INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=
      - PM2_INSTANCES=2
      - LOOP_NODE_MAX_COUNT=100
      - MAX_TOOLS_NUM=10
      - MAX_PARALLEL_LIMIT=10
      - MAX_ITERATIONS_NUM=99
      - MAX_TREE_DEPTH=50
      - ENABLE_WEBSITE_JINAREADER=true
      - ENABLE_WEBSITE_FIRECRAWL=true
      - ENABLE_WEBSITE_WATERCRAWL=true

  dify-redis:
    image: $(getScriptImageByContainerName dify-redis)
    container_name: dify-redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command: redis-server
      --requirepass $DIFY_REDIS_PASSWORD
      --appendonly no
    networks:
      - int-dify-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  dify-nginx:
    image: $(getScriptImageByContainerName dify-nginx)
    container_name: dify-nginx
    hostname: dify-nginx
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      dify-db:
        condition: service_healthy
      dify-vectordb:
        condition: service_started
      dify-redis:
        condition: service_started
      dify-api:
        condition: service_started
      dify-web:
        condition: service_started
    networks:
      - int-dify-net
      - dock-proxy-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/nginx/nginx.conf.template:/etc/nginx/nginx.conf.template
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/nginx/proxy.conf.template:/etc/nginx/proxy.conf.template
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/nginx/https.conf.template:/etc/nginx/https.conf.template
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/nginx/conf.d:/etc/nginx/conf.d
      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/nginx/docker-entrypoint.sh:/docker-entrypoint.sh:ro

#  dify-minio:
#    image: $(getScriptImageByContainerName dify-minio)
#    container_name: dify-minio
#    hostname: dify-minio
#    restart: unless-stopped
#    env_file: stack.env
#    security_opt:
#      - no-new-privileges:true
#    depends_on:
#      - dify-db
#    networks:
#      - int-dify-net
#    entrypoint: >
#      /bin/sh -c "
#        minio server /data --address ':9000' --console-address ':9001' &
#        MINIO_PID=\\\$!
#        while ! curl -s http://localhost:9000/minio/health/live; do
#          echo 'Waiting for MinIO to start...'
#          sleep 1
#        done
#        sleep 5
#        mc alias set minio http://localhost:9000 ${DIFY_MINIO_KEY} ${DIFY_MINIO_SECRET}
#        echo 'Creating bucket dify'
#        mc mb minio/dify
#        wait \\\$MINIO_PID
#      "
#    volumes:
#      - /etc/localtime:/etc/localtime:ro
#      - /etc/timezone:/etc/timezone:ro
#      - /etc/ssl/certs:/etc/ssl/certs:ro
#      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
#      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
#      - \${PORTAINER_HSHQ_STACKS_DIR}/dify/minio:/data

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-internalmail-net:
    name: dock-internalmail
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  dock-dbs-net:
    name: dock-dbs
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  int-dify-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  #Set these for Minio container
#MINIO_ROOT_USER=$DIFY_MINIO_KEY
#MINIO_ROOT_PASSWORD=$DIFY_MINIO_SECRET
#MINIO_REGION=us-east-1
#S3_ENDPOINT=http://dify-minio:9000
#S3_REGION=us-east-1
#S3_BUCKET_NAME=dify
#S3_ACCESS_KEY=$DIFY_MINIO_KEY
#S3_SECRET_KEY=$DIFY_MINIO_SECRET
  cat < $HOME/dify.env
TZ=\${PORTAINER_TZ}
LOG_TZ=\${PORTAINER_TZ}
POSTGRES_DB=$DIFY_DATABASE_NAME
POSTGRES_USER=$DIFY_DATABASE_USER
POSTGRES_PASSWORD=$DIFY_DATABASE_USER_PASSWORD
HTTP_PROXY=
HTTPS_PROXY=
NO_PROXY=dify-db,dify-vectordb,dify-ssrf,dify-api,dify-worker,dify-beat,dify-plugind,dify-sandbox,dify-web,dify-redis,dify-nginx,dify-minio
CONSOLE_API_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
CONSOLE_WEB_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
SERVICE_API_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
TRIGGER_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
APP_API_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
APP_WEB_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
FILES_URL=https://$SUB_DIFY_APP.$HOMESERVER_DOMAIN
INTERNAL_FILES_URL=http://dify-api:5001
SECRET_KEY=$DIFY_SECRET_KEY
INIT_PASSWORD=
DEPLOY_ENV=PRODUCTION
OPENAI_API_BASE=http://litellm-proxy:4000/v1
OPENAI_OPENAI_KEY=$LITELLM_AUTH_KEY
ENABLE_WEBSITE_JINAREADER=true
ENABLE_WEBSITE_FIRECRAWL=true
ENABLE_WEBSITE_WATERCRAWL=true
DB_TYPE=postgresql
DB_USERNAME=$DIFY_DATABASE_USER
DB_PASSWORD=$DIFY_DATABASE_USER_PASSWORD
DB_HOST=dify-db
DB_PORT=5432
DB_DATABASE=$DIFY_DATABASE_NAME
SQLALCHEMY_POOL_SIZE=30
SQLALCHEMY_MAX_OVERFLOW=10
SQLALCHEMY_POOL_RECYCLE=3600
SQLALCHEMY_ECHO=false
SQLALCHEMY_POOL_PRE_PING=false
SQLALCHEMY_POOL_USE_LIFO=false
SQLALCHEMY_POOL_TIMEOUT=30
REDIS_HOST=dify-redis
REDIS_PASSWORD=$DIFY_REDIS_PASSWORD
CELERY_BROKER_URL=redis://:$DIFY_REDIS_PASSWORD@dify-redis:6379/1
CELERY_BACKEND=redis
BROKER_USE_SSL=false
VECTOR_STORE=qdrant
QDRANT_URL=http://dify-vectordb:6333
QDRANT_API_KEY=$DIFY_QDRANT_API_KEY
MAIL_TYPE=smtp
MAIL_DEFAULT_SEND_FROM=Dify $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>
SMTP_SERVER=$SMTP_HOSTNAME
SMTP_PORT=$SMTP_HOSTPORT
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_USE_TLS=true
SMTP_OPPORTUNISTIC_TLS=true
SANDBOX_HOST=dify-sandbox
CODE_EXECUTION_ENDPOINT=http://dify-sandbox:8194
CODE_EXECUTION_API_KEY=$DIFY_CODE_EXECUTION_API_KEY
SSRF_PROXY_HTTP_URL=http://dify-ssrf:3128
SSRF_PROXY_HTTPS_URL=http://dify-ssrf:3128
SANDBOX_API_KEY=$DIFY_SANDBOX_API_KEY
SANDBOX_HTTP_PROXY=http://dify-ssrf:3128
SANDBOX_HTTPS_PROXY=http://dify-ssrf:3128
STORAGE_TYPE=local
MINIO_ROOT_USER=
MINIO_ROOT_PASSWORD=
MINIO_REGION=
S3_ENDPOINT=
S3_REGION=
S3_BUCKET_NAME=
S3_ACCESS_KEY=
S3_SECRET_KEY=
SSRF_SANDBOX_HOST=dify-sandbox
PLUGIN_DAEMON_KEY=$DIFY_PLUGIN_DAEMON_KEY
PLUGIN_DAEMON_URL=http://dify-plugind:5002
PLUGIN_DIFY_INNER_API_KEY=$DIFY_PLUGIN_DIFY_INNER_API_KEY
PLUGIN_DIFY_INNER_API_URL=http://dify-api:5001
INNER_API_KEY_FOR_PLUGIN=$DIFY_PLUGIN_DIFY_INNER_API_KEY
NEXT_TELEMETRY_DISABLED=1
NUMEXPR_MAX_THREADS=16
LANG=C.UTF-8
LC_ALL=C.UTF-8
PYTHONIOENCODING=utf-8
LOG_LEVEL=INFO
LOG_OUTPUT_FORMAT=text
LOG_FILE=/app/logs/server.log
LOG_FILE_MAX_SIZE=20
LOG_FILE_BACKUP_COUNT=5
LOG_DATEFORMAT=%Y-%m-%d %H:%M:%S
DEBUG=false
FLASK_DEBUG=false
ENABLE_REQUEST_LOGGING=False
CHECK_UPDATE_URL=https://updates.dify.ai
MIGRATION_ENABLED=true
FILES_ACCESS_TIMEOUT=300
ACCESS_TOKEN_EXPIRE_MINUTES=60
REFRESH_TOKEN_EXPIRE_DAYS=30
APP_DEFAULT_ACTIVE_REQUESTS=0
APP_MAX_ACTIVE_REQUESTS=0
APP_MAX_EXECUTION_TIME=1200
DIFY_BIND_ADDRESS=0.0.0.0
DIFY_PORT=5001
SERVER_WORKER_AMOUNT=1
SERVER_WORKER_CLASS=gevent
SERVER_WORKER_CONNECTIONS=10
GUNICORN_TIMEOUT=360
CELERY_AUTO_SCALE=false
API_TOOL_DEFAULT_CONNECT_TIMEOUT=10
API_TOOL_DEFAULT_READ_TIMEOUT=60
NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false
POSTGRES_MAX_CONNECTIONS=100
POSTGRES_SHARED_BUFFERS=128MB
POSTGRES_WORK_MEM=4MB
POSTGRES_MAINTENANCE_WORK_MEM=64MB
POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB
POSTGRES_STATEMENT_TIMEOUT=0
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT=0
REDIS_PORT=6379
REDIS_USE_SSL=false
REDIS_DB=0
REDIS_USE_SENTINEL=false
REDIS_USE_CLUSTERS=false
CELERY_USE_SENTINEL=false
NEXT_PUBLIC_BATCH_CONCURRENCY=5
S3_USE_AWS_MANAGED_IAM=false
ARCHIVE_STORAGE_ENABLED=false
VECTOR_INDEX_NAME_PREFIX=Vector_index
QDRANT_CLIENT_TIMEOUT=20
QDRANT_GRPC_ENABLED=false
QDRANT_GRPC_PORT=6334
QDRANT_REPLICATION_FACTOR=1
UPLOAD_FILE_SIZE_LIMIT=15
UPLOAD_FILE_BATCH_LIMIT=5
SINGLE_CHUNK_ATTACHMENT_LIMIT=10
IMAGE_FILE_BATCH_LIMIT=10
ATTACHMENT_IMAGE_FILE_SIZE_LIMIT=2
ATTACHMENT_IMAGE_DOWNLOAD_TIMEOUT=60
ETL_TYPE=dify
SCARF_NO_ANALYTICS=true
PROMPT_GENERATION_MAX_TOKENS=512
CODE_GENERATION_MAX_TOKENS=1024
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
MULTIMODAL_SEND_FORMAT=base64
UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
PLUGIN_SENTRY_ENABLED=false
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
INVITE_EXPIRY_HOURS=72
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
CODE_EXECUTION_SSL_VERIFY=True
CODE_EXECUTION_POOL_MAX_CONNECTIONS=100
CODE_EXECUTION_POOL_MAX_KEEPALIVE_CONNECTIONS=20
CODE_EXECUTION_POOL_KEEPALIVE_EXPIRY=5.0
CODE_MAX_NUMBER=9223372036854775807
CODE_MIN_NUMBER=-9223372036854775808
CODE_MAX_DEPTH=5
CODE_MAX_PRECISION=20
CODE_MAX_STRING_LENGTH=400000
CODE_MAX_STRING_ARRAY_LENGTH=30
CODE_MAX_OBJECT_ARRAY_LENGTH=30
CODE_MAX_NUMBER_ARRAY_LENGTH=1000
CODE_EXECUTION_CONNECT_TIMEOUT=10
CODE_EXECUTION_READ_TIMEOUT=60
CODE_EXECUTION_WRITE_TIMEOUT=10
TEMPLATE_TRANSFORM_MAX_LENGTH=400000
WORKFLOW_MAX_EXECUTION_STEPS=500
WORKFLOW_MAX_EXECUTION_TIME=1200
WORKFLOW_CALL_MAX_DEPTH=5
MAX_VARIABLE_SIZE=204800
WORKFLOW_FILE_UPLOAD_LIMIT=10
GRAPH_ENGINE_MIN_WORKERS=1
GRAPH_ENGINE_MAX_WORKERS=10
GRAPH_ENGINE_SCALE_UP_THRESHOLD=3
GRAPH_ENGINE_SCALE_DOWN_IDLE_TIME=5.0
WORKFLOW_NODE_EXECUTION_STORAGE=rdbms
CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository
CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository
API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository
API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository
WORKFLOW_LOG_CLEANUP_ENABLED=false
WORKFLOW_LOG_RETENTION_DAYS=30
WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100
LOGSTORE_DUAL_WRITE_ENABLED=false
LOGSTORE_DUAL_READ_ENABLED=true
LOGSTORE_ENABLE_PUT_GRAPH_FIELD=true
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
HTTP_REQUEST_NODE_SSL_VERIFY=True
HTTP_REQUEST_MAX_CONNECT_TIMEOUT=10
HTTP_REQUEST_MAX_READ_TIMEOUT=600
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
WEBHOOK_REQUEST_BODY_MAX_SIZE=10485760
RESPECT_XFORWARD_HEADERS_ENABLED=false
LOOP_NODE_MAX_COUNT=100
MAX_TOOLS_NUM=10
MAX_PARALLEL_LIMIT=10
MAX_ITERATIONS_NUM=99
TEXT_GENERATION_TIMEOUT_MS=60000
ALLOW_UNSAFE_DATA_SCHEME=false
MAX_TREE_DEPTH=50
PGDATA=/var/lib/postgresql/data/pgdata
SANDBOX_GIN_MODE=release
SANDBOX_WORKER_TIMEOUT=15
SANDBOX_ENABLE_NETWORK=true
SANDBOX_PORT=8194
SSRF_HTTP_PORT=3128
SSRF_COREDUMP_DIR=/var/spool/squid
SSRF_REVERSE_PROXY_PORT=8194
SSRF_DEFAULT_TIME_OUT=5
SSRF_DEFAULT_CONNECT_TIME_OUT=5
SSRF_DEFAULT_READ_TIME_OUT=5
SSRF_DEFAULT_WRITE_TIME_OUT=5
SSRF_POOL_MAX_CONNECTIONS=100
SSRF_POOL_MAX_KEEPALIVE_CONNECTIONS=20
SSRF_POOL_KEEPALIVE_EXPIRY=5.0
CREATE_TIDB_SERVICE_JOB_ENABLED=false
MAX_SUBMIT_COUNT=100
TOP_K_MAX_VALUE=10
EXPOSE_PLUGIN_DAEMON_PORT=5002
PLUGIN_DAEMON_PORT=5002
PLUGIN_MAX_PACKAGE_SIZE=52428800
PLUGIN_PPROF_ENABLED=false
PLUGIN_DEBUGGING_HOST=0.0.0.0
PLUGIN_DEBUGGING_PORT=5003
EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
ENDPOINT_URL_TEMPLATE=http://localhost/e/{hook_id}
MARKETPLACE_ENABLED=true
MARKETPLACE_API_URL=https://marketplace.dify.ai
FORCE_VERIFYING_SIGNATURE=true
ENFORCE_LANGGENIUS_PLUGIN_SIGNATURES=true
PLUGIN_STDIO_BUFFER_SIZE=1024
PLUGIN_STDIO_MAX_BUFFER_SIZE=5242880
PLUGIN_PYTHON_ENV_INIT_TIMEOUT=120
PLUGIN_MAX_EXECUTION_TIMEOUT=600
PLUGIN_DAEMON_TIMEOUT=600.0
PIP_MIRROR_URL=
PLUGIN_STORAGE_TYPE=local
PLUGIN_STORAGE_LOCAL_ROOT=/app/storage
PLUGIN_WORKING_PATH=/app/storage/cwd
PLUGIN_INSTALLED_PATH=plugin
PLUGIN_PACKAGE_CACHE_PATH=plugin_packages
PLUGIN_MEDIA_CACHE_PATH=assets
PLUGIN_STORAGE_OSS_BUCKET=
PLUGIN_S3_USE_AWS=false
PLUGIN_S3_USE_AWS_MANAGED_IAM=false
PLUGIN_S3_USE_PATH_STYLE=true
ENABLE_OTEL=false
ALLOW_EMBED=false
QUEUE_MONITOR_THRESHOLD=200
QUEUE_MONITOR_ALERT_EMAILS=
QUEUE_MONITOR_INTERVAL=30
SWAGGER_UI_ENABLED=false
SWAGGER_UI_PATH=/swagger-ui.html
DSL_EXPORT_ENCRYPT_DATASET_ID=true
DATASET_MAX_SEGMENTS_PER_REQUEST=0
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false
ENABLE_CLEAN_UNUSED_DATASETS_TASK=false
ENABLE_CREATE_TIDB_SERVERLESS_TASK=false
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false
ENABLE_CLEAN_MESSAGES=false
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false
ENABLE_DATASETS_QUEUE_MONITOR=false
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true
ENABLE_WORKFLOW_SCHEDULE_POLLER_TASK=true
WORKFLOW_SCHEDULE_POLLER_INTERVAL=1
WORKFLOW_SCHEDULE_POLLER_BATCH_SIZE=100
WORKFLOW_SCHEDULE_MAX_DISPATCH_PER_TICK=0
TENANT_ISOLATED_TASK_CONCURRENCY=1
ANNOTATION_IMPORT_FILE_SIZE_LIMIT=2
ANNOTATION_IMPORT_MAX_RECORDS=10000
ANNOTATION_IMPORT_MIN_RECORDS=1
ANNOTATION_IMPORT_RATE_LIMIT_PER_MINUTE=5
ANNOTATION_IMPORT_RATE_LIMIT_PER_HOUR=20
ANNOTATION_IMPORT_MAX_CONCURRENT=5
AMPLITUDE_API_KEY=
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
NGINX_SERVER_NAME=_
NGINX_HTTPS_ENABLED=false
NGINX_PORT=80
NGINX_WORKER_PROCESSES=auto
NGINX_CLIENT_MAX_BODY_SIZE=100M
NGINX_KEEPALIVE_TIMEOUT=65
NGINX_PROXY_READ_TIMEOUT=3600s
NGINX_PROXY_SEND_TIMEOUT=3600s
NGINX_ENABLE_CERTBOT_CHALLENGE=false
EOFMT
  cat < $HSHQ_STACKS_DIR/dify/nginx/https.conf.template
listen \${NGINX_SSL_PORT} ssl;
ssl_certificate \${SSL_CERTIFICATE_PATH};
ssl_certificate_key \${SSL_CERTIFICATE_KEY_PATH};
ssl_protocols \${NGINX_SSL_PROTOCOLS};
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
EOFSE
  cat < $HSHQ_STACKS_DIR/dify/nginx/nginx.conf.template
# Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration.

user  nginx;
worker_processes  \${NGINX_WORKER_PROCESSES};

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '\$remote_addr - \$remote_user [\$time_local] "\$request" '
                      '\$status \$body_bytes_sent "\$http_referer" '
                      '"\$http_user_agent" "\$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  \${NGINX_KEEPALIVE_TIMEOUT};

    #gzip  on;
    client_max_body_size \${NGINX_CLIENT_MAX_BODY_SIZE};

    include /etc/nginx/conf.d/*.conf;
}
EOFSE
  cat < $HSHQ_STACKS_DIR/dify/nginx/proxy.conf.template
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Port \$server_port;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout \${NGINX_PROXY_READ_TIMEOUT};
proxy_send_timeout \${NGINX_PROXY_SEND_TIMEOUT};
EOFSE
  cat < $HSHQ_STACKS_DIR/dify/nginx/conf.d/default.conf.template
# Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration.

resolver 127.0.0.11 valid=10s;
upstream backend {
    server dify-api:5001;
}
upstream frontend {
    server dify-web:3000;
}
upstream plugind {
    server dify-plugind:5002;
}

server {
    listen \${NGINX_PORT};
    server_name \${NGINX_SERVER_NAME};

    location /console/api {
      proxy_pass http://backend;
      include proxy.conf;
    }

    location /api {
      proxy_pass http://backend;
      include proxy.conf;
    }

    location /v1 {
      proxy_pass http://backend;
      include proxy.conf;
    }

    location /files {
      proxy_pass http://backend;
      include proxy.conf;
    }

    location /explore {
      proxy_pass http://frontend;
      include proxy.conf;
    }

    location /e/ {
      proxy_pass http://plugind;
      proxy_set_header Dify-Hook-Url \$scheme://\$host\$request_uri;
      include proxy.conf;
    }

    location / {
      proxy_pass http://frontend;
      include proxy.conf;
    }

    location /mcp {
      proxy_pass http://backend;
      include proxy.conf;
    }

    location /triggers {
      proxy_pass http://backend;
      include proxy.conf;
    }
    
    # placeholder for acme challenge location
    \${ACME_CHALLENGE_LOCATION}

    # placeholder for https config defined in https.conf.template
    \${HTTPS_CONFIG}
}
EOFSE
  cat < $HSHQ_STACKS_DIR/dify/nginx/docker-entrypoint.sh
#!/bin/bash

HTTPS_CONFIG=''

if [ "\${NGINX_HTTPS_ENABLED}" = "true" ]; then
    # Check if the certificate and key files for the specified domain exist
    if [ -n "\${CERTBOT_DOMAIN}" ] && \
       [ -f "/etc/letsencrypt/live/\${CERTBOT_DOMAIN}/\${NGINX_SSL_CERT_FILENAME}" ] && \
       [ -f "/etc/letsencrypt/live/\${CERTBOT_DOMAIN}/\${NGINX_SSL_CERT_KEY_FILENAME}" ]; then
        SSL_CERTIFICATE_PATH="/etc/letsencrypt/live/\${CERTBOT_DOMAIN}/\${NGINX_SSL_CERT_FILENAME}"
        SSL_CERTIFICATE_KEY_PATH="/etc/letsencrypt/live/\${CERTBOT_DOMAIN}/\${NGINX_SSL_CERT_KEY_FILENAME}"
    else
        SSL_CERTIFICATE_PATH="/etc/ssl/\${NGINX_SSL_CERT_FILENAME}"
        SSL_CERTIFICATE_KEY_PATH="/etc/ssl/\${NGINX_SSL_CERT_KEY_FILENAME}"
    fi
    export SSL_CERTIFICATE_PATH
    export SSL_CERTIFICATE_KEY_PATH

    # set the HTTPS_CONFIG environment variable to the content of the https.conf.template
    HTTPS_CONFIG=\$(envsubst < /etc/nginx/https.conf.template)
    export HTTPS_CONFIG
    # Substitute the HTTPS_CONFIG in the default.conf.template with content from https.conf.template
    envsubst '\${HTTPS_CONFIG}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
fi
export HTTPS_CONFIG

if [ "\${NGINX_ENABLE_CERTBOT_CHALLENGE}" = "true" ]; then
    ACME_CHALLENGE_LOCATION='location /.well-known/acme-challenge/ { root /var/www/html; }'
else
    ACME_CHALLENGE_LOCATION=''
fi
export ACME_CHALLENGE_LOCATION

env_vars=\$(printenv | cut -d= -f1 | sed 's/^/\$/g' | paste -sd, -)

envsubst "\$env_vars" < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
envsubst "\$env_vars" < /etc/nginx/proxy.conf.template > /etc/nginx/proxy.conf

envsubst "\$env_vars" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf

# Start Nginx using the default entrypoint
exec nginx -g 'daemon off;'
EOFSE
  chmod +x $HSHQ_STACKS_DIR/dify/nginx/docker-entrypoint.sh
  cat < $HSHQ_STACKS_DIR/dify/config/squid.conf.template
acl localnet src 0.0.0.1-0.255.255.255	# RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8		# RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10		# RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16 	# RFC 3927 link-local (directly plugged) machines
acl localnet src 172.16.0.0/12		# RFC 1918 local private network (LAN)
acl localnet src 192.168.0.0/16		# RFC 1918 local private network (LAN)
acl localnet src fc00::/7       	# RFC 4193 local private network range
acl localnet src fe80::/10      	# RFC 4291 link-local (directly plugged) machines
acl SSL_ports port 443
# acl SSL_ports port 1025-65535   # Enable the configuration to resolve this issue: https://github.com/langgenius/dify/issues/12792
acl Safe_ports port 80		# http
acl Safe_ports port 21		# ftp
acl Safe_ports port 443		# https
acl Safe_ports port 70		# gopher
acl Safe_ports port 210		# wais
acl Safe_ports port 1025-65535	# unregistered ports
acl Safe_ports port 280		# http-mgmt
acl Safe_ports port 488		# gss-http
acl Safe_ports port 591		# filemaker
acl Safe_ports port 777		# multiling http
acl CONNECT method CONNECT
acl allowed_domains dstdomain .marketplace.dify.ai
http_access allow allowed_domains
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localhost
include /etc/squid/conf.d/*.conf
http_access deny all

################################## Proxy Server ################################
http_port \${HTTP_PORT}
coredump_dir \${COREDUMP_DIR}
refresh_pattern ^ftp:		1440	20%	10080
refresh_pattern ^gopher:	1440	0%	1440
refresh_pattern -i (/cgi-bin/|\?) 0	0%	0
refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)\$ 0 0% 0 refresh-ims
refresh_pattern \/Release(|\.gpg)\$ 0 0% 0 refresh-ims
refresh_pattern \/InRelease\$ 0 0% 0 refresh-ims
refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)\$ 0 0% 0 refresh-ims
refresh_pattern .		0	20%	4320


# cache_dir ufs /var/spool/squid 100 16 256
# upstream proxy, set to your own upstream proxy IP to avoid SSRF attacks
# cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default

################################## Reverse Proxy To Sandbox ################################
http_port \${REVERSE_PROXY_PORT} accel vhost
cache_peer \${SANDBOX_HOST} parent \${SANDBOX_PORT} 0 no-query originserver
acl src_all src all
http_access allow src_all

# Unless the option's size is increased, an error will occur when uploading more than two files.
client_request_buffer_max_size 100 MB

################################## Performance & Concurrency ###############################
# Increase file descriptor limit for high concurrency
max_filedescriptors 65536

# Timeout configurations for image requests
connect_timeout 30 seconds
request_timeout 2 minutes
read_timeout 2 minutes
client_lifetime 5 minutes
shutdown_lifetime 30 seconds

# Persistent connections - improve performance for multiple requests
server_persistent_connections on
client_persistent_connections on
persistent_request_timeout 30 seconds
pconn_timeout 1 minute

# Connection pool and concurrency limits
client_db on
server_idle_pconn_timeout 2 minutes
client_idle_pconn_timeout 2 minutes

# Quick abort settings - don't abort requests that are mostly done
quick_abort_min 16 KB
quick_abort_max 16 MB
quick_abort_pct 95

# Memory and cache optimization
memory_cache_mode disk
cache_mem 256 MB
maximum_object_size_in_memory 512 KB

# DNS resolver settings for better performance
dns_timeout 30 seconds
dns_retransmit_interval 5 seconds
# By default, Squid uses the system's configured DNS resolvers.
# If you need to override them, set dns_nameservers to appropriate servers
# for your environment (for example, internal/corporate DNS). The following
# is an example using public DNS and SHOULD be customized before use:
# dns_nameservers 8.8.8.8 8.8.4.4

# Logging format for better debugging
logformat dify_log %ts.%03tu %6tr %>a %Ss/%03>Hs % $HSHQ_STACKS_DIR/dify/config/docker-entrypoint.sh
#!/bin/bash

# Modified based on Squid OCI image entrypoint

# This entrypoint aims to forward the squid logs to stdout to assist users of
# common container related tooling (e.g., kubernetes, docker-compose, etc) to
# access the service logs.

# Moreover, it invokes the squid binary, leaving all the desired parameters to
# be provided by the "command" passed to the spawned container. If no command
# is provided by the user, the default behavior (as per the CMD statement in
# the Dockerfile) will be to use Ubuntu's default configuration [1] and run
# squid with the "-NYC" options to mimic the behavior of the Ubuntu provided
# systemd unit.

# [1] The default configuration is changed in the Dockerfile to allow local
# network connections. See the Dockerfile for further information.

echo "[ENTRYPOINT] re-create snakeoil self-signed certificate removed in the build process"
if [ ! -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then
    /usr/sbin/make-ssl-cert generate-default-snakeoil --force-overwrite > /dev/null 2>&1
fi

tail -F /var/log/squid/access.log 2>/dev/null &
tail -F /var/log/squid/error.log 2>/dev/null &
tail -F /var/log/squid/store.log 2>/dev/null &
tail -F /var/log/squid/cache.log 2>/dev/null &

# Replace environment variables in the template and output to the squid.conf
echo "[ENTRYPOINT] replacing environment variables in the template"
awk '{
    while(match(\$0, /\\\${[A-Za-z_][A-Za-z_0-9]*}/)) {
        var = substr(\$0, RSTART+2, RLENGTH-3)
        val = ENVIRON[var]
        \$0 = substr(\$0, 1, RSTART-1) val substr(\$0, RSTART+RLENGTH)
    }
    print
}' /etc/squid/squid.conf.template > /etc/squid/squid.conf

/usr/sbin/squid -Nz
echo "[ENTRYPOINT] starting squid"
/usr/sbin/squid -f /etc/squid/squid.conf -NYC 1
EOFSE
  cat < $HSHQ_STACKS_DIR/dify/sandbox/conf/config.yaml
app:
  port: 8194
  debug: True
  key: dify-sandbox
max_workers: 4
max_requests: 50
worker_timeout: 5
python_path: /usr/local/bin/python3
enable_network: True
allowed_syscalls:
proxy:
  socks5: ''
  http: ''
  https: ''
EOFSE
  cat < $HOME/genhash.py
import base64
import binascii
import hashlib
import re
import secrets

password_pattern = r"^(?=.*[a-zA-Z])(?=.*\d).{8,}\$"


def valid_password(password):
    # Define a regex pattern for password rules
    pattern = password_pattern
    # Check if the password matches the pattern
    if re.match(pattern, password) is not None:
        return password

    raise ValueError("Password must contain letters and numbers, and the length must be greater than 8.")

def hash_pass(password_str):
    salt = secrets.token_bytes(16)
    base64_salt = base64.b64encode(salt).decode()
    password_hashed = hash_password(password_str, salt)
    base64_password_hashed = base64.b64encode(password_hashed).decode()
    print(base64_salt + "," + base64_password_hashed)

def hash_password(password_str, salt_byte):
    dk = hashlib.pbkdf2_hmac("sha256", password_str.encode("utf-8"), salt_byte, 10000)
    return binascii.hexlify(dk)


def compare_password(password_str, password_hashed_base64, salt_base64):
    # compare password for login
    return hash_password(password_str, base64.b64decode(salt_base64)) == base64.b64decode(password_hashed_base64)

hash_pass("$DIFY_ADMIN_PASSWORD")
EOFSQ
  pwhash=$(python3 $HOME/genhash.py)
  DIFY_ADMIN_PASSWORD_SALT=$(echo "$pwhash" | cut -d"," -f1)
  DIFY_ADMIN_PASSWORD_HASH=$(echo "$pwhash" | cut -d"," -f2)
  rm -f $HOME/genhash.py
  acctuuid=$(uuidgen)
  tenantuuid=$(uuidgen)
  taj_uuid=$(uuidgen)
  tpaus_uuid=$(uuidgen)
  dtnow=$(date '+%Y-%m-%d %H:%M:%S.%6N')
  dtnow2=$(date '+%Y-%m-%d %H:%M:%S')
  mkdir $HSHQ_STACKS_DIR/dify/storage/privkeys/$tenantuuid
  openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out $HSHQ_STACKS_DIR/dify/storage/privkeys/$tenantuuid/private.pem > /dev/null 2>&1
  chmod 644 $HSHQ_STACKS_DIR/dify/storage/privkeys/$tenantuuid/private.pem
  pubkey=$(openssl pkey -in $HSHQ_STACKS_DIR/dify/storage/privkeys/$tenantuuid/private.pem -pubout)
  cat < $HSHQ_STACKS_DIR/dify/dbexport/setupDBSettings.sh
#!/bin/bash

PGPASSWORD=$DIFY_DATABASE_USER_PASSWORD

echo "insert into accounts(id, name, email, password, password_salt, avatar, interface_language, interface_theme, timezone, last_login_at, last_login_ip, status, initialized_at, created_at, updated_at, last_active_at) values('$acctuuid','$DIFY_ADMIN_USERNAME','$DIFY_ADMIN_EMAIL_ADDRESS','$DIFY_ADMIN_PASSWORD_HASH','$DIFY_ADMIN_PASSWORD_SALT',NULL,'en-US','dark','$TZ',NULL,NULL,'active','$dtnow','$dtnow2','$dtnow','$dtnow2');" | psql -U $DIFY_DATABASE_USER $DIFY_DATABASE_NAME

echo "insert into dify_setups(version, setup_at) values('1.11.2','$dtnow2');" | psql -U $DIFY_DATABASE_USER $DIFY_DATABASE_NAME

echo "insert into tenants(id, name, encrypt_public_key, plan, status, created_at, updated_at, custom_config) values('$tenantuuid','${DIFY_ADMIN_USERNAME}''s Workspace','$pubkey','basic','normal','$dtnow2','$dtnow',NULL);" | psql -U $DIFY_DATABASE_USER $DIFY_DATABASE_NAME

echo "insert into tenant_account_joins(id, tenant_id, account_id, role, invited_by, created_at, updated_at, current) values('$taj_uuid','$tenantuuid','$acctuuid','owner',NULL,'$dtnow2','$dtnow2','f');" | psql -U $DIFY_DATABASE_USER $DIFY_DATABASE_NAME

echo "insert into tenant_plugin_auto_upgrade_strategies(id, tenant_id, strategy_setting, upgrade_time_of_day, upgrade_mode, exclude_plugins, include_plugins, created_at, updated_at) values('$tpaus_uuid','$tenantuuid','fix_only',0,'exclude','[]'::json,'[]'::json,'$dtnow','$dtnow');" | psql -U $DIFY_DATABASE_USER $DIFY_DATABASE_NAME

EOFDS
  chmod +x $HSHQ_STACKS_DIR/dify/dbexport/setupDBSettings.sh
  sudo chown -R 1001:1001 $HSHQ_STACKS_DIR/dify/storage
}

function performUpdateDify()
{
  perform_stack_name=dify
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/langgenius/qdrant:v1.8.3,mirror.gcr.io/ubuntu/squid:latest,mirror.gcr.io/langgenius/dify-api:1.11.2,mirror.gcr.io/langgenius/dify-plugin-daemon:0.5.2-local,mirror.gcr.io/langgenius/dify-sandbox:0.2.12,mirror.gcr.io/langgenius/dify-web:1.11.2,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/nginx:1.29.4-trixie,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
      image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm"
      image_update_map[1]="mirror.gcr.io/langgenius/qdrant:v1.8.3,mirror.gcr.io/langgenius/qdrant:v1.8.3"
      image_update_map[2]="mirror.gcr.io/ubuntu/squid:latest,mirror.gcr.io/ubuntu/squid:latest"
      image_update_map[3]="mirror.gcr.io/langgenius/dify-api:1.11.2,mirror.gcr.io/langgenius/dify-api:1.11.2"
      image_update_map[4]="mirror.gcr.io/langgenius/dify-plugin-daemon:0.5.2-local,mirror.gcr.io/langgenius/dify-plugin-daemon:0.5.2-local"
      image_update_map[5]="mirror.gcr.io/langgenius/dify-sandbox:0.2.12,mirror.gcr.io/langgenius/dify-sandbox:0.2.12"
      image_update_map[6]="mirror.gcr.io/langgenius/dify-web:1.11.2,mirror.gcr.io/langgenius/dify-web:1.11.2"
      image_update_map[7]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[8]="mirror.gcr.io/nginx:1.29.4-trixie,mirror.gcr.io/nginx:1.29.4-trixie"
      image_update_map[9]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    2)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/langgenius/qdrant:v1.8.3,mirror.gcr.io/ubuntu/squid:latest,mirror.gcr.io/langgenius/dify-api:1.11.2,mirror.gcr.io/langgenius/dify-plugin-daemon:0.5.2-local,mirror.gcr.io/langgenius/dify-sandbox:0.2.12,mirror.gcr.io/langgenius/dify-web:1.11.2,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/nginx:1.29.4-trixie,ghcr.io/homeserverhq/minio:v1.0
      image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm"
      image_update_map[1]="mirror.gcr.io/langgenius/qdrant:v1.8.3,mirror.gcr.io/langgenius/qdrant:v1.8.3"
      image_update_map[2]="mirror.gcr.io/ubuntu/squid:latest,mirror.gcr.io/ubuntu/squid:latest"
      image_update_map[3]="mirror.gcr.io/langgenius/dify-api:1.11.2,mirror.gcr.io/langgenius/dify-api:1.11.2"
      image_update_map[4]="mirror.gcr.io/langgenius/dify-plugin-daemon:0.5.2-local,mirror.gcr.io/langgenius/dify-plugin-daemon:0.5.2-local"
      image_update_map[5]="mirror.gcr.io/langgenius/dify-sandbox:0.2.12,mirror.gcr.io/langgenius/dify-sandbox:0.2.12"
      image_update_map[6]="mirror.gcr.io/langgenius/dify-web:1.11.2,mirror.gcr.io/langgenius/dify-web:1.11.2"
      image_update_map[7]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[8]="mirror.gcr.io/nginx:1.29.4-trixie,mirror.gcr.io/nginx:1.29.4-trixie"
      image_update_map[9]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

# MindsDB
function installMindsDB()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory mindsdb "MindsDB"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName mindsdb-db)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName mindsdb-app)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName mindsdb-redis)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/mindsdb
  mkdir $HSHQ_STACKS_DIR/mindsdb/data
  mkdir $HSHQ_STACKS_DIR/mindsdb/db
  mkdir $HSHQ_STACKS_DIR/mindsdb/dbimport
  mkdir $HSHQ_STACKS_DIR/mindsdb/dbexport
  chmod 777 $HSHQ_STACKS_DIR/mindsdb/dbexport
  initServicesCredentials
  set +e
  addUserMailu alias $MINDSDB_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS
  outputConfigMindsDB
  installStack mindsdb mindsdb-app "mindsdb: http API: started on 47334" $HOME/mindsdb.env 3
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$MINDSDB_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_MINDSDB_APP Admin Login Info" -b "$FMLNAME_MINDSDB_APP Admin Username: $MINDSDB_ADMIN_USERNAME\n$FMLNAME_MINDSDB_APP Admin Password: $MINDSDB_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    MINDSDB_INIT_ENV=true
    updateConfigVar MINDSDB_INIT_ENV $MINDSDB_INIT_ENV
  fi
  wizard_notes=$(cat <"
  sleep 3
  addReadOnlyUserToDatabase MindsDB postgres mindsdb-db $MINDSDB_DATABASE_NAME $MINDSDB_DATABASE_USER $MINDSDB_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/mindsdb/dbexport $MINDSDB_DATABASE_READONLYUSER $MINDSDB_DATABASE_READONLYUSER_PASSWORD
  if [ -z "$FMLNAME_MINDSDB_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_MINDSDB_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://mindsdb-app:47334 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_MINDSDB_APP $MANAGETLS_MINDSDB_APP "$is_integrate_hshq" $NETDEFAULT_MINDSDB_APP "$inner_block"
  insertSubAuthelia $SUB_MINDSDB_APP.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll mindsdb "$FMLNAME_MINDSDB_APP" $USERTYPE_MINDSDB_APP "https://$SUB_MINDSDB_APP.$HOMESERVER_DOMAIN" "mindsdb.png" "$(getHeimdallOrderFromSub $SUB_MINDSDB_APP $USERTYPE_MINDSDB_APP)"
    restartAllCaddyContainers
    checkAddDBConnection true mindsdb "$FMLNAME_MINDSDB_APP" postgres mindsdb-db $MINDSDB_DATABASE_NAME $MINDSDB_DATABASE_USER $MINDSDB_DATABASE_USER_PASSWORD
  fi
}

function outputDBsList()
{
  local outputFilename="$1"
  cat < $outputFilename
"ActivePieces" postgres activepieces-db $ACTIVEPIECES_DATABASE_NAME $ACTIVEPIECES_DATABASE_READONLYUSER $ACTIVEPIECES_DATABASE_READONLYUSER_PASSWORD
"Adminer" mysql adminer-db $ADMINER_DATABASE_NAME $ADMINER_DATABASE_READONLYUSER $ADMINER_DATABASE_READONLYUSER_PASSWORD
"Automatisch" postgres automatisch-db $AUTOMATISCH_DATABASE_NAME $AUTOMATISCH_DATABASE_READONLYUSER $AUTOMATISCH_DATABASE_READONLYUSER_PASSWORD
"Budibase" mysql budibase-db $BUDIBASE_DATABASE_NAME $BUDIBASE_DATABASE_READONLYUSER $BUDIBASE_DATABASE_READONLYUSER_PASSWORD
"Calcom" postgres calcom-db $CALCOM_DATABASE_NAME $CALCOM_DATABASE_READONLYUSER $CALCOM_DATABASE_READONLYUSER_PASSWORD
"Discourse" postgres discourse-db $DISCOURSE_DATABASE_NAME $DISCOURSE_DATABASE_READONLYUSER $DISCOURSE_DATABASE_READONLYUSER_PASSWORD
"Dolibarr" mysql dolibarr-db $DOLIBARR_DATABASE_NAME $DOLIBARR_DATABASE_READONLYUSER $DOLIBARR_DATABASE_READONLYUSER_PASSWORD
"EasyAppointments" mysql easyappointments-db $EASYAPPOINTMENTS_DATABASE_NAME $EASYAPPOINTMENTS_DATABASE_READONLYUSER $EASYAPPOINTMENTS_DATABASE_READONLYUSER_PASSWORD
"EspoCRM" mysql espocrm-db $ESPOCRM_DATABASE_NAME $ESPOCRM_DATABASE_READONLYUSER $ESPOCRM_DATABASE_READONLYUSER_PASSWORD
"FireflyIII" postgres firefly-db $FIREFLY_DATABASE_NAME $FIREFLY_DATABASE_READONLYUSER $FIREFLY_DATABASE_READONLYUSER_PASSWORD
"FrappeHR" mysql frappe-hr-db $FRAPPE_HR_DATABASE_NAME $FRAPPE_HR_DATABASE_READONLYUSER $FRAPPE_HR_DATABASE_READONLYUSER_PASSWORD
"FreshRSS" postgres freshrss-db $FRESHRSS_DATABASE_NAME $FRESHRSS_DATABASE_READONLYUSER $FRESHRSS_DATABASE_READONLYUSER_PASSWORD
"Ghost" mysql ghost-db $GHOST_DATABASE_NAME $GHOST_DATABASE_READONLYUSER $GHOST_DATABASE_READONLYUSER_PASSWORD
"Gitea" postgres gitea-db $GITEA_DATABASE_NAME $GITEA_DATABASE_READONLYUSER $GITEA_DATABASE_READONLYUSER_PASSWORD
"Gitlab" postgres gitlab-db $GITLAB_DATABASE_NAME $GITLAB_DATABASE_READONLYUSER $GITLAB_DATABASE_READONLYUSER_PASSWORD
"Guacamole" mysql guacamole-db $GUACAMOLE_DATABASE_NAME $GUACAMOLE_DATABASE_READONLYUSER $GUACAMOLE_DATABASE_READONLYUSER_PASSWORD
"HomeAssistant" postgres homeassistant-db $HOMEASSISTANT_DATABASE_NAME $HOMEASSISTANT_DATABASE_READONLYUSER $HOMEASSISTANT_DATABASE_READONLYUSER_PASSWORD
"Huginn" postgres huginn-db $HUGINN_DATABASE_NAME $HUGINN_DATABASE_READONLYUSER $HUGINN_DATABASE_READONLYUSER_PASSWORD
"Immich" postgres immich-db $IMMICH_DATABASE_NAME $IMMICH_DATABASE_READONLYUSER $IMMICH_DATABASE_READONLYUSER_PASSWORD
"Invidious" postgres invidious-db $INVIDIOUS_DATABASE_NAME $INVIDIOUS_DATABASE_READONLYUSER $INVIDIOUS_DATABASE_READONLYUSER_PASSWORD
"InvoiceNinja" mysql invoiceninja-db $INVOICENINJA_DATABASE_NAME $INVOICENINJA_DATABASE_READONLYUSER $INVOICENINJA_DATABASE_READONLYUSER_PASSWORD
"InvoiceShelf" postgres invoiceshelf-db $INVOICESHELF_DATABASE_NAME $INVOICESHELF_DATABASE_READONLYUSER $INVOICESHELF_DATABASE_READONLYUSER_PASSWORD
"Kanboard" postgres kanboard-db $KANBOARD_DATABASE_NAME $KANBOARD_DATABASE_READONLYUSER $KANBOARD_DATABASE_READONLYUSER_PASSWORD
"Keila" postgres keila-db $KEILA_DATABASE_NAME $KEILA_DATABASE_READONLYUSER $KEILA_DATABASE_READONLYUSER_PASSWORD
"KillBillAPI" mysql killbill-db $KILLBILL_KB_DATABASE_NAME $KILLBILL_KB_DATABASE_READONLYUSER $KILLBILL_KB_DATABASE_READONLYUSER_PASSWORD
"KillBill" mysql killbill-db $KILLBILL_KAUI_DATABASE_NAME $KILLBILL_KAUI_DATABASE_READONLYUSER $KILLBILL_KAUI_DATABASE_READONLYUSER_PASSWORD
"Linkwarden" postgres linkwarden-db $LINKWARDEN_DATABASE_NAME $LINKWARDEN_DATABASE_READONLYUSER $LINKWARDEN_DATABASE_READONLYUSER_PASSWORD
"Mastodon" postgres mastodon-db $MASTODON_DATABASE_NAME $MASTODON_DATABASE_READONLYUSER $MASTODON_DATABASE_READONLYUSER_PASSWORD
"Matomo" mysql matomo-db $MATOMO_DATABASE_NAME $MATOMO_DATABASE_READONLYUSER $MATOMO_DATABASE_READONLYUSER_PASSWORD
"Matrix" postgres matrix-db $MATRIX_DATABASE_NAME $MATRIX_DATABASE_READONLYUSER $MATRIX_DATABASE_READONLYUSER_PASSWORD
"Mealie" postgres mealie-db $MEALIE_DATABASE_NAME $MEALIE_DATABASE_READONLYUSER $MEALIE_DATABASE_READONLYUSER_PASSWORD
"MeshCentral" mysql meshcentral-db $MESHCENTRAL_DATABASE_NAME $MESHCENTRAL_DATABASE_READONLYUSER $MESHCENTRAL_DATABASE_READONLYUSER_PASSWORD
"Metabase" postgres metabase-db $METABASE_DATABASE_NAME $METABASE_DATABASE_READONLYUSER $METABASE_DATABASE_READONLYUSER_PASSWORD
"MintHCM" mysql minthcm-db $MINTHCM_DATABASE_NAME $MINTHCM_DATABASE_READONLYUSER $MINTHCM_DATABASE_READONLYUSER_PASSWORD
"n8n" postgres n8n-db $N8N_DATABASE_NAME $N8N_DATABASE_READONLYUSER $N8N_DATABASE_READONLYUSER_PASSWORD
"Nextcloud" postgres nextcloud-db $NEXTCLOUD_DATABASE_NAME $NEXTCLOUD_DATABASE_READONLYUSER $NEXTCLOUD_DATABASE_READONLYUSER_PASSWORD
"Odoo" postgres odoo-db $ODOO_DATABASE_NAME $ODOO_DATABASE_READONLYUSER $ODOO_DATABASE_READONLYUSER_PASSWORD
"Ombi" mysql ombi-db $OMBI_DATABASE_NAME $OMBI_DATABASE_READONLYUSER $OMBI_DATABASE_READONLYUSER_PASSWORD
"OpenProject" postgres openproject-db $OPENPROJECT_DATABASE_NAME $OPENPROJECT_DATABASE_READONLYUSER $OPENPROJECT_DATABASE_READONLYUSER_PASSWORD
"Paperless" postgres paperless-db $PAPERLESS_DATABASE_NAME $PAPERLESS_DATABASE_READONLYUSER $PAPERLESS_DATABASE_READONLYUSER_PASSWORD
"Pastefy" mysql pastefy-db $PASTEFY_DATABASE_NAME $PASTEFY_DATABASE_READONLYUSER $PASTEFY_DATABASE_READONLYUSER_PASSWORD
"PeerTube" postgres peertube-db $PEERTUBE_DATABASE_NAME $PEERTUBE_DATABASE_READONLYUSER $PEERTUBE_DATABASE_READONLYUSER_PASSWORD
"Penpot" postgres penpot-db $PENPOT_DATABASE_NAME $PENPOT_DATABASE_READONLYUSER $PENPOT_DATABASE_READONLYUSER_PASSWORD
"PhotoPrism" mysql photoprism-db $PHOTOPRISM_DATABASE_NAME $PHOTOPRISM_DATABASE_READONLYUSER $PHOTOPRISM_DATABASE_READONLYUSER_PASSWORD
"Piped" postgres piped-db $PIPED_DATABASE_NAME $PIPED_DATABASE_READONLYUSER $PIPED_DATABASE_READONLYUSER_PASSWORD
"Pixelfed" mysql pixelfed-db $PIXELFED_DATABASE_NAME $PIXELFED_DATABASE_READONLYUSER $PIXELFED_DATABASE_READONLYUSER_PASSWORD
"Rallly" postgres rallly-db $RALLLY_DATABASE_NAME $RALLLY_DATABASE_READONLYUSER $RALLLY_DATABASE_READONLYUSER_PASSWORD
"Shlink" postgres shlink-db $SHLINK_DATABASE_NAME $SHLINK_DATABASE_READONLYUSER $SHLINK_DATABASE_READONLYUSER_PASSWORD
"StandardNotes" mysql standardnotes-db $STANDARDNOTES_DATABASE_NAME $STANDARDNOTES_DATABASE_READONLYUSER $STANDARDNOTES_DATABASE_READONLYUSER_PASSWORD
"Speedtest Tracker Local" postgres speedtest-tracker-local-db $SPEEDTEST_TRACKER_LOCAL_DATABASE_NAME $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_LOCAL_DATABASE_READONLYUSER_PASSWORD
"Speedtest Tracker VPN" postgres speedtest-tracker-vpn-db $SPEEDTEST_TRACKER_VPN_DATABASE_NAME $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER $SPEEDTEST_TRACKER_VPN_DATABASE_READONLYUSER_PASSWORD
"Twenty" postgres twenty-db $TWENTY_DATABASE_NAME $TWENTY_DATABASE_READONLYUSER $TWENTY_DATABASE_READONLYUSER_PASSWORD
"Vaultwarden" postgres vaultwarden-db $VAULTWARDEN_DATABASE_NAME $VAULTWARDEN_DATABASE_READONLYUSER $VAULTWARDEN_DATABASE_READONLYUSER_PASSWORD
"Wallabag" postgres wallabag-db $WALLABAG_DATABASE_NAME $WALLABAG_DATABASE_READONLYUSER $WALLABAG_DATABASE_READONLYUSER_PASSWORD
"Wiki.js" postgres wikijs-db $WIKIJS_DATABASE_NAME $WIKIJS_DATABASE_READONLYUSER $WIKIJS_DATABASE_READONLYUSER_PASSWORD
"Wordpress" mysql wordpress-db $WORDPRESS_DATABASE_NAME $WORDPRESS_DATABASE_READONLYUSER $WORDPRESS_DATABASE_READONLYUSER_PASSWORD
"Yamtrack" postgres yamtrack-db $YAMTRACK_DATABASE_NAME $YAMTRACK_DATABASE_READONLYUSER $YAMTRACK_DATABASE_READONLYUSER_PASSWORD
"Zammad" postgres zammad-db $ZAMMAD_DATABASE_NAME $ZAMMAD_DATABASE_READONLYUSER $ZAMMAD_DATABASE_READONLYUSER_PASSWORD
"Zulip" postgres zulip-db $ZULIP_DATABASE_NAME $ZULIP_DATABASE_READONLYUSER $ZULIP_DATABASE_READONLYUSER_PASSWORD
"Taiga" postgres taiga-db $TAIGA_DATABASE_NAME $TAIGA_DATABASE_READONLYUSER $TAIGA_DATABASE_READONLYUSER_PASSWORD
"DocuSeal" postgres docuseal-db $DOCUSEAL_DATABASE_NAME $DOCUSEAL_DATABASE_READONLYUSER $DOCUSEAL_DATABASE_READONLYUSER_PASSWORD
"ControlR" postgres controlr-db $CONTROLR_DATABASE_NAME $CONTROLR_DATABASE_READONLYUSER $CONTROLR_DATABASE_READONLYUSER_PASSWORD
"Akaunting" mysql akaunting-db $AKAUNTING_DATABASE_NAME $AKAUNTING_DATABASE_READONLYUSER $AKAUNTING_DATABASE_READONLYUSER_PASSWORD
"Axelor" postgres axelor-db $AXELOR_DATABASE_NAME $AXELOR_DATABASE_READONLYUSER $AXELOR_DATABASE_READONLYUSER_PASSWORD
"Langflow" postgres langflow-db $LANGFLOW_DATABASE_NAME $LANGFLOW_DATABASE_READONLYUSER $LANGFLOW_DATABASE_READONLYUSER_PASSWORD
"LibreChat" postgres librechat-db $LIBRECHAT_DATABASE_NAME $LIBRECHAT_DATABASE_READONLYUSER $LIBRECHAT_DATABASE_READONLYUSER_PASSWORD
"OpenWebUI" postgres openwebui-db $OPENWEBUI_DATABASE_NAME $OPENWEBUI_DATABASE_READONLYUSER $OPENWEBUI_DATABASE_READONLYUSER_PASSWORD
"Khoj" postgres khoj-db $KHOJ_DATABASE_NAME $KHOJ_DATABASE_READONLYUSER $KHOJ_DATABASE_READONLYUSER_PASSWORD
"LobeChat" postgres lobechat-db $LOBECHAT_DATABASE_NAME $LOBECHAT_DATABASE_READONLYUSER $LOBECHAT_DATABASE_READONLYUSER_PASSWORD
"RAGFlow" postgres ragflow-db $RAGFLOW_DATABASE_NAME $RAGFLOW_DATABASE_READONLYUSER $RAGFLOW_DATABASE_READONLYUSER_PASSWORD
"Dify" postgres dify-db $DIFY_DATABASE_NAME $DIFY_DATABASE_READONLYUSER $DIFY_DATABASE_READONLYUSER_PASSWORD
"MindsDB" postgres mindsdb-db $MINDSDB_DATABASE_NAME $MINDSDB_DATABASE_READONLYUSER $MINDSDB_DATABASE_READONLYUSER_PASSWORD
"WaterCrawl" postgres watercrawl-db $WATERCRAWL_DATABASE_NAME $WATERCRAWL_DATABASE_READONLYUSER $WATERCRAWL_DATABASE_READONLYUSER_PASSWORD
"Flowise" postgres flowise-db $FLOWISE_DATABASE_NAME $FLOWISE_DATABASE_READONLYUSER $FLOWISE_DATABASE_READONLYUSER_PASSWORD
"NocoDB" postgres nocodb-db $NOCODB_DATABASE_NAME $NOCODB_DATABASE_READONLYUSER $NOCODB_DATABASE_READONLYUSER_PASSWORD
"Ente" postgres ente-db $ENTE_DATABASE_NAME $ENTE_DATABASE_READONLYUSER $ENTE_DATABASE_READONLYUSER_PASSWORD
"Morphic" postgres morphic-db $MORPHIC_DATABASE_NAME $MORPHIC_DATABASE_READONLYUSER $MORPHIC_DATABASE_READONLYUSER_PASSWORD
"Memos" postgres memos-db $MEMOS_DATABASE_NAME $MEMOS_DATABASE_READONLYUSER $MEMOS_DATABASE_READONLYUSER_PASSWORD
"Speakr" postgres speakr-db $SPEAKR_DATABASE_NAME $SPEAKR_DATABASE_READONLYUSER $SPEAKR_DATABASE_READONLYUSER_PASSWORD
"Monica" mysql monica-db $MONICA_DATABASE_NAME $MONICA_DATABASE_READONLYUSER $MONICA_DATABASE_READONLYUSER_PASSWORD
"AFFiNE" postgres affine-db $AFFINE_DATABASE_NAME $AFFINE_DATABASE_READONLYUSER $AFFINE_DATABASE_READONLYUSER_PASSWORD
"Joplin" postgres joplin-db $JOPLIN_DATABASE_NAME $JOPLIN_DATABASE_READONLYUSER $JOPLIN_DATABASE_READONLYUSER_PASSWORD
"JoplinTranscribe" postgres joplin-transcribe-db $JOPLIN_TRANSCRIBE_DATABASE_NAME $JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER $JOPLIN_TRANSCRIBE_DATABASE_READONLYUSER_PASSWORD
"Superset" postgres superset-db $SUPERSET_DATABASE_NAME $SUPERSET_DATABASE_READONLYUSER $SUPERSET_DATABASE_READONLYUSER_PASSWORD
"Wekan" mongodb wekan-db $WEKAN_DATABASE_NAME $WEKAN_DATABASE_READONLYUSER $WEKAN_DATABASE_READONLYUSER_PASSWORD
"Revolt" mongodb revolt-db $REVOLT_DATABASE_NAME $REVOLT_DATABASE_READONLYUSER $REVOLT_DATABASE_READONLYUSER_PASSWORD
"OpenSign" mongodb opensign-db $OPENSIGN_DATABASE_NAME $OPENSIGN_DATABASE_READONLYUSER $OPENSIGN_DATABASE_READONLYUSER_PASSWORD
"LibreChatMongoDB" mongodb librechat-mongodb $LIBRECHAT_MONGODB_DATABASE $LIBRECHAT_MONGODB_READONLYUSER $LIBRECHAT_MONGODB_READONLYUSER_PASSWORD
"DocsGPT" mongodb docsgpt-db $DOCSGPT_DATABASE_NAME $DOCSGPT_DATABASE_READONLYUSER $DOCSGPT_DATABASE_READONLYUSER_PASSWORD
"LiteLLM" postgres litellm-db $LITELLM_DATABASE_NAME $LITELLM_DATABASE_READONLYUSER $LITELLM_DATABASE_READONLYUSER_PASSWORD
"Langfuse" postgres langfuse-db $LANGFUSE_DATABASE_NAME $LANGFUSE_DATABASE_READONLYUSER $LANGFUSE_DATABASE_READONLYUSER_PASSWORD
"Skyvern" postgres skyvern-db $SKYVERN_DATABASE_NAME $SKYVERN_DATABASE_READONLYUSER $SKYVERN_DATABASE_READONLYUSER_PASSWORD
"Wger" postgres wger-db $WGER_DATABASE_NAME $WGER_DATABASE_READONLYUSER $WGER_DATABASE_READONLYUSER_PASSWORD
"WorkoutCool" postgres workoutcool-db $WORKOUTCOOL_DATABASE_NAME $WORKOUTCOOL_DATABASE_READONLYUSER $WORKOUTCOOL_DATABASE_READONLYUSER_PASSWORD
"AutoKB" postgres autokb-db $AUTOKB_DATABASE_NAME $AUTOKB_DATABASE_READONLYUSER $AUTOKB_DATABASE_READONLYUSER_PASSWORD
"SuiteCRM" mysql suitecrm-db $SUITECRM_DATABASE_NAME $SUITECRM_DATABASE_READONLYUSER $SUITECRM_DATABASE_READONLYUSER_PASSWORD
"HedgeDoc" postgres hedgedoc-db $HEDGEDOC_DATABASE_NAME $HEDGEDOC_DATABASE_READONLYUSER $HEDGEDOC_DATABASE_READONLYUSER_PASSWORD
"BasicMemory" postgres basicmemory-db $BASICMEMORY_DATABASE_NAME $BASICMEMORY_DATABASE_READONLYUSER $BASICMEMORY_DATABASE_READONLYUSER_PASSWORD
"Cognee" postgres cognee-db $COGNEE_DATABASE_NAME $COGNEE_DATABASE_READONLYUSER $COGNEE_DATABASE_READONLYUSER_PASSWORD
"LightRAG" postgres lightrag-db $LIGHTRAG_DATABASE_NAME $LIGHTRAG_DATABASE_READONLYUSER $LIGHTRAG_DATABASE_READONLYUSER_PASSWORD
"mdCollab" postgres mdcollab-db $MDCOLLAB_DATABASE_NAME $MDCOLLAB_DATABASE_READONLYUSER $MDCOLLAB_DATABASE_READONLYUSER_PASSWORD
#ADD_NEW_AISTACK_DB_IMPORT_HERE
EOFAS
}

function outputConfigMindsDB()
{
  cat < $HOME/mindsdb-compose.yml
$STACK_VERSION_PREFIX mindsdb $(getScriptStackVersion mindsdb)

services:
  mindsdb-db:
    image: $(getScriptImageByContainerName mindsdb-db)
    container_name: mindsdb-db
    hostname: mindsdb-db
    user: "\${PORTAINER_UID}:\${PORTAINER_GID}"
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    shm_size: 256mb
    networks:
      - int-mindsdb-net
      - dock-dbs-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/mindsdb/db:/var/lib/postgresql/data
      - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/mindsdb/dbexport:/dbexport
      - \${PORTAINER_HSHQ_STACKS_DIR}/mindsdb/dbimport:/dbimport
    labels:
      - "ofelia.enabled=true"
      - "ofelia.job-exec.mindsdb-hourly-db.schedule=@every 1h"
      - "ofelia.job-exec.mindsdb-hourly-db.command=/exportDB.sh"
      - "ofelia.job-exec.mindsdb-hourly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.mindsdb-hourly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.mindsdb-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.mindsdb-hourly-db.email-from=MindsDB Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.mindsdb-hourly-db.mail-only-on-error=true"
      - "ofelia.job-exec.mindsdb-monthly-db.schedule=0 0 8 1 * *"
      - "ofelia.job-exec.mindsdb-monthly-db.command=/exportDB.sh"
      - "ofelia.job-exec.mindsdb-monthly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.mindsdb-monthly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.mindsdb-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.mindsdb-monthly-db.email-from=MindsDB Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.mindsdb-monthly-db.mail-only-on-error=false"

  mindsdb-app:
    image: $(getScriptImageByContainerName mindsdb-app)
    container_name: mindsdb-app
    hostname: mindsdb-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - mindsdb-db
    networks:
      - int-mindsdb-net
      - dock-ext-net
      - dock-internalmail-net
      - dock-aipriv-net
      - dock-dbs-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - v-mindsdb-data:/mindsdb
      - /etc/ssl/certs/ca-certificates.crt:/usr/local/lib/\${PYTHON_VER}/site-packages/pip/_vendor/certifi/cacert.pem:ro
      - /etc/ssl/certs/ca-certificates.crt:/venv/lib/\${PYTHON_VER}/site-packages/certifi/cacert.pem:ro
      - /etc/ssl/certs/ca-certificates.crt:/venv/lib/\${PYTHON_VER}/site-packages/pip/_vendor/certifi/cacert.pem:ro
      - /etc/ssl/certs/ca-certificates.crt:/venv/lib/\${PYTHON_VER}/site-packages/pip-25.3.dist-info/licenses/src/pip/_vendor/certifi/cacert.pem:ro

  mindsdb-redis:
    image: $(getScriptImageByContainerName mindsdb-redis)
    container_name: mindsdb-redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command: redis-server
      --requirepass $MINDSDB_REDIS_PASSWORD
      --appendonly no
    networks:
      - int-mindsdb-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - v-mindsdb-redis:/data

volumes:
  v-mindsdb-data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: \${PORTAINER_HSHQ_STACKS_DIR}/mindsdb/data

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-internalmail-net:
    name: dock-internalmail
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  dock-dbs-net:
    name: dock-dbs
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  int-mindsdb-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  cat < $HOME/mindsdb.env
TZ=\${PORTAINER_TZ}
POSTGRES_DB=$MINDSDB_DATABASE_NAME
POSTGRES_USER=$MINDSDB_DATABASE_USER
POSTGRES_PASSWORD=$MINDSDB_DATABASE_USER_PASSWORD
MINDSDB_DOCKER_ENV=1
MINDSDB_APIS=http
MINDSDB_DATA_CATALOG_ENABLED=1
MINDSDB_ML_QUEUE_TYPE=redis
MINDSDB_ML_QUEUE_HOST=mindsdb-redis
MINDSDB_ML_QUEUE_PASSWORD=$MINDSDB_REDIS_PASSWORD
MINDSDB_DB_CON=postgresql://$MINDSDB_DATABASE_USER:$MINDSDB_DATABASE_USER_PASSWORD@mindsdb-db:5432/$MINDSDB_DATABASE_NAME
MINDSDB_STORAGE_DIR=/mindsdb/var
MINDSDB_USERNAME=$MINDSDB_ADMIN_USERNAME
MINDSDB_PASSWORD=$MINDSDB_ADMIN_PASSWORD
SENTRY_IO_DSN=
SENTRY_IO_ENVIRONMENT=local
PYTHON_VER=python3.10
MINDSDB_LOG_LEVEL=DEBUG
MINDSDB_CONSOLE_LOG_LEVEL=DEBUG
MINDSDB_FILE_LOG_LEVEL=DEBUG
MINDSDB_TELEMETRY=false
EOFMT
  outputDBsList $HSHQ_STACKS_DIR/mindsdb/dbimport/$ALLDBS_IMPORT_FILENAME
  cat < $HSHQ_STACKS_DIR/mindsdb/dbimport/importConnections.sh
#!/bin/bash

MINDSDB_DATABASE_NAME=$MINDSDB_DATABASE_NAME
MINDSDB_DATABASE_USER=$MINDSDB_DATABASE_USER
PGPASSWORD=$MINDSDB_DATABASE_USER_PASSWORD
curdt=\$(date '+%Y-%m-%d %H:%M:%S.%3N')

function main()
{
  if ! [ -f /dbimport/$ALLDBS_IMPORT_FILENAME ]; then
    return
  fi
  while read curLine; do
    if [ -z "\$curLine" ] || [ "\${curLine:0:1}" = "#" ]; then
      continue
    fi
    connName=\$(echo "\$curLine" | cut -d" " -f1 | sed 's/"//g')
    connType=\$(echo "\$curLine" | cut -d" " -f2)
    connHost=\$(echo "\$curLine" | cut -d" " -f3)
    connDatabase=\$(echo "\$curLine" | cut -d" " -f4)
    connUser=\$(echo "\$curLine" | cut -d" " -f5)
    connPassword=\$(echo "\$curLine" | cut -d" " -f6)
    case "\$connType" in
      "postgres")
        connPort=5432
      ;;
      "mysql")
        connPort=3306
      ;;
      "mongodb")
        connPort=27017
      ;;
      *)
        continue
      ;;
    esac
    sqlcmd="select count(*) from integration where name='\$connName';"
    checkExist=\$(echo "\$sqlcmd" | psql -t -U \$MINDSDB_DATABASE_USER \$MINDSDB_DATABASE_NAME | xargs)
    if [ -z "\$checkExist" ]; then
      echo "ERROR: There was a problem adding this DB Connection to MindsDB - \$connName"
      continue
    fi
    if [ \$checkExist -eq 0 ]; then
      if [ "\$connType" = "mongodb" ]; then
        sqlcmd="insert into integration(updated_at, created_at, name, data, engine) values ('\$curdt', '\$curdt', '\$connName','{\"host\": \"mongodb://\$connUser:\$connPassword@\$connHost:\$connPort/\$connDatabase?authSource=admin\", \"password\": \"\$connPassword\", \"port\": \"\$connPort\"}','\$connType');"
      else
        sqlcmd="insert into integration(updated_at, created_at, name, data, engine) values ('\$curdt', '\$curdt', '\$connName','{\"database\": \"\$connDatabase\", \"host\": \"\$connHost\", \"password\": \"\$connPassword\", \"port\": \"\$connPort\", \"user\": \"\$connUser\"}','\$connType');"
      fi
      echo "\$sqlcmd" | psql -U \$MINDSDB_DATABASE_USER \$MINDSDB_DATABASE_NAME > /dev/null 2>&1
    else
      echo "INFO: DB Connection already exists in MindsDB - \$connName"
    fi
  done "
  if ! [ "$WATERCRAWL_INIT_ENV" = "true" ]; then
    WATERCRAWL_INIT_ENV=true
    updateConfigVar WATERCRAWL_INIT_ENV $WATERCRAWL_INIT_ENV
  fi
  sleep 3
  addReadOnlyUserToDatabase WaterCrawl postgres watercrawl-db $WATERCRAWL_DATABASE_NAME $WATERCRAWL_DATABASE_USER $WATERCRAWL_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/watercrawl/dbexport $WATERCRAWL_DATABASE_READONLYUSER $WATERCRAWL_DATABASE_READONLYUSER_PASSWORD
  if [ -z "$FMLNAME_WATERCRAWL_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://watercrawl-nginx {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_WATERCRAWL_APP $MANAGETLS_WATERCRAWL_APP "$is_integrate_hshq" $NETDEFAULT_WATERCRAWL_APP "$inner_block"
  insertSubAuthelia $SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll watercrawl "$FMLNAME_WATERCRAWL_APP" $USERTYPE_WATERCRAWL_APP "https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN" "watercrawl.png" "$(getHeimdallOrderFromSub $SUB_WATERCRAWL_APP $USERTYPE_WATERCRAWL_APP)"
    restartAllCaddyContainers
    checkAddDBConnection true watercrawl "$FMLNAME_WATERCRAWL_APP" postgres watercrawl-db $WATERCRAWL_DATABASE_NAME $WATERCRAWL_DATABASE_USER $WATERCRAWL_DATABASE_USER_PASSWORD
  fi
}

function outputConfigWaterCrawl()
{
  cat < $HOME/watercrawl-compose.yml
$STACK_VERSION_PREFIX watercrawl $(getScriptStackVersion watercrawl)

services:
  watercrawl-db:
    image: $(getScriptImageByContainerName watercrawl-db)
    container_name: watercrawl-db
    hostname: watercrawl-db
    user: "\${PORTAINER_UID}:\${PORTAINER_GID}"
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    shm_size: 256mb
    networks:
      - int-watercrawl-net
      - dock-dbs-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/watercrawl/db:/var/lib/postgresql/data
      - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/watercrawl/dbexport:/dbexport
    labels:
      - "ofelia.enabled=true"
      - "ofelia.job-exec.watercrawl-hourly-db.schedule=@every 1h"
      - "ofelia.job-exec.watercrawl-hourly-db.command=/exportDB.sh"
      - "ofelia.job-exec.watercrawl-hourly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.watercrawl-hourly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.watercrawl-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.watercrawl-hourly-db.email-from=WaterCrawl Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.watercrawl-hourly-db.mail-only-on-error=true"
      - "ofelia.job-exec.watercrawl-monthly-db.schedule=0 0 8 1 * *"
      - "ofelia.job-exec.watercrawl-monthly-db.command=/exportDB.sh"
      - "ofelia.job-exec.watercrawl-monthly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.watercrawl-monthly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.watercrawl-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.watercrawl-monthly-db.email-from=WaterCrawl Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.watercrawl-monthly-db.mail-only-on-error=false"

  watercrawl-app:
    image: $(getScriptImageByContainerName watercrawl-app)
    container_name: watercrawl-app
    hostname: watercrawl-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - watercrawl-db
    command: [ "gunicorn", "-b", "0.0.0.0:9000", "-w", "2", "watercrawl.wsgi:application", "--access-logfile", "-", "--error-logfile", "-", "--timeout", "60" ]
    networks:
      - int-watercrawl-net
      - dock-aipriv-net
      - dock-ext-net
      - dock-internalmail-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  watercrawl-celery:
    image: $(getScriptImageByContainerName watercrawl-celery)
    container_name: watercrawl-celery
    hostname: watercrawl-celery
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - watercrawl-db
    command: [ "celery", "-A", "watercrawl", "worker", "-l", "info", "-S", "django" ]
    networks:
      - int-watercrawl-net
      - dock-aipriv-net
      - dock-ext-net
      - dock-internalmail-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  watercrawl-beat:
    image: $(getScriptImageByContainerName watercrawl-beat)
    container_name: watercrawl-beat
    hostname: watercrawl-beat
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - watercrawl-db
    command: [ "celery", "-A", "watercrawl", "beat", "-l", "info", "-S", "django" ]
    networks:
      - int-watercrawl-net
      - dock-aipriv-net
      - dock-ext-net
      - dock-internalmail-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  watercrawl-playwright:
    image: $(getScriptImageByContainerName watercrawl-playwright)
    container_name: watercrawl-playwright
    hostname: watercrawl-playwright
    restart: unless-stopped
    env_file: stack.env
    user: root
    security_opt:
      - no-new-privileges:true
    depends_on:
      - watercrawl-db
    networks:
      - int-watercrawl-net
      - dock-aipriv-net
      - dock-ext-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
    environment:
      - PORT=8000
      - HOST=0.0.0.0

  watercrawl-frontend:
    image: $(getScriptImageByContainerName watercrawl-frontend)
    container_name: watercrawl-frontend
    hostname: watercrawl-frontend
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - watercrawl-db
    command: [ "npm", "run", "serve" ]
    networks:
      - int-watercrawl-net
      - dock-aipriv-net
      - dock-ext-net
      - dock-internalmail-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  watercrawl-nginx:
    image: $(getScriptImageByContainerName watercrawl-nginx)
    container_name: watercrawl-nginx
    hostname: watercrawl-nginx
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - watercrawl-app
      - watercrawl-frontend
      - watercrawl-minio
    command: [ "/bin/sh", "/entrypoint.sh" ]
    networks:
      - int-watercrawl-net
      - dock-proxy-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/watercrawl/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
      - \${PORTAINER_HSHQ_STACKS_DIR}/watercrawl/nginx/entrypoint.sh:/entrypoint.sh

  watercrawl-minio:
    image: $(getScriptImageByContainerName watercrawl-minio)
    container_name: watercrawl-minio
    hostname: watercrawl-minio
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    command: server /data --console-address ":9001"
    networks:
      - int-watercrawl-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/watercrawl/minio:/data

  watercrawl-redis:
    image: $(getScriptImageByContainerName watercrawl-redis)
    container_name: watercrawl-redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command: redis-server
      --requirepass $WATERCRAWL_REDIS_PASSWORD
      --appendonly no
    networks:
      - int-watercrawl-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  watercrawl-mcp:
    image: $(getScriptImageByContainerName watercrawl-mcp)
    container_name: watercrawl-mcp
    hostname: watercrawl-mcp
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - watercrawl-db
    command: [ "sse", "--base-url", "http://watercrawl-app:9000", '--port', '3000', '--endpoint', '/sse' ]
    networks:
      - int-watercrawl-net
      - dock-aipriv-net
      - dock-ext-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-internalmail-net:
    name: dock-internalmail
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  dock-dbs-net:
    name: dock-dbs
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  int-watercrawl-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  cat < $HOME/watercrawl.env
TZ=\${PORTAINER_TZ}
TIME_ZONE=America/Chicago
POSTGRES_HOST=watercrawl-db
POSTGRES_DB=$WATERCRAWL_DATABASE_NAME
POSTGRES_USER=$WATERCRAWL_DATABASE_USER
POSTGRES_PASSWORD=$WATERCRAWL_DATABASE_USER_PASSWORD
DATABASE_URL=postgresql://$WATERCRAWL_DATABASE_USER:$WATERCRAWL_DATABASE_USER_PASSWORD@watercrawl-db:5432/$WATERCRAWL_DATABASE_NAME
SECRET_KEY=$WATERCRAWL_SECRET_KEY
API_ENCRYPTION_KEY=$WATERCRAWL_API_ENCRYPTION_KEY
DEBUG=True
FRONTEND_URL=https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN
CELERY_BROKER_URL=redis://:$WATERCRAWL_REDIS_PASSWORD@watercrawl-redis:6379/0
REDIS_URL=redis://:$WATERCRAWL_REDIS_PASSWORD@watercrawl-redis:6379/1
REDIS_LOCKER_URL=redis://:$WATERCRAWL_REDIS_PASSWORD@watercrawl-redis:6379/3
MINIO_ROOT_USER=$WATERCRAWL_MINIO_KEY
MINIO_ROOT_PASSWORD=$WATERCRAWL_MINIO_SECRET
MINIO_ENDPOINT=watercrawl-minio:9000
MINIO_EXTERNAL_ENDPOINT=$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN
MINIO_ACCESS_KEY=$WATERCRAWL_MINIO_KEY
MINIO_SECRET_KEY=$WATERCRAWL_MINIO_SECRET
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS=True
MINIO_USE_HTTPS=False
MINIO_BROWSER_REDIRECT_URL=https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN/minio-console/
MINIO_SERVER_URL=http://watercrawl-minio:9000
CORS_ALLOW_ALL_ORIGINS=True
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=$SMTP_HOSTNAME
EMAIL_PORT=$SMTP_HOSTPORT
DEFAULT_FROM_EMAIL=WaterCrawl $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>
PLAYWRIGHT_SERVER=http://watercrawl-playwright:8000
PLAYWRIGHT_API_KEY=$WATERCRAWL_PLAYWRIGHT_API_KEY
AUTH_API_KEY=$WATERCRAWL_PLAYWRIGHT_API_KEY
API_BASE_URL=https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN/
MCP_SERVER=https://$SUB_WATERCRAWL_APP.$HOMESERVER_DOMAIN/sse
IS_ENTERPRISE_MODE_ACTIVE=False
IS_LOGIN_ACTIVE=True
IS_SIGNUP_ACTIVE=False
IS_GITHUB_LOGIN_ACTIVE=False
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
IS_GOOGLE_LOGIN_ACTIVE=False
EOFMT
  cat < $HSHQ_STACKS_DIR/watercrawl/nginx/nginx.conf
server {
    listen 80;
    server_name localhost;
    client_max_body_size 100M;

    # Frontend
    location / {
        proxy_pass http://watercrawl-frontend:80;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }

    # API
    location /api/ {
        proxy_pass http://watercrawl-app:9000;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }

    # MCP
    location ~ ^/(sse|messages) {
        proxy_pass http://watercrawl-mcp:3000;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;


        # Important SSE settings
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Disable buffering so events are sent immediately
        proxy_buffering off;
        proxy_cache off;

        # Increase timeouts so connection stays open
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }


    # MinIO private bucket
    location /private/ {
        proxy_pass http://watercrawl-minio:9000/private/;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_buffering off;
    }

    # MinIO public bucket
    location /public/ {
        proxy_pass http://watercrawl-minio:9000/public/;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_buffering off;
    }

    # MinIO API - for direct S3 operations
    # location /minio/api/ {
    #     proxy_pass http://watercrawl-minio:9000/;
    #     proxy_set_header Host \$host;
    #     proxy_set_header X-Real-IP \$remote_addr;
    #     proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    #     proxy_set_header X-Forwarded-Proto \$scheme;
    #     proxy_buffering off;
    # }

    # MinIO Console
    location /minio-console/ {
        proxy_pass http://watercrawl-minio:9001/;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        # Rewrite location headers
        proxy_redirect / /minio-console/;
    }
}
EOFMT
  cat < $HSHQ_STACKS_DIR/watercrawl/nginx/entrypoint.sh
#!/bin/sh
set -e

# Start Nginx
exec nginx -g 'daemon off;'
EOFMT
}

function performUpdateWaterCrawl()
{
  perform_stack_name=watercrawl
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:17.5-bookworm,mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/playwright:1.2,mirror.gcr.io/watercrawl/frontend:v0.12.1,mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/watercrawl/mcp:v1.3.0
      image_update_map[0]="mirror.gcr.io/postgres:17.5-bookworm,mirror.gcr.io/postgres:17.5-bookworm"
      image_update_map[1]="mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/watercrawl:v0.12.1"
      image_update_map[2]="mirror.gcr.io/watercrawl/playwright:1.2,mirror.gcr.io/watercrawl/playwright:1.2"
      image_update_map[3]="mirror.gcr.io/watercrawl/frontend:v0.12.1,mirror.gcr.io/watercrawl/frontend:v0.12.1"
      image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine"
      image_update_map[5]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0"
      image_update_map[6]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[7]="mirror.gcr.io/watercrawl/mcp:v1.3.0,mirror.gcr.io/watercrawl/mcp:v1.3.0"
    ;;
    2)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:17.5-bookworm,mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/playwright:1.2,mirror.gcr.io/watercrawl/frontend:v0.12.1,mirror.gcr.io/nginx:1.29.3-alpine,ghcr.io/homeserverhq/minio:v1.0,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/watercrawl/mcp:v1.3.0
      image_update_map[0]="mirror.gcr.io/postgres:17.5-bookworm,mirror.gcr.io/postgres:17.5-bookworm"
      image_update_map[1]="mirror.gcr.io/watercrawl/watercrawl:v0.12.1,mirror.gcr.io/watercrawl/watercrawl:v0.12.1"
      image_update_map[2]="mirror.gcr.io/watercrawl/playwright:1.2,mirror.gcr.io/watercrawl/playwright:1.2"
      image_update_map[3]="mirror.gcr.io/watercrawl/frontend:v0.12.1,mirror.gcr.io/watercrawl/frontend:v0.12.1"
      image_update_map[4]="mirror.gcr.io/nginx:1.29.3-alpine,mirror.gcr.io/nginx:1.29.3-alpine"
      image_update_map[5]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0"
      image_update_map[6]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[7]="mirror.gcr.io/watercrawl/mcp:v1.3.0,mirror.gcr.io/watercrawl/mcp:v1.3.0"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

# Flowise
function installFlowise()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory flowise "Flowise"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName flowise-db)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName flowise-app)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName flowise-worker)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName flowise-redis)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName flowise-minio)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/flowise
  mkdir $HSHQ_STACKS_DIR/flowise/minio
  mkdir $HSHQ_STACKS_DIR/flowise/data
  mkdir $HSHQ_STACKS_DIR/flowise/db
  mkdir $HSHQ_STACKS_DIR/flowise/dbexport
  chmod 777 $HSHQ_STACKS_DIR/flowise/dbexport
  initServicesCredentials
  set +e
  addUserMailu alias $FLOWISE_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS
  outputConfigFlowise
  installStack flowise flowise-app "Flowise Server is listening at" $HOME/flowise.env 5
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$FLOWISE_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_FLOWISE_APP Admin Login Info" -b "$FMLNAME_FLOWISE_APP Admin Email: $FLOWISE_ADMIN_EMAIL_ADDRESS\n$FMLNAME_FLOWISE_APP Admin Password: $FLOWISE_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    FLOWISE_INIT_ENV=true
    updateConfigVar FLOWISE_INIT_ENV $FLOWISE_INIT_ENV
  fi
  sleep 3
  docker exec flowise-db /dbexport/setupDBSettings.sh > /dev/null 2>&1
  rm -f $HSHQ_STACKS_DIR/flowise/dbexport/setupDBSettings.sh
  addReadOnlyUserToDatabase Flowise postgres flowise-db $FLOWISE_DATABASE_NAME $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/flowise/dbexport $FLOWISE_DATABASE_READONLYUSER $FLOWISE_DATABASE_READONLYUSER_PASSWORD
  if [ -z "$FMLNAME_FLOWISE_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_FLOWISE_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://flowise-app:3000 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_FLOWISE_APP $MANAGETLS_FLOWISE_APP "$is_integrate_hshq" $NETDEFAULT_FLOWISE_APP "$inner_block"
  insertSubAuthelia $SUB_FLOWISE_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll flowise "$FMLNAME_FLOWISE_APP" $USERTYPE_FLOWISE_APP "https://$SUB_FLOWISE_APP.$HOMESERVER_DOMAIN" "flowise.png" "$(getHeimdallOrderFromSub $SUB_FLOWISE_APP $USERTYPE_FLOWISE_APP)"
    restartAllCaddyContainers
    checkAddDBConnection true flowise "$FMLNAME_FLOWISE_APP" postgres flowise-db $FLOWISE_DATABASE_NAME $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_USER_PASSWORD
  fi
}

function outputConfigFlowise()
{
  cat < $HOME/flowise-compose.yml
$STACK_VERSION_PREFIX flowise $(getScriptStackVersion flowise)

services:
  flowise-db:
    image: $(getScriptImageByContainerName flowise-db)
    container_name: flowise-db
    hostname: flowise-db
    user: "\${PORTAINER_UID}:\${PORTAINER_GID}"
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    shm_size: 256mb
    networks:
      - int-flowise-net
      - dock-dbs-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/flowise/db:/var/lib/postgresql/data
      - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/flowise/dbexport:/dbexport
    labels:
      - "ofelia.enabled=true"
      - "ofelia.job-exec.flowise-hourly-db.schedule=@every 1h"
      - "ofelia.job-exec.flowise-hourly-db.command=/exportDB.sh"
      - "ofelia.job-exec.flowise-hourly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.flowise-hourly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.flowise-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.flowise-hourly-db.email-from=Flowise Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.flowise-hourly-db.mail-only-on-error=true"
      - "ofelia.job-exec.flowise-monthly-db.schedule=0 0 8 1 * *"
      - "ofelia.job-exec.flowise-monthly-db.command=/exportDB.sh"
      - "ofelia.job-exec.flowise-monthly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.flowise-monthly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.flowise-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.flowise-monthly-db.email-from=Flowise Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.flowise-monthly-db.mail-only-on-error=false"

  flowise-app:
    image: $(getScriptImageByContainerName flowise-app)
    container_name: flowise-app
    hostname: flowise-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - flowise-db
      - flowise-redis
      - flowise-minio
    entrypoint: /bin/sh -c "sleep 3; flowise start"
    networks:
      - int-flowise-net
      - dock-ext-net
      - dock-internalmail-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/flowise/data:/root/.flowise

  flowise-worker:
    image: $(getScriptImageByContainerName flowise-worker)
    container_name: flowise-worker
    hostname: flowise-worker
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - flowise-db
      - flowise-redis
      - flowise-minio
    entrypoint: /bin/sh -c "node /app/healthcheck/healthcheck.js & sleep 5 && pnpm run start-worker"
    networks:
      - int-flowise-net
      - dock-ext-net
      - dock-internalmail-net
      - dock-aipriv-net
    healthcheck:
        test: ['CMD', 'curl', '-f', 'http://localhost:5566/healthz']
        interval: 10s
        timeout: 5s
        retries: 5
        start_period: 30s
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/flowise/data:/root/.flowise

  flowise-redis:
    image: $(getScriptImageByContainerName flowise-redis)
    container_name: flowise-redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command: redis-server
      --requirepass $FLOWISE_REDIS_PASSWORD
      --appendonly no
    networks:
      - int-flowise-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  flowise-minio:
    image: $(getScriptImageByContainerName flowise-minio)
    container_name: flowise-minio
    hostname: flowise-minio
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-flowise-net
    entrypoint: >
      /bin/sh -c "
        minio server /data --address ':9000' --console-address ':9001' &
        MINIO_PID=\\\$!
        while ! curl -s http://localhost:9000/minio/health/live; do
          echo 'Waiting for MinIO to start...'
          sleep 1
        done
        sleep 5
        mc alias set minio http://localhost:9000 ${FLOWISE_MINIO_KEY} ${FLOWISE_MINIO_SECRET}
        echo 'Creating bucket flowise'
        mc mb minio/flowise
        wait \\\$MINIO_PID
      "
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/flowise/minio:/data

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-internalmail-net:
    name: dock-internalmail
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  dock-dbs-net:
    name: dock-dbs
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  int-flowise-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  cat < $HOME/flowise.env
TZ=\${PORTAINER_TZ}
POSTGRES_DB=$FLOWISE_DATABASE_NAME
POSTGRES_USER=$FLOWISE_DATABASE_USER
POSTGRES_PASSWORD=$FLOWISE_DATABASE_USER_PASSWORD
DATABASE_TYPE=postgres
DATABASE_PORT=5432
DATABASE_HOST=flowise-db
DATABASE_NAME=$FLOWISE_DATABASE_NAME
DATABASE_USER=$FLOWISE_DATABASE_USER
DATABASE_PASSWORD=$FLOWISE_DATABASE_USER_PASSWORD
SECRETKEY_STORAGE_TYPE=local
SECRETKEY_PATH=/home/node/.flowise
LOG_PATH=/home/node/.flowise/logs
MINIO_ROOT_USER=$FLOWISE_MINIO_KEY
MINIO_ROOT_PASSWORD=$FLOWISE_MINIO_SECRET
STORAGE_TYPE=s3
BLOB_STORAGE_PATH=/home/node/.flowise/storage
S3_STORAGE_BUCKET_NAME=flowise
S3_STORAGE_ACCESS_KEY_ID=$FLOWISE_MINIO_KEY
S3_STORAGE_SECRET_ACCESS_KEY=$FLOWISE_MINIO_SECRET
S3_STORAGE_REGION=us-west-1
S3_ENDPOINT_URL=http://flowise-minio:9000
S3_FORCE_PATH_STYLE=true
SHOW_COMMUNITY_NODES=true
DISABLE_FLOWISE_TELEMETRY=true
APP_URL=https://$SUB_FLOWISE_APP.$HOMESERVER_DOMAIN
SMTP_HOST=$SMTP_HOSTNAME
SMTP_PORT=$SMTP_HOSTPORT
SMTP_USER=
SMTP_PASSWORD=
SMTP_SECURE=false
ALLOW_UNAUTHORIZED_CERTS=true
SENDER_EMAIL=Flowise $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>
JWT_AUTH_TOKEN_SECRET=$FLOWISE_JWT_SECRET
JWT_REFRESH_TOKEN_SECRET=$FLOWISE_JWT_REFRESH_SECRET
JWT_ISSUER=ISSUER
JWT_AUDIENCE=AUDIENCE
JWT_TOKEN_EXPIRY_IN_MINUTES=360
JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
EXPRESS_SESSION_SECRET=$FLOWISE_EXPRESS_SESSION_SECRET
TOKEN_HASH_SECRET=$FLOWISE_TOKEN_HASH_SECRET
ENABLE_METRICS=false
MODE=queue
QUEUE_NAME=flowise-queue
QUEUE_REDIS_EVENT_STREAM_MAX_LEN=1000
WORKER_CONCURRENCY=5
REMOVE_ON_AGE=86400
REMOVE_ON_COUNT=1000
REDIS_URL=redis://:$FLOWISE_REDIS_PASSWORD@flowise-redis:6379
REDIS_HOST=flowise-redis
REDIS_PORT=6379
REDIS_PASSWORD=$FLOWISE_REDIS_PASSWORD
EOFMT
  useruuid=$(uuidgen)
  orguuid=$(uuidgen)
  workuuid=$(uuidgen)
  dtnow=$(date '+%Y-%m-%d %H:%M:%S.%6N')
  cat < $HSHQ_STACKS_DIR/flowise/dbexport/setupDBSettings.sh
#!/bin/bash

PGPASSWORD=$FLOWISE_DATABASE_USER_PASSWORD
FLOWISE_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 5 "" "$FLOWISE_ADMIN_PASSWORD" | tr -d ':\n' | sed 's/$2y/$2a/' | sed 's/\$/\\\$/g')

roleid=\$(echo "select id from role where name='owner';" | psql -t -A -U $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_NAME 2> /dev/null)

echo "insert into \"user\"(id, name, email, credential, \"tempToken\", \"tokenExpiry\", status, \"createdDate\", \"updatedDate\", \"createdBy\", \"updatedBy\") values('$useruuid','Flowise HSHQ Admin','$FLOWISE_ADMIN_EMAIL_ADDRESS','\$FLOWISE_ADMIN_PASSWORD_HASH',NULL,NULL,'active','$dtnow','$dtnow','$useruuid','$useruuid');" | psql -U $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_NAME

echo "insert into organization(id, name, \"customerId\", \"subscriptionId\", \"createdDate\", \"updatedDate\", \"createdBy\", \"updatedBy\") values('$orguuid','$HOMESERVER_NAME',NULL,NULL,'$dtnow','$dtnow','$useruuid','$useruuid');" | psql -U $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_NAME

echo "insert into organization_user(\"organizationId\", \"userId\", \"roleId\", status, \"createdDate\", \"updatedDate\", \"createdBy\", \"updatedBy\") values('$orguuid','$useruuid','\$roleid','active','$dtnow','$dtnow','$useruuid','$useruuid');" | psql -U $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_NAME

echo "insert into workspace(id, name, description, \"createdDate\", \"updatedDate\", \"organizationId\", \"createdBy\", \"updatedBy\") values('$workuuid','$HOMESERVER_NAME Workspace',NULL,'$dtnow','$dtnow','$orguuid','$useruuid','$useruuid');" | psql -U $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_NAME

echo "insert into workspace_user(\"workspaceId\", \"userId\", \"roleId\", status, \"lastLogin\", \"createdDate\", \"updatedDate\", \"createdBy\", \"updatedBy\") values('$workuuid','$useruuid','\$roleid','active','$dtnow','$dtnow','$dtnow','$useruuid','$useruuid');" | psql -U $FLOWISE_DATABASE_USER $FLOWISE_DATABASE_NAME

EOFDS
  chmod +x $HSHQ_STACKS_DIR/flowise/dbexport/setupDBSettings.sh
}

function performUpdateFlowise()
{
  perform_stack_name=flowise
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/flowiseai/flowise:3.0.12,mirror.gcr.io/flowiseai/flowise-worker:3.0.12,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
      image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6"
      image_update_map[1]="mirror.gcr.io/flowiseai/flowise:3.0.12,mirror.gcr.io/flowiseai/flowise:3.1.4"
      image_update_map[2]="mirror.gcr.io/flowiseai/flowise-worker:3.0.12,mirror.gcr.io/flowiseai/flowise-worker:3.1.4"
      image_update_map[3]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[4]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    2)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:17.6,mirror.gcr.io/flowiseai/flowise:3.1.4,mirror.gcr.io/flowiseai/flowise-worker:3.1.4,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/minio:v1.0
      image_update_map[0]="mirror.gcr.io/postgres:17.6,mirror.gcr.io/postgres:17.6"
      image_update_map[1]="mirror.gcr.io/flowiseai/flowise:3.1.4,mirror.gcr.io/flowiseai/flowise:3.1.4"
      image_update_map[2]="mirror.gcr.io/flowiseai/flowise-worker:3.1.4,mirror.gcr.io/flowiseai/flowise-worker:3.1.4"
      image_update_map[3]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[4]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

# SurfSense (project is not ready for production yet)
function installSurfSense()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory surfsense "SurfSense"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName surfsense-app)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/surfsense
  mkdir $HSHQ_STACKS_DIR/surfsense/data
  initServicesCredentials
  set +e
  addUserMailu alias $SURFSENSE_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS
  SURFSENSE_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $SURFSENSE_ADMIN_PASSWORD | tr -d ':\n')
  outputConfigSurfSense
  installStack surfsense surfsense-app "Uvicorn running on http" $HOME/surfsense.env 10
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$SURFSENSE_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_SURFSENSE_APP Admin Login Info" -b "$FMLNAME_SURFSENSE_APP Admin Email: $SURFSENSE_ADMIN_EMAIL_ADDRESS\n$FMLNAME_SURFSENSE_APP Admin Password: $SURFSENSE_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    SURFSENSE_INIT_ENV=true
    updateConfigVar SURFSENSE_INIT_ENV $SURFSENSE_INIT_ENV
  fi
  sleep 3
  if [ -z "$FMLNAME_SURFSENSE_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_SURFSENSE_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://surfsense-app:3000 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_SURFSENSE_APP $MANAGETLS_SURFSENSE_APP "$is_integrate_hshq" $NETDEFAULT_SURFSENSE_APP "$inner_block"
  insertSubAuthelia $SUB_SURFSENSE_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  inner_block=""
  inner_block=$inner_block">>https://$SUB_SURFSENSE_BACKEND.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://surfsense-app:8000 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_SURFSENSE_BACKEND $MANAGETLS_SURFSENSE_BACKEND "$is_integrate_hshq" $NETDEFAULT_SURFSENSE_BACKEND "$inner_block"
  insertSubAuthelia $SUB_SURFSENSE_BACKEND.$HOMESERVER_DOMAIN bypass

  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll surfsense "$FMLNAME_SURFSENSE_APP" $USERTYPE_SURFSENSE_APP "https://$SUB_SURFSENSE_APP.$HOMESERVER_DOMAIN" "surfsense.png" "$(getHeimdallOrderFromSub $SUB_SURFSENSE_APP $USERTYPE_SURFSENSE_APP)"
    insertEnableSvcUptimeKuma surfsense "$FMLNAME_SURFSENSE_BACKEND" $USERTYPE_SURFSENSE_BACKEND "https://$SUB_SURFSENSE_BACKEND.$HOMESERVER_DOMAIN" true
    restartAllCaddyContainers
  fi
}

function outputConfigSurfSense()
{
  cat < $HOME/surfsense-compose.yml
$STACK_VERSION_PREFIX surfsense $(getScriptStackVersion surfsense)

services:
  surfsense-app:
    image: $(getScriptImageByContainerName surfsense-app)
    container_name: surfsense-app
    hostname: surfsense-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - dock-ext-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - v-surfsense-data:/data

volumes:
  v-surfsense-data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: \${PORTAINER_HSHQ_STACKS_DIR}/surfsense/data

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  dock-ext-net:
    name: dock-ext
    external: true

EOFMT
  cat < $HOME/surfsense.env
TZ=\${PORTAINER_TZ}
SECRET_KEY=$SURFSENSE_SECRET_KEY
AUTH_TYPE=LOCAL
ETL_SERVICE=DOCLING
REGISTRATION_ENABLED=TRUE
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL
NEXT_PUBLIC_FASTAPI_BACKEND_URL=https://$SUB_SURFSENSE_BACKEND.$HOMESERVER_DOMAIN/
NODE_ENV=production
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
EOFMT
}

function performUpdateSurfSense()
{
  perform_stack_name=surfsense
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v1
      curImageList=ghcr.io/modsetter/surfsense:v1.0.92
      image_update_map[0]="ghcr.io/modsetter/surfsense:v1.0.92,ghcr.io/modsetter/surfsense:v1.0.92"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

# NocoDB
function installNocoDB()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory nocodb "NocoDB"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName nocodb-db)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName nocodb-app)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName nocodb-redis)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName nocodb-minio)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/nocodb
  mkdir $HSHQ_STACKS_DIR/nocodb/data
  mkdir $HSHQ_STACKS_DIR/nocodb/db
  mkdir $HSHQ_STACKS_DIR/nocodb/dbexport
  chmod 777 $HSHQ_STACKS_DIR/nocodb/dbexport
  initServicesCredentials
  set +e
  addUserMailu alias $NOCODB_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS
  NOCODB_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $NOCODB_ADMIN_PASSWORD | tr -d ':\n')
  outputConfigNocoDB
  installStack nocodb nocodb-app "Nest application successfully started" $HOME/nocodb.env
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$NOCODB_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_NOCODB_APP Admin Login Info" -b "$FMLNAME_NOCODB_APP Admin Email: $NOCODB_ADMIN_EMAIL_ADDRESS\n$FMLNAME_NOCODB_APP Admin Password: $NOCODB_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    NOCODB_INIT_ENV=true
    updateConfigVar NOCODB_INIT_ENV $NOCODB_INIT_ENV
  fi
  sleep 3
  addReadOnlyUserToDatabase NocoDB postgres nocodb-db $NOCODB_DATABASE_NAME $NOCODB_DATABASE_USER $NOCODB_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/nocodb/dbexport $NOCODB_DATABASE_READONLYUSER $NOCODB_DATABASE_READONLYUSER_PASSWORD
  if [ -z "$FMLNAME_NOCODB_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_NOCODB_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://nocodb-app:8080 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_NOCODB_APP $MANAGETLS_NOCODB_APP "$is_integrate_hshq" $NETDEFAULT_NOCODB_APP "$inner_block"
  insertSubAuthelia $SUB_NOCODB_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll nocodb "$FMLNAME_NOCODB_APP" $USERTYPE_NOCODB_APP "https://$SUB_NOCODB_APP.$HOMESERVER_DOMAIN" "nocodb.png" "$(getHeimdallOrderFromSub $SUB_NOCODB_APP $USERTYPE_NOCODB_APP)"
    restartAllCaddyContainers
    checkAddDBConnection true nocodb "$FMLNAME_NOCODB_APP" postgres nocodb-db $NOCODB_DATABASE_NAME $NOCODB_DATABASE_USER $NOCODB_DATABASE_USER_PASSWORD
  fi
}

function outputConfigNocoDB()
{
  cat < $HOME/nocodb-compose.yml
$STACK_VERSION_PREFIX nocodb $(getScriptStackVersion nocodb)

services:
  nocodb-db:
    image: $(getScriptImageByContainerName nocodb-db)
    container_name: nocodb-db
    hostname: nocodb-db
    user: "\${PORTAINER_UID}:\${PORTAINER_GID}"
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    shm_size: 256mb
    networks:
      - int-nocodb-net
      - dock-dbs-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/nocodb/db:/var/lib/postgresql/data
      - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/nocodb/dbexport:/dbexport
    labels:
      - "ofelia.enabled=true"
      - "ofelia.job-exec.nocodb-hourly-db.schedule=@every 1h"
      - "ofelia.job-exec.nocodb-hourly-db.command=/exportDB.sh"
      - "ofelia.job-exec.nocodb-hourly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.nocodb-hourly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.nocodb-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.nocodb-hourly-db.email-from=NocoDB Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.nocodb-hourly-db.mail-only-on-error=true"
      - "ofelia.job-exec.nocodb-monthly-db.schedule=0 0 8 1 * *"
      - "ofelia.job-exec.nocodb-monthly-db.command=/exportDB.sh"
      - "ofelia.job-exec.nocodb-monthly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.nocodb-monthly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.nocodb-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.nocodb-monthly-db.email-from=NocoDB Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.nocodb-monthly-db.mail-only-on-error=false"

  nocodb-app:
    image: $(getScriptImageByContainerName nocodb-app)
    container_name: nocodb-app
    hostname: nocodb-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - nocodb-db
    networks:
      - int-nocodb-net
      - dock-ext-net
      - dock-internalmail-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - v-nocodb-data:/usr/app/data

  nocodb-redis:
    image: $(getScriptImageByContainerName nocodb-redis)
    container_name: nocodb-redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command: redis-server
      --requirepass $NOCODB_REDIS_PASSWORD
      --appendonly no
    networks:
      - int-nocodb-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

  nocodb-minio:
    image: $(getScriptImageByContainerName nocodb-minio)
    container_name: nocodb-minio
    hostname: nocodb-minio
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - nocodb-db
    command: server /data
    networks:
      - int-nocodb-net
    entrypoint: >
      /bin/sh -c "
        minio server /data --address ':9000' --console-address ':9001' &
        MINIO_PID=\\\$!
        while ! curl -s http://localhost:9000/minio/health/live; do
          echo 'Waiting for MinIO to start...'
          sleep 1
        done
        sleep 5
        mc alias set minio http://localhost:9000 ${NOCODB_MINIO_KEY} ${NOCODB_MINIO_SECRET}
        echo 'Creating bucket nocodb'
        mc mb minio/nocodb
        wait \\\$MINIO_PID
      "
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/nocodb/minio:/data

volumes:
  v-nocodb-data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: \${PORTAINER_HSHQ_STACKS_DIR}/nocodb/data

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-internalmail-net:
    name: dock-internalmail
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  dock-dbs-net:
    name: dock-dbs
    external: true
  int-nocodb-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  cat < $HOME/nocodb.env
TZ=\${PORTAINER_TZ}
POSTGRES_DB=$NOCODB_DATABASE_NAME
POSTGRES_USER=$NOCODB_DATABASE_USER
POSTGRES_PASSWORD=$NOCODB_DATABASE_USER_PASSWORD
DATABASE_URL=postgres://$NOCODB_DATABASE_USER:$NOCODB_DATABASE_USER_PASSWORD@nocodb-db/$NOCODB_DATABASE_NAME
NC_AUTH_JWT_SECRET=$NOCODB_AUTH_JWT_SECRET
NC_ADMIN_EMAIL=$NOCODB_ADMIN_EMAIL_ADDRESS
NC_ADMIN_PASSWORD=$NOCODB_ADMIN_PASSWORD
MINIO_ROOT_USER=$NOCODB_MINIO_KEY
MINIO_ROOT_PASSWORD=$NOCODB_MINIO_SECRET
NC_S3_BUCKET_NAME=nocodb
NC_S3_REGION=us-west-1
NC_S3_ENDPOINT=http://nocodb-minio:9000
NC_S3_ACCESS_KEY=$NOCODB_MINIO_KEY
NC_S3_ACCESS_SECRET=$NOCODB_MINIO_SECRET
NC_S3_FORCE_PATH_STYLE=true
NC_SMTP_FROM=NocoDB $(getAdminEmailName) <$EMAIL_ADMIN_EMAIL_ADDRESS>
NC_SMTP_HOST=$SMTP_HOSTNAME
NC_SMTP_PORT=$SMTP_HOSTPORT
NC_SMTP_SECURE=false
NC_SMTP_IGNORE_TLS=true
NC_SMTP_REJECT_UNAUTHORIZED=false
NODE_ENV=production
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
NC_PUBLIC_URL=https://$SUB_NOCODB_APP.$HOMESERVER_DOMAIN
NC_REDIS_URL=redis://:$NOCODB_REDIS_PASSWORD@nocodb-redis:6379
NC_INVITE_ONLY_SIGNUP=true
NC_DISABLE_TELE=true
NC_DISABLE_SUPPORT_CHAT=true
EOFMT
}

function performUpdateNocoDB()
{
  perform_stack_name=nocodb
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/nocodb/nocodb:0.265.1,mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
      image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm"
      image_update_map[1]="mirror.gcr.io/nocodb/nocodb:0.265.1,mirror.gcr.io/nocodb/nocodb:0.265.1"
      image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    2)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/nocodb/nocodb:0.265.1,mirror.gcr.io/valkey/valkey:alpine3.23,ghcr.io/homeserverhq/minio:v1.0
      image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm"
      image_update_map[1]="mirror.gcr.io/nocodb/nocodb:0.265.1,mirror.gcr.io/nocodb/nocodb:0.265.1"
      image_update_map[2]="mirror.gcr.io/valkey/valkey:alpine3.23,mirror.gcr.io/valkey/valkey:alpine3.23"
      image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

# Ente
function installEnte()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory ente "Ente"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName ente-server)
  if [ $? -ne 0 ]; then
    buildOrPullImage hshq/ente-server:v1
  fi
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName ente-db)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName ente-web)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName ente-minio)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/ente
  mkdir $HSHQ_STACKS_DIR/ente/config
  mkdir $HSHQ_STACKS_DIR/ente/data
  mkdir $HSHQ_STACKS_DIR/ente/minio
  mkdir $HSHQ_STACKS_DIR/ente/db
  mkdir $HSHQ_STACKS_DIR/ente/dbexport
  chmod 777 $HSHQ_STACKS_DIR/ente/dbexport
  initServicesCredentials
  set +e
  addUserMailu alias $ENTE_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS
  ENTE_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $ENTE_ADMIN_PASSWORD | tr -d ':\n')
  outputConfigEnte
  installStack ente ente-server "" $HOME/ente.env
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$ENTE_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_ENTE_APP Admin Login Info" -b "$FMLNAME_ENTE_APP Admin Username: $ENTE_ADMIN_USERNAME\n$FMLNAME_ENTE_APP Admin Email: $ENTE_ADMIN_EMAIL_ADDRESS\n$FMLNAME_ENTE_APP Admin Password: $ENTE_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    ENTE_INIT_ENV=true
    updateConfigVar ENTE_INIT_ENV $ENTE_INIT_ENV
  fi
  sleep 3
  addReadOnlyUserToDatabase Ente postgres ente-db $ENTE_DATABASE_NAME $ENTE_DATABASE_USER $ENTE_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/ente/dbexport $ENTE_DATABASE_READONLYUSER $ENTE_DATABASE_READONLYUSER_PASSWORD
  if [ -z "$FMLNAME_ENTE_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_ENTE_SERVER.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://ente-server:8080 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_ENTE_SERVER $MANAGETLS_ENTE_SERVER "$is_integrate_hshq" $NETDEFAULT_ENTE_SERVER "$inner_block"
  insertSubAuthelia $SUB_ENTE_SERVER.$HOMESERVER_DOMAIN bypass
  inner_block=""
  inner_block=$inner_block">>https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://ente-web:3000 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_ENTE_APP $MANAGETLS_ENTE_APP "$is_integrate_hshq" $NETDEFAULT_ENTE_APP "$inner_block"
  insertSubAuthelia $SUB_ENTE_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll ente "$FMLNAME_ENTE_APP" $USERTYPE_ENTE_APP "https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN" "ente.png" "$(getHeimdallOrderFromSub $SUB_ENTE_APP $USERTYPE_ENTE_APP)"
    restartAllCaddyContainers
    checkAddDBConnection true ente "$FMLNAME_ENTE_APP" postgres ente-db $ENTE_DATABASE_NAME $ENTE_DATABASE_USER $ENTE_DATABASE_USER_PASSWORD
  fi
}

function outputConfigEnte()
{
  cat < $HOME/ente-compose.yml
$STACK_VERSION_PREFIX ente $(getScriptStackVersion ente)

services:
  ente-db:
    image: $(getScriptImageByContainerName ente-db)
    container_name: ente-db
    hostname: ente-db
    user: "\${PORTAINER_UID}:\${PORTAINER_GID}"
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    shm_size: 256mb
    networks:
      - int-ente-net
      - dock-dbs-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/ente/db:/var/lib/postgresql/data
      - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/ente/dbexport:/dbexport
    labels:
      - "ofelia.enabled=true"
      - "ofelia.job-exec.ente-hourly-db.schedule=@every 1h"
      - "ofelia.job-exec.ente-hourly-db.command=/exportDB.sh"
      - "ofelia.job-exec.ente-hourly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.ente-hourly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.ente-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.ente-hourly-db.email-from=Ente Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.ente-hourly-db.mail-only-on-error=true"
      - "ofelia.job-exec.ente-monthly-db.schedule=0 0 8 1 * *"
      - "ofelia.job-exec.ente-monthly-db.command=/exportDB.sh"
      - "ofelia.job-exec.ente-monthly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.ente-monthly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.ente-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.ente-monthly-db.email-from=Ente Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.ente-monthly-db.mail-only-on-error=false"

  ente-server:
    image: $(getScriptImageByContainerName ente-server)
    container_name: ente-server
    hostname: ente-server
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - ente-db
    networks:
      - int-ente-net
      - dock-ext-net
      - dock-internalmail-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/ente/data:/data:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/ente/config/museum.yaml:/museum.yaml:ro

  ente-web:
    image: $(getScriptImageByContainerName ente-web)
    container_name: ente-web
    hostname: ente-web
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      - ente-db
    networks:
      - int-ente-net
      - dock-ext-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  ente-minio:
    image: $(getScriptImageByContainerName ente-minio)
    container_name: ente-minio
    hostname: ente-minio
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-ente-net
    entrypoint: >
      /bin/sh -c "
        minio server /data --address ':3200' --console-address ':9001' &
        MINIO_PID=\\\$!
        while ! curl -s http://localhost:3200/minio/health/live; do
          echo 'Waiting for MinIO to start...'
          sleep 1
        done
        sleep 5
        mc alias set h0 http://ente-minio:3200 ${ENTE_MINIO_KEY} ${ENTE_MINIO_SECRET}
        echo 'Creating buckets'
        cd /data
        mc mb -p b2-eu-cen
        mc mb -p wasabi-eu-central-2-v3
        mc mb -p scw-eu-fr-v3
        wait \\\$MINIO_PID
      "
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/ente/minio:/data

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-internalmail-net:
    name: dock-internalmail
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  dock-dbs-net:
    name: dock-dbs
    external: true
  int-ente-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  cat < $HOME/ente.env
TZ=\${PORTAINER_TZ}
POSTGRES_DB=$ENTE_DATABASE_NAME
POSTGRES_USER=$ENTE_DATABASE_USER
POSTGRES_PASSWORD=$ENTE_DATABASE_USER_PASSWORD
ENTE_API_ORIGIN=https://$SUB_ENTE_SERVER.$HOMESERVER_DOMAIN
ENTE_PHOTOS_ORIGIN=https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN
MINIO_ROOT_USER=$ENTE_MINIO_KEY
MINIO_ROOT_PASSWORD=$ENTE_MINIO_SECRET
ENTE_CREDENTIALS_FILE=/museum.yaml
EOFMT
  cat < $HSHQ_STACKS_DIR/ente/config/museum.yaml
db:
  host: ente-db
  port: 5432
  name: $ENTE_DATABASE_NAME
  user: $ENTE_DATABASE_USER
  password: $ENTE_DATABASE_USER_PASSWORD

s3:
  are_local_buckets: true
  use_path_style_urls: true
  b2-eu-cen:
    key: $ENTE_MINIO_KEY
    secret: $ENTE_MINIO_SECRET
    endpoint: http://ente-minio:3200
    region: eu-central-2
    bucket: b2-eu-cen
  wasabi-eu-central-2-v3:
    key: $ENTE_MINIO_KEY
    secret: $ENTE_MINIO_SECRET
    endpoint: http://ente-minio:3200
    region: eu-central-2
    bucket: wasabi-eu-central-2-v3
  scw-eu-fr-v3:
    key: $ENTE_MINIO_KEY
    secret: $ENTE_MINIO_SECRET
    endpoint: http://ente-minio:3200
    region: eu-central-2
    bucket: scw-eu-fr-v3

apps:
  public-albums: https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN
  cast: https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN
  public-locker: https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN
  embed-albums: https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN
  accounts: https://$SUB_ENTE_APP.$HOMESERVER_DOMAIN

key:
  encryption: $ENTE_MUSEUM_KEY
  hash: $ENTE_MUSEUM_HASH

jwt:
  secret: $ENTE_MUSEUM_JWT_SECRET

smtp:
  host: $SMTP_HOSTNAME
  port: $SMTP_HOSTPORT
  username:
  password:
  email: $EMAIL_ADMIN_EMAIL_ADDRESS
  sender-name: Ente $(getAdminEmailName)
  encryption: starttls
EOFMT
}

function performUpdateEnte()
{
  perform_stack_name=ente
  prepPerformUpdate
  if [ $? -ne 0 ]; then return 1; fi
  # The current version is included as a placeholder for when the next version arrives.
  case "$perform_stack_ver" in
    1)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:16.9-bookworm,hshq/ente-server:v1,ghcr.io/ente-io/web:460ee1671b08b119b894f0ddd71b4c906fb29647,mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
      image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm"
      image_update_map[1]="hshq/ente-server:v1,hshq/ente-server:v1"
      image_update_map[2]="ghcr.io/ente-io/web:460ee1671b08b119b894f0ddd71b4c906fb29647,ghcr.io/ente-io/web:460ee1671b08b119b894f0ddd71b4c906fb29647"
      image_update_map[3]="mirror.gcr.io/minio/minio:RELEASE.2025-09-07T16-13-09Z,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    2)
      newVer=v2
      curImageList=mirror.gcr.io/postgres:16.9-bookworm,hshq/ente-server:v1,ghcr.io/ente-io/web:460ee1671b08b119b894f0ddd71b4c906fb29647,ghcr.io/homeserverhq/minio:v1.0
      image_update_map[0]="mirror.gcr.io/postgres:16.9-bookworm,mirror.gcr.io/postgres:16.9-bookworm"
      image_update_map[1]="hshq/ente-server:v1,hshq/ente-server:v1"
      image_update_map[2]="ghcr.io/ente-io/web:460ee1671b08b119b894f0ddd71b4c906fb29647,ghcr.io/ente-io/web:460ee1671b08b119b894f0ddd71b4c906fb29647"
      image_update_map[3]="ghcr.io/homeserverhq/minio:v1.0,ghcr.io/homeserverhq/minio:v1.0"
    ;;
    *)
      is_upgrade_error=true
      perform_update_report="ERROR ($perform_stack_name): Unknown version (v$perform_stack_ver)"
      return
    ;;
  esac
  upgradeStack "$perform_stack_name" "$perform_stack_id" "$oldVer" "$newVer" "$curImageList" "$perform_compose" doNothing false
  perform_update_report="${perform_update_report}$stack_upgrade_report"
}

function buildImageEnteServerV1()
{
  set +e
  sudo rm -fr $HSHQ_BUILD_DIR/ente
  cd $HSHQ_BUILD_DIR
  git clone https://github.com/ente-io/ente.git
  cd ente/server
  docker image build -t hshq/ente-server:v1 .
  rtval=$?
  cd
  sudo rm -fr $HSHQ_BUILD_DIR/ente
  return $rtval
}

# Morphic
function installMorphic()
{
  set +e
  is_integrate_hshq=$1
  checkDeleteStackAndDirectory morphic "Morphic"
  cdRes=$?
  if [ $cdRes -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-db)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-vector)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-studio)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-kong)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-auth)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-rest)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-realtime)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-storage)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-imgproxy)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-meta)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-functions)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-analytics)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-supabase-supavisor)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-app)
  if [ $? -ne 0 ]; then
    return 1
  fi
  pullImage $(getScriptImageByContainerName morphic-redis)
  if [ $? -ne 0 ]; then
    return 1
  fi
  set -e
  mkdir $HSHQ_STACKS_DIR/morphic
  mkdir $HSHQ_STACKS_DIR/morphic/config
  mkdir $HSHQ_STACKS_DIR/morphic/storage
  mkdir $HSHQ_STACKS_DIR/morphic/functions
  mkdir $HSHQ_STACKS_DIR/morphic/functions/hello
  mkdir $HSHQ_STACKS_DIR/morphic/functions/main
  mkdir $HSHQ_STACKS_DIR/morphic/dbimport
  mkdir $HSHQ_STACKS_DIR/morphic/dbconfig
  mkdir $HSHQ_STACKS_DIR/morphic/db
  mkdir $HSHQ_STACKS_DIR/morphic/dbexport
  chmod 777 $HSHQ_STACKS_DIR/morphic/dbexport
  mkdir $HSHQ_STACKS_DIR/morphic/redis
  initServicesCredentials
  set +e
  addUserMailu alias $MORPHIC_ADMIN_USERNAME $HOMESERVER_DOMAIN $EMAIL_ADMIN_EMAIL_ADDRESS
  MORPHIC_ADMIN_PASSWORD_HASH=$(htpasswd -bnBC 10 "" $MORPHIC_ADMIN_PASSWORD | tr -d ':\n')
  outputConfigMorphic
  installStack morphic morphic-app "Ready in" $HOME/morphic.env
  retVal=$?
  if [ $retVal -ne 0 ]; then
    return $retVal
  fi
  if ! [ "$MORPHIC_INIT_ENV" = "true" ]; then
    sendEmail -s "$FMLNAME_MORPHIC_APP Admin Login Info" -b "$FMLNAME_MORPHIC_APP Admin Username: $MORPHIC_ADMIN_USERNAME\n$FMLNAME_MORPHIC_APP Admin Email: $MORPHIC_ADMIN_EMAIL_ADDRESS\n$FMLNAME_MORPHIC_APP Admin Password: $MORPHIC_ADMIN_PASSWORD\n" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>"
    MORPHIC_INIT_ENV=true
    updateConfigVar MORPHIC_INIT_ENV $MORPHIC_INIT_ENV
  fi
  sleep 3
  addReadOnlyUserToDatabase Morphic postgres morphic-db $MORPHIC_DATABASE_NAME $MORPHIC_DATABASE_USER $MORPHIC_DATABASE_USER_PASSWORD $HSHQ_STACKS_DIR/morphic/dbexport $MORPHIC_DATABASE_READONLYUSER $MORPHIC_DATABASE_READONLYUSER_PASSWORD
  if [ -z "$FMLNAME_MORPHIC_APP" ]; then
    set +e
    echo "ERROR: Formal name is empty, returning..."
    return 1
  fi
  set -e
  inner_block=""
  inner_block=$inner_block">>https://$SUB_MORPHIC_APP.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://morphic-app:3000 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_MORPHIC_APP $MANAGETLS_MORPHIC_APP "$is_integrate_hshq" $NETDEFAULT_MORPHIC_APP "$inner_block"
  insertSubAuthelia $SUB_MORPHIC_APP.$HOMESERVER_DOMAIN ${LDAP_PRIMARY_USER_GROUP_NAME}
  inner_block=""
  inner_block=$inner_block">>https://$SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN {\n"
  inner_block=$inner_block">>>>REPLACE-TLS-BLOCK\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_RIP\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_FWDAUTH\n"
  inner_block=$inner_block">>>>import $CADDY_SNIPPET_SAFEHEADER\n"
  inner_block=$inner_block">>>>handle @subnet {\n"
  inner_block=$inner_block">>>>>>import $CADDY_SNIPPET_INTERNAL_GUARD\n"
  inner_block=$inner_block">>>>>>reverse_proxy http://morphic-supabase-kong:8000 {\n"
  inner_block=$inner_block">>>>>>>>import $CADDY_SNIPPET_TRUSTEDPROXIES\n"
  inner_block=$inner_block">>>>>>}\n"
  inner_block=$inner_block">>>>}\n"
  inner_block=$inner_block">>>>respond 404\n"
  inner_block=$inner_block">>}"
  updateCaddyBlocks $SUB_MORPHIC_SUPABASE $MANAGETLS_MORPHIC_SUPABASE "$is_integrate_hshq" $NETDEFAULT_MORPHIC_SUPABASE "$inner_block"
  insertSubAuthelia $SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN ${LDAP_ADMIN_USER_GROUP_NAME}
  if ! [ "$is_integrate_hshq" = "false" ]; then
    insertEnableSvcAll morphic "$FMLNAME_MORPHIC_APP" $USERTYPE_MORPHIC_APP "https://$SUB_MORPHIC_APP.$HOMESERVER_DOMAIN" "morphic.png" "$(getHeimdallOrderFromSub $SUB_MORPHIC_APP $USERTYPE_MORPHIC_APP)"
    insertEnableSvcHeimdall morphic "$FMLNAME_MORPHIC_SUPABASE" $USERTYPE_MORPHIC_SUPABASE "https://$SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN" "supabase.png" true "$(getHeimdallOrderFromSub $SUB_MORPHIC_SUPABASE $USERTYPE_MORPHIC_SUPABASE)"
    restartAllCaddyContainers
    checkAddDBConnection true morphic "$FMLNAME_MORPHIC_APP" postgres morphic-db $MORPHIC_DATABASE_NAME $MORPHIC_DATABASE_USER $MORPHIC_DATABASE_USER_PASSWORD
  fi
}

function outputConfigMorphic()
{
  cat < $HOME/morphic-compose.yml
$STACK_VERSION_PREFIX morphic $(getScriptStackVersion morphic)

services:
  morphic-db:
    image: $(getScriptImageByContainerName morphic-db)
    container_name: morphic-db
    hostname: morphic-db
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-morphic-net
      - dock-dbs-net
    depends_on:
      morphic-supabase-vector:
        condition: service_started
    command:
      [
        "postgres",
        "-c",
        "config_file=/etc/postgresql/postgresql.conf",
        "-c",
        "log_min_messages=fatal"
      ]
    healthcheck:
      test:
        [
        "CMD",
        "pg_isready",
        "-U",
        "postgres",
        "-h",
        "localhost"
        ]
      interval: 5s
      timeout: 5s
      retries: 10
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/db:/var/lib/postgresql/data
      - \${PORTAINER_HSHQ_SCRIPTS_DIR}/user/exportPostgres.sh:/exportDB.sh:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/dbexport:/dbexport
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/dbimport/01-init.sql:/docker-entrypoint-initdb.d/init-scripts/01-init.sql
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/dbimport/11-migrations.sql:/docker-entrypoint-initdb.d/migrations/11-migrations.sql
      - v-morphic-dbconfig:/etc/postgresql-custom
    labels:
      - "ofelia.enabled=true"
      - "ofelia.job-exec.morphic-hourly-db.schedule=@every 1h"
      - "ofelia.job-exec.morphic-hourly-db.command=/exportDB.sh"
      - "ofelia.job-exec.morphic-hourly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.morphic-hourly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.morphic-hourly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.morphic-hourly-db.email-from=Morphic Hourly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.morphic-hourly-db.mail-only-on-error=true"
      - "ofelia.job-exec.morphic-monthly-db.schedule=0 0 8 1 * *"
      - "ofelia.job-exec.morphic-monthly-db.command=/exportDB.sh"
      - "ofelia.job-exec.morphic-monthly-db.smtp-host=$SMTP_HOSTNAME"
      - "ofelia.job-exec.morphic-monthly-db.smtp-port=$SMTP_HOSTPORT"
      - "ofelia.job-exec.morphic-monthly-db.email-to=$EMAIL_ADMIN_EMAIL_ADDRESS"
      - "ofelia.job-exec.morphic-monthly-db.email-from=Morphic Monthly DB Export <$EMAIL_ADMIN_EMAIL_ADDRESS>"
      - "ofelia.job-exec.morphic-monthly-db.mail-only-on-error=false"
    environment:
      - POSTGRES_HOST=/var/run/postgresql

  morphic-supabase-vector:
    image: $(getScriptImageByContainerName morphic-supabase-vector)
    container_name: morphic-supabase-vector
    hostname: morphic-supabase-vector
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - label=disable
    networks:
      - int-morphic-net
      - dock-dbs-net
    command:
      [
        "--config",
        "/etc/vector/vector.yml"
      ]
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "--no-verbose",
          "--tries=1",
          "--spider",
          "http://morphic-supabase-vector:9001/health"
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/config/vector.yml:/etc/vector/vector.yml:ro,z
      - /var/run/docker.sock:/var/run/docker.sock:ro,z

  morphic-supabase-studio:
    image: $(getScriptImageByContainerName morphic-supabase-studio)
    container_name: morphic-supabase-studio
    hostname: morphic-supabase-studio
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-supabase-analytics:
        condition: service_healthy
    networks:
      - int-morphic-net
      - dock-ext-net
    healthcheck:
      test:
        [
          "CMD",
          "node",
          "-e",
          "fetch('http://morphic-supabase-studio:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})"
        ]
      timeout: 10s
      interval: 5s
      retries: 3
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  morphic-supabase-kong:
    image: $(getScriptImageByContainerName morphic-supabase-kong)
    container_name: morphic-supabase-kong
    hostname: morphic-supabase-kong
    restart: unless-stopped
    env_file: stack.env
    depends_on:
      morphic-supabase-analytics:
        condition: service_healthy
    entrypoint: bash -c 'eval "echo \"\$\$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
    networks:
      - int-morphic-net
      - dock-ext-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/config/kong.yml:/home/kong/temp.yml:ro,z

  morphic-supabase-auth:
    image: $(getScriptImageByContainerName morphic-supabase-auth)
    container_name: morphic-supabase-auth
    hostname: morphic-supabase-auth
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-db:
        condition: service_healthy
      morphic-supabase-analytics:
        condition: service_healthy
    networks:
      - int-morphic-net
      - dock-ext-net
      - dock-internalmail-net
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "--no-verbose",
          "--tries=1",
          "--spider",
          "http://localhost:9999/health"
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  morphic-supabase-rest:
    image: $(getScriptImageByContainerName morphic-supabase-rest)
    container_name: morphic-supabase-rest
    hostname: morphic-supabase-rest
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-db:
        condition: service_healthy
      morphic-supabase-analytics:
        condition: service_healthy
    command:
      [
        "postgrest"
      ]
    networks:
      - int-morphic-net
      - dock-ext-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  morphic-supabase-realtime:
    image: $(getScriptImageByContainerName morphic-supabase-realtime)
    container_name: morphic-supabase-realtime
    hostname: morphic-supabase-realtime
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-db:
        condition: service_healthy
      morphic-supabase-analytics:
        condition: service_healthy
    networks:
      - int-morphic-net
      - dock-ext-net
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "curl -sSfL --head -o /dev/null -H \"Authorization: Bearer \${ANON_KEY}\" http://localhost:4000/api/tenants/realtime-dev/health"
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
    environment:
      - PORT=4000

  morphic-supabase-storage:
    image: $(getScriptImageByContainerName morphic-supabase-storage)
    container_name: morphic-supabase-storage
    hostname: morphic-supabase-storage
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-db:
        condition: service_healthy
      morphic-supabase-rest:
        condition: service_started
      morphic-supabase-imgproxy:
        condition: service_started
    networks:
      - int-morphic-net
      - dock-ext-net
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "--no-verbose",
          "--tries=1",
          "--spider",
          "http://morphic-supabase-storage:5000/status"
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/storage:/var/lib/storage:z
    environment:
      - DATABASE_URL=postgres://supabase_storage_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB}
      - REGION=stub

  morphic-supabase-imgproxy:
    image: $(getScriptImageByContainerName morphic-supabase-imgproxy)
    container_name: morphic-supabase-imgproxy
    hostname: morphic-supabase-imgproxy
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    networks:
      - int-morphic-net
      - dock-ext-net
    healthcheck:
      test:
        [
          "CMD",
          "imgproxy",
          "health"
        ]
      timeout: 5s
      interval: 5s
      retries: 3
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/storage:/var/lib/storage:z

  morphic-supabase-meta:
    image: $(getScriptImageByContainerName morphic-supabase-meta)
    container_name: morphic-supabase-meta
    hostname: morphic-supabase-meta
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-db:
        condition: service_healthy
      morphic-supabase-analytics:
        condition: service_healthy
    networks:
      - int-morphic-net
      - dock-ext-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  morphic-supabase-functions:
    image: $(getScriptImageByContainerName morphic-supabase-functions)
    container_name: morphic-supabase-functions
    hostname: morphic-supabase-functions
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-supabase-analytics:
        condition: service_healthy
    command:
      [
        "start",
        "--main-service",
        "/home/deno/functions/main"
      ]
    networks:
      - int-morphic-net
      - dock-ext-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/functions:/home/deno/functions:Z

  morphic-supabase-analytics:
    image: $(getScriptImageByContainerName morphic-supabase-analytics)
    container_name: morphic-supabase-analytics
    hostname: morphic-supabase-analytics
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-db:
        condition: service_healthy
    networks:
      - int-morphic-net
      - dock-ext-net
    healthcheck:
      test:
        [
          "CMD",
          "curl",
          "http://localhost:4000/health"
        ]
      timeout: 5s
      interval: 5s
      retries: 10
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  morphic-supabase-supavisor:
    image: $(getScriptImageByContainerName morphic-supabase-supavisor)
    container_name: morphic-supabase-supavisor
    hostname: morphic-supabase-supavisor
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    depends_on:
      morphic-db:
        condition: service_healthy
      morphic-supabase-analytics:
        condition: service_healthy
    networks:
      - int-morphic-net
      - dock-ext-net
    healthcheck:
      test:
        [
          "CMD",
          "curl",
          "-sSfL",
          "--head",
          "-o",
          "/dev/null",
          "http://127.0.0.1:4000/api/health"
        ]
      interval: 10s
      timeout: 5s
      retries: 5
    command:
      [
        "/bin/sh",
        "-c",
        "/app/bin/migrate && /app/bin/supavisor eval \"\$\$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
      ]
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro
      - \${PORTAINER_HSHQ_STACKS_DIR}/morphic/config/pooler.exs:/etc/pooler/pooler.exs:ro,z
    environment:
      - DATABASE_URL=ecto://supabase_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/_supabase

  morphic-app:
    image: $(getScriptImageByContainerName morphic-app)
    container_name: morphic-app
    hostname: morphic-app
    restart: unless-stopped
    env_file: stack.env
    security_opt:
      - no-new-privileges:true
    command: bun start -H 0.0.0.0
    depends_on:
      morphic-db:
        condition: service_healthy
      morphic-redis:
        condition: service_started
    networks:
      - int-morphic-net
      - dock-ext-net
      - dock-aipriv-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /usr/share/ca-certificates:/usr/share/ca-certificates:ro
      - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro

  morphic-redis:
    image: $(getScriptImageByContainerName morphic-redis)
    container_name: morphic-redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command: redis-server
      --requirepass $MORPHIC_REDIS_PASSWORD
      --appendonly yes
    networks:
      - int-morphic-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
      - v-morphic-redis:/data

volumes:
  v-morphic-dbconfig:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: \${PORTAINER_HSHQ_STACKS_DIR}/morphic/dbconfig
  v-morphic-redis:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: \${PORTAINER_HSHQ_STACKS_DIR}/morphic/redis

networks:
  dock-proxy-net:
    name: dock-proxy
    external: true
  dock-internalmail-net:
    name: dock-internalmail
    external: true
  dock-ext-net:
    name: dock-ext
    external: true
  dock-dbs-net:
    name: dock-dbs
    external: true
  dock-aipriv-net:
    name: dock-aipriv
    external: true
  int-morphic-net:
    driver: bridge
    internal: true
    ipam:
      driver: default

EOFMT
  cat < $HOME/morphic.env
TZ=\${PORTAINER_TZ}
NODE_ENV=production
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
BASE_URL=https://$SUB_MORPHIC_APP.$HOMESERVER_DOMAIN
ENABLE_SAVE_CHAT_HISTORY=true
USE_LOCAL_REDIS=true
LOCAL_REDIS_URL=redis://:$MORPHIC_REDIS_PASSWORD@morphic-redis:6379
OPENAI_COMPATIBLE_API_KEY=$LITELLM_AUTH_KEY
OPENAI_COMPATIBLE_API_BASE_URL=http://litellm-proxy:4000
OPENAI_API_KEY=$LITELLM_AUTH_KEY
OPENAI_BASE_URL=http://litellm-proxy:4000
SEARCH_API=searxng
SEARXNG_API_URL=http://searxng-app:8080
SEARXNG_SECRET=$SEARXNG_SECRET_KEY
SEARXNG_PORT=8080
SEARXNG_BIND_ADDRESS=0.0.0.0
SEARXNG_IMAGE_PROXY=true
SEARXNG_LIMITER=false
SEARXNG_DEFAULT_DEPTH=basic
SEARXNG_MAX_RESULTS=50
SEARXNG_ENGINES=google,startpage,wolframalpha,duckduckgo,yahoo,wikipedia
SEARXNG_TIME_RANGE=None
SEARXNG_SAFESEARCH=0
NEXT_PUBLIC_ENABLE_SHARE=true
NEXT_PUBLIC_SUPABASE_URL=https://$SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN
NEXT_PUBLIC_SUPABASE_ANON_KEY=$MORPHIC_SUPABASE_ANON_KEY
KONG_NGINX_WORKER_PROCESSES=4
POSTGRES_DB=$MORPHIC_DATABASE_NAME
POSTGRES_USER=$MORPHIC_DATABASE_USER
POSTGRES_PASSWORD=$MORPHIC_DATABASE_USER_PASSWORD
ADDITIONAL_REDIRECT_URLS=
ANON_KEY=$MORPHIC_SUPABASE_ANON_KEY
API_EXTERNAL_URL=https://$SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN
API_JWT_SECRET=\${JWT_SECRET}
APP_NAME=realtime
AUTH_JWT_SECRET=\${JWT_SECRET}
CLUSTER_POSTGRES=1
CRYPTO_KEY=\${PG_META_CRYPTO_KEY}
DASHBOARD_PASSWORD=$MORPHIC_SUPABASE_DASHBOARD_PASSWORD
DASHBOARD_USERNAME=$MORPHIC_SUPABASE_DASHBOARD_USERNAME
DB_AFTER_CONNECT_QUERY='SET search_path TO _realtime'
DB_DATABASE=_supabase
DB_ENC_KEY=$MORPHIC_SUPABASE_DB_ENC_KEY
DB_HOST=\${POSTGRES_HOST}
DB_HOSTNAME=\${POSTGRES_HOST}
DB_NAME=\${POSTGRES_DB}
DB_PASSWORD=\${POSTGRES_PASSWORD}
DB_POOL_SIZE=\${POOLER_DB_POOL_SIZE}
DB_PORT=\${POSTGRES_PORT}
DB_SCHEMA=_analytics
DB_USER=\${POSTGRES_USER}
DB_USERNAME=\${POSTGRES_USER}
DEFAULT_ORGANIZATION_NAME=\${STUDIO_DEFAULT_ORGANIZATION}
DEFAULT_PROJECT_NAME=\${STUDIO_DEFAULT_PROJECT}
DISABLE_SIGNUP=0
DNS_NODES=''
DOCKER_SOCKET_LOCATION=/var/run/docker.sock
ENABLE_ANONYMOUS_USERS=0
ENABLE_EMAIL_AUTOCONFIRM=1
ENABLE_EMAIL_SIGNUP=1
ENABLE_IMAGE_TRANSFORMATION=true
ENABLE_PHONE_AUTOCONFIRM=1
ENABLE_PHONE_SIGNUP=1
ERL_AFLAGS=-proto_dist inet_tcp
FILE_SIZE_LIMIT=52428800
FILE_STORAGE_BACKEND_PATH=/var/lib/storage
FUNCTIONS_VERIFY_JWT=0
GLOBAL_S3_BUCKET=stub
GOOGLE_PROJECT_ID=
GOOGLE_PROJECT_NUMBER=
GOTRUE_API_HOST=0.0.0.0
GOTRUE_API_PORT=9999
GOTRUE_DB_DATABASE_URL=postgres://supabase_auth_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB}
GOTRUE_DB_DRIVER=postgres
GOTRUE_DISABLE_SIGNUP=\${DISABLE_SIGNUP}
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=\${ENABLE_ANONYMOUS_USERS}
GOTRUE_EXTERNAL_EMAIL_ENABLED=\${ENABLE_EMAIL_SIGNUP}
GOTRUE_EXTERNAL_PHONE_ENABLED=\${ENABLE_PHONE_SIGNUP}
GOTRUE_JWT_ADMIN_ROLES=service_role
GOTRUE_JWT_AUD=authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated
GOTRUE_JWT_EXP=\${JWT_EXPIRY}
GOTRUE_JWT_SECRET=\${JWT_SECRET}
GOTRUE_MAILER_AUTOCONFIRM=\${ENABLE_EMAIL_AUTOCONFIRM}
GOTRUE_MAILER_URLPATHS_CONFIRMATION=\${MAILER_URLPATHS_CONFIRMATION}
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=\${MAILER_URLPATHS_EMAIL_CHANGE}
GOTRUE_MAILER_URLPATHS_INVITE=\${MAILER_URLPATHS_INVITE}
GOTRUE_MAILER_URLPATHS_RECOVERY=\${MAILER_URLPATHS_RECOVERY}
GOTRUE_SITE_URL=\${SITE_URL}
GOTRUE_SMS_AUTOCONFIRM=\${ENABLE_PHONE_AUTOCONFIRM}
GOTRUE_SMTP_ADMIN_EMAIL=\${SMTP_ADMIN_EMAIL}
GOTRUE_SMTP_HOST=\${SMTP_HOST}
GOTRUE_SMTP_PASS=\${SMTP_PASS}
GOTRUE_SMTP_PORT=\${SMTP_PORT}
GOTRUE_SMTP_SENDER_NAME=\${SMTP_SENDER_NAME}
GOTRUE_SMTP_USER=\${SMTP_USER}
GOTRUE_URI_ALLOW_LIST=\${ADDITIONAL_REDIRECT_URLS}
HOSTNAME=::
IMGPROXY_BIND=:5001
IMGPROXY_AUTO_WEBP=true
IMGPROXY_JPEG_PROGRESSIVE=true
IMGPROXY_LOCAL_FILESYSTEM_ROOT=/
IMGPROXY_URL=http://morphic-supabase-imgproxy:5001
IMGPROXY_USE_ETAG=1
JWT_EXP=\${JWT_EXPIRY}
JWT_EXPIRY=3600
JWT_SECRET=$MORPHIC_SUPABASE_JWT_SECRET
KONG_DATABASE=off
KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml
KONG_DNS_ORDER=LAST,A,CNAME
KONG_HTTP_PORT=8000
KONG_HTTPS_PORT=8443
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k
KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k
KONG_PLUGINS=request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction
LOGFLARE_API_KEY=\${LOGFLARE_PUBLIC_ACCESS_TOKEN}
LOGFLARE_FEATURE_FLAG_OVERRIDE=multibackend=TRUE
LOGFLARE_NODE_HOST=127.0.0.1
LOGFLARE_PRIVATE_ACCESS_TOKEN=$MORPHIC_SUPABASE_LOGFLARE_PRIVATE_ACCESS_TOKEN
LOGFLARE_PUBLIC_ACCESS_TOKEN=$MORPHIC_SUPABASE_LOGFLARE_PUBLIC_ACCESS_TOKEN
LOGFLARE_SINGLE_TENANT=1
LOGFLARE_SUPABASE_MODE=1
LOGFLARE_URL=http://morphic-supabase-analytics:4000
MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify
MAILER_URLPATHS_INVITE=/auth/v1/verify
MAILER_URLPATHS_RECOVERY=/auth/v1/verify
METRICS_JWT_SECRET=\${JWT_SECRET}
NEXT_ANALYTICS_BACKEND_PROVIDER=postgres
NEXT_PUBLIC_ENABLE_LOGS=1
PG_META_CRYPTO_KEY=$MORPHIC_SUPABASE_PG_META_CRYPTO_KEY
PG_META_DB_HOST=\${POSTGRES_HOST}
PG_META_DB_NAME=\${POSTGRES_DB}
PG_META_DB_PASSWORD=\${POSTGRES_PASSWORD}
PG_META_DB_PORT=\${POSTGRES_PORT}
PG_META_DB_USER=\${POSTGRES_USER}
PG_META_PORT=8080
PGDATABASE=\${POSTGRES_DB}
PGPASSWORD=\${POSTGRES_PASSWORD}
PGPORT=\${POSTGRES_PORT}
PGRST_APP_SETTINGS_JWT_EXP=\${JWT_EXPIRY}
PGRST_APP_SETTINGS_JWT_SECRET=\${JWT_SECRET}
PGRST_DB_ANON_ROLE=anon
PGRST_DB_SCHEMAS=public,storage,graphql_public
PGRST_DB_URI=postgres://authenticator:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB}
PGRST_DB_USE_LEGACY_GUCS=0
PGRST_JWT_SECRET=\${JWT_SECRET}
POOLER_DB_POOL_SIZE=5
POOLER_DEFAULT_POOL_SIZE=20
POOLER_MAX_CLIENT_CONN=100
POOLER_POOL_MODE=transaction
POOLER_PROXY_PORT_TRANSACTION=6543
POOLER_TENANT_ID=your-tenant-id
POSTGRES_BACKEND_SCHEMA=_analytics
POSTGRES_BACKEND_URL=postgresql://\${POSTGRES_USER}:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${DB_DATABASE}
POSTGRES_HOST=morphic-db
POSTGRES_PORT=5432
POSTGREST_URL=http://morphic-supabase-rest:3000
REGION=local
REQUEST_ALLOW_X_FORWARDED_PATH=1
RLIMIT_NOFILE=10000
RUN_JANITOR=true
SECRET_KEY_BASE=$MORPHIC_SUPABASE_SECRET_KEY_BASE
SEED_SELF_HOST=true
SERVICE_KEY=\${SERVICE_ROLE_KEY}
SERVICE_ROLE_KEY=$MORPHIC_SUPABASE_SERVICE_ROLE_KEY
SITE_URL=https://$SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN
SMTP_ADMIN_EMAIL=$EMAIL_ADMIN_EMAIL_ADDRESS
SMTP_HOST=$SMTP_HOSTNAME
SMTP_PASS=
SMTP_PORT=$SMTP_HOSTPORT
SMTP_SENDER_NAME=Morphic Supabase $(getAdminEmailName)
SMTP_USER=
STORAGE_BACKEND=file
STUDIO_DEFAULT_ORGANIZATION=$HOMESERVER_NAME
STUDIO_DEFAULT_PROJECT=Morphic
STUDIO_PG_META_URL=http://morphic-supabase-meta:8080
SUPABASE_ANON_KEY=\${ANON_KEY}
SUPABASE_DB_URL=postgresql://postgres:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB}
SUPABASE_PUBLIC_URL=https://$SUB_MORPHIC_SUPABASE.$HOMESERVER_DOMAIN
SUPABASE_SERVICE_KEY=\${SERVICE_ROLE_KEY}
SUPABASE_SERVICE_ROLE_KEY=\${SERVICE_ROLE_KEY}
SUPABASE_URL=http://morphic-supabase-kong:8000
TENANT_ID=stub
VAULT_ENC_KEY=$MORPHIC_SUPABASE_VAULT_ENC_KEY
VERIFY_JWT=\${FUNCTIONS_VERIFY_JWT}
EOFMT
  cat < $HSHQ_STACKS_DIR/morphic/dbimport/01-init.sql
-- webhooks.sql
BEGIN;
  -- Create pg_net extension
  CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
  -- Create supabase_functions schema
  CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
  GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
  ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
  ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
  ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
  -- supabase_functions.migrations definition
  CREATE TABLE supabase_functions.migrations (
    version text PRIMARY KEY,
    inserted_at timestamptz NOT NULL DEFAULT NOW()
  );
  -- Initial supabase_functions migration
  INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
  -- supabase_functions.hooks definition
  CREATE TABLE supabase_functions.hooks (
    id bigserial PRIMARY KEY,
    hook_table_id integer NOT NULL,
    hook_name text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT NOW(),
    request_id bigint
  );
  CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
  CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
  COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
  CREATE FUNCTION supabase_functions.http_request()
    RETURNS trigger
    LANGUAGE plpgsql
    AS \$function\$
    DECLARE
      request_id bigint;
      payload jsonb;
      url text := TG_ARGV[0]::text;
      method text := TG_ARGV[1]::text;
      headers jsonb DEFAULT '{}'::jsonb;
      params jsonb DEFAULT '{}'::jsonb;
      timeout_ms integer DEFAULT 1000;
    BEGIN
      IF url IS NULL OR url = 'null' THEN
        RAISE EXCEPTION 'url argument is missing';
      END IF;

      IF method IS NULL OR method = 'null' THEN
        RAISE EXCEPTION 'method argument is missing';
      END IF;

      IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
        headers = '{"Content-Type": "application/json"}'::jsonb;
      ELSE
        headers = TG_ARGV[2]::jsonb;
      END IF;

      IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
        params = '{}'::jsonb;
      ELSE
        params = TG_ARGV[3]::jsonb;
      END IF;

      IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
        timeout_ms = 1000;
      ELSE
        timeout_ms = TG_ARGV[4]::integer;
      END IF;

      CASE
        WHEN method = 'GET' THEN
          SELECT http_get INTO request_id FROM net.http_get(
            url,
            params,
            headers,
            timeout_ms
          );
        WHEN method = 'POST' THEN
          payload = jsonb_build_object(
            'old_record', OLD,
            'record', NEW,
            'type', TG_OP,
            'table', TG_TABLE_NAME,
            'schema', TG_TABLE_SCHEMA
          );

          SELECT http_post INTO request_id FROM net.http_post(
            url,
            payload,
            params,
            headers,
            timeout_ms
          );
        ELSE
          RAISE EXCEPTION 'method argument % is invalid', method;
      END CASE;

      INSERT INTO supabase_functions.hooks
        (hook_table_id, hook_name, request_id)
      VALUES
        (TG_RELID, TG_NAME, request_id);

      RETURN NEW;
    END
  \$function\$;
  -- Supabase super admin
  DO
  \$\$
  BEGIN
    IF NOT EXISTS (
      SELECT 1
      FROM pg_roles
      WHERE rolname = 'supabase_functions_admin'
    )
    THEN
      CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
    END IF;
  END
  \$\$;
  GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
  GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
  GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
  ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
  ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
  ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
  ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
  GRANT supabase_functions_admin TO postgres;
  -- Remove unused supabase_pg_net_admin role
  DO
  \$\$
  BEGIN
    IF EXISTS (
      SELECT 1
      FROM pg_roles
      WHERE rolname = 'supabase_pg_net_admin'
    )
    THEN
      REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
      DROP OWNED BY supabase_pg_net_admin;
      DROP ROLE supabase_pg_net_admin;
    END IF;
  END
  \$\$;
  -- pg_net grants when extension is already enabled
  DO
  \$\$
  BEGIN
    IF EXISTS (
      SELECT 1
      FROM pg_extension
      WHERE extname = 'pg_net'
    )
    THEN
      GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
      ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
      ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
      ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
      ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
      REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
      REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
      GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
      GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
    END IF;
  END
  \$\$;
  -- Event trigger for pg_net
  CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
  RETURNS event_trigger
  LANGUAGE plpgsql
  AS \$\$
  BEGIN
    IF EXISTS (
      SELECT 1
      FROM pg_event_trigger_ddl_commands() AS ev
      JOIN pg_extension AS ext
      ON ev.objid = ext.oid
      WHERE ext.extname = 'pg_net'
    )
    THEN
      GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
      ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
      ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
      ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
      ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
      REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
      REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
      GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
      GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
    END IF;
  END;
  \$\$;
  COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
  DO
  \$\$
  BEGIN
    IF NOT EXISTS (
      SELECT 1
      FROM pg_event_trigger
      WHERE evtname = 'issue_pg_net_access'
    ) THEN
      CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
      EXECUTE PROCEDURE extensions.grant_pg_net_access();
    END IF;
  END
  \$\$;
  INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
  ALTER function supabase_functions.http_request() SECURITY DEFINER;
  ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
  REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
  GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
COMMIT;

-- jwt.sql
\set jwt_secret $MORPHIC_SUPABASE_JWT_SECRET
\set jwt_exp 3600

ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';

-- roles.sql
-- NOTE: change to your own passwords for production environments
\set pgpass $MORPHIC_DATABASE_USER_PASSWORD

ALTER USER authenticator WITH PASSWORD :'pgpass';
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';

EOFMT
  chmod 0755 $HSHQ_STACKS_DIR/morphic/dbimport/01-init.sql
  cat < $HSHQ_STACKS_DIR/morphic/dbimport/11-migrations.sql
-- _supabase.sql
\set pguser $MORPHIC_DATABASE_USER

CREATE DATABASE _supabase WITH OWNER :pguser;

-- logs.sql
\set pguser $MORPHIC_DATABASE_USER

\c _supabase
create schema if not exists _analytics;
alter schema _analytics owner to :pguser;
\c postgres

-- pooler.sql
\set pguser $MORPHIC_DATABASE_USER

\c _supabase
create schema if not exists _supavisor;
alter schema _supavisor owner to :pguser;
\c postgres

-- realtime.sql
\set pguser $MORPHIC_DATABASE_USER

create schema if not exists _realtime;
alter schema _realtime owner to :pguser;
EOFMT
  chmod 0755 $HSHQ_STACKS_DIR/morphic/dbimport/11-migrations.sql
  cat < $HSHQ_STACKS_DIR/morphic/config/vector.yml
api:
  enabled: true
  address: 0.0.0.0:9001

sources:
  docker_host:
    type: docker_logs
    exclude_containers:
      - morphic-supabase-vector

transforms:
  project_logs:
    type: remap
    inputs:
      - docker_host
    source: |-
      .project = "default"
      .event_message = del(.message)
      .appname = del(.container_name)
      del(.container_created_at)
      del(.container_id)
      del(.source_type)
      del(.stream)
      del(.label)
      del(.image)
      del(.host)
      del(.stream)
  router:
    type: route
    inputs:
      - project_logs
    route:
      kong: '.appname == "morphic-supabase-kong"'
      auth: '.appname == "morphic-supabase-auth"'
      rest: '.appname == "morphic-supabase-rest"'
      realtime: '.appname == "morphic-supabase-realtime"'
      storage: '.appname == "morphic-supabase-storage"'
      functions: '.appname == "morphic-supabase-edge-functions"'
      db: '.appname == "morphic-supabase-db"'
  # Ignores non nginx errors since they are related with kong booting up
  kong_logs:
    type: remap
    inputs:
      - router.kong
    source: |-
      req, err = parse_nginx_log(.event_message, "combined")
      if err == null {
          .timestamp = req.timestamp
          .metadata.request.headers.referer = req.referer
          .metadata.request.headers.user_agent = req.agent
          .metadata.request.headers.cf_connecting_ip = req.client
          .metadata.request.method = req.method
          .metadata.request.path = req.path
          .metadata.request.protocol = req.protocol
          .metadata.response.status_code = req.status
      }
      if err != null {
        abort
      }
  # Ignores non nginx errors since they are related with kong booting up
  kong_err:
    type: remap
    inputs:
      - router.kong
    source: |-
      .metadata.request.method = "GET"
      .metadata.response.status_code = 200
      parsed, err = parse_nginx_log(.event_message, "error")
      if err == null {
          .timestamp = parsed.timestamp
          .severity = parsed.severity
          .metadata.request.host = parsed.host
          .metadata.request.headers.cf_connecting_ip = parsed.client
          url, err = split(parsed.request, " ")
          if err == null {
              .metadata.request.method = url[0]
              .metadata.request.path = url[1]
              .metadata.request.protocol = url[2]
          }
      }
      if err != null {
        abort
      }
  # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.
  auth_logs:
    type: remap
    inputs:
      - router.auth
    source: |-
      parsed, err = parse_json(.event_message)
      if err == null {
          .metadata.timestamp = parsed.time
          .metadata = merge!(.metadata, parsed)
      }
  # PostgREST logs are structured so we separate timestamp from message using regex
  rest_logs:
    type: remap
    inputs:
      - router.rest
    source: |-
      parsed, err = parse_regex(.event_message, r'^(?P