From b2a89970d39396c615c00fbf7f8a3f74e7c806a1 Mon Sep 17 00:00:00 2001
From: "Hahn Axel (hahn)" <axel.hahn@unibe.ch>
Date: Mon, 6 Jan 2025 15:03:07 +0100
Subject: [PATCH] update docker dev env

---
 docker/.env                                   |   2 +-
 docker/containers/web-server/Dockerfile       |   6 +-
 .../apache/sites-enabled/vhost_app.conf       |   2 +-
 .../web-server/php/extra-php-config.ini       |   2 +-
 docker/docker-compose.yml                     |   4 +-
 docker/init.sh                                | 732 ++++++++++++++----
 docker/init.sh.cfg                            |   7 +-
 7 files changed, 612 insertions(+), 143 deletions(-)

diff --git a/docker/.env b/docker/.env
index 95e9205..0ddeb5f 100644
--- a/docker/.env
+++ b/docker/.env
@@ -1,6 +1,6 @@
 # ======================================================================
 #
-# GENERATED BY init.sh - template: ./templates/dot_env - e2cde05722688ff85d3a93e9cd55787e
+# GENERATED BY init.sh - template: templates/dot_env - e2cde05722688ff85d3a93e9cd55787e
 # values to be used in docker-composer.yml
 #
 # ======================================================================
diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile
index 0c17062..93975a1 100644
--- a/docker/containers/web-server/Dockerfile
+++ b/docker/containers/web-server/Dockerfile
@@ -1,13 +1,13 @@
 #
-# GENERATED BY init.sh - template: ./templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15
+# GENERATED BY init.sh - template: templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15
 #
-FROM php:8.2-apache
+FROM php:8.4-apache
 
 # install packages
 RUN apt-get update && apt-get install -y git unzip zip libapache2-mod-xsendfile
 
 # enable apache modules
-RUN a2enmod xsendfile
+RUN a2enmod rewrite xsendfile
 
 # install php packages
 COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
diff --git a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
index 6d6e7fb..db98532 100644
--- a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
+++ b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
@@ -1,5 +1,5 @@
 #
-# GENERATED BY init.sh - template: ./templates/vhost_app.conf - 4dfd63417ad808a5ed00ffaf117464a8
+# GENERATED BY init.sh - template: templates/vhost_app.conf - 4dfd63417ad808a5ed00ffaf117464a8
 #
 <VirtualHost *:80>
   DocumentRoot /var/www/ci-pkg/public_html
diff --git a/docker/containers/web-server/php/extra-php-config.ini b/docker/containers/web-server/php/extra-php-config.ini
index aa13bd7..8fc5696 100644
--- a/docker/containers/web-server/php/extra-php-config.ini
+++ b/docker/containers/web-server/php/extra-php-config.ini
@@ -1,5 +1,5 @@
 ;
-; GENERATED BY init.sh - template: ./templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2
+; GENERATED BY init.sh - template: templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2
 ;
 [PHP]
 
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 39e932e..85be423 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,5 +1,5 @@
 #
-# GENERATED BY init.sh - template: ./templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b
+# GENERATED BY init.sh - template: templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b
 #
 # ======================================================================
 #
@@ -19,7 +19,7 @@ services:
     build:
       context: .
       dockerfile: ./containers/web-server/Dockerfile
-    image: "php:8.2-apache"
+    image: "php:8.4-apache"
     container_name: 'ci-pkg-server'
     ports:
       - '${APP_PORT}:80'
diff --git a/docker/init.sh b/docker/init.sh
index 060db24..04fc725 100755
--- a/docker/init.sh
+++ b/docker/init.sh
@@ -4,53 +4,354 @@
 # DOCKER PHP DEV ENVIRONMENT :: INIT
 #
 # ----------------------------------------------------------------------
-# 2021-11-nn  v1.0 <axel.hahn@iml.unibe.ch>
-# 2022-07-19  v1.1 <axel.hahn@iml.unibe.ch>  support multiple dirs for setfacl
-# 2022-11-16  v1.2 <www.axel-hahn.de>        use docker-compose -p "$APP_NAME"
-# 2022-12-18  v1.3 <www.axel-hahn.de>        add -p "$APP_NAME" in other docker commands
-# 2022-12-20  v1.4 <axel.hahn@unibe.ch>      replace fgrep with grep -F
-# 2023-03-06  v1.5 <www.axel-hahn.de>        up with and without --build
-# 2023-08-17  v1.6 <www.axel-hahn.de>        menu selection with single key (without return)
+# 2021-11-nn  v1.0  <axel.hahn@iml.unibe.ch>
+# 2022-07-19  v1.1  <axel.hahn@iml.unibe.ch>  support multiple dirs for setfacl
+# 2022-11-16  v1.2  <www.axel-hahn.de>        use docker-compose -p "$APP_NAME"
+# 2022-12-18  v1.3  <www.axel-hahn.de>        add -p "$APP_NAME" in other docker commands
+# 2022-12-20  v1.4  <axel.hahn@unibe.ch>      replace fgrep with grep -F
+# 2023-03-06  v1.5  <www.axel-hahn.de>        up with and without --build
+# 2023-08-17  v1.6  <www.axel-hahn.de>        menu selection with single key (without return)
+# 2023-11-10  v1.7  <axel.hahn@unibe.ch>      replace docker-compose with "docker compose"
+# 2023-11-13  v1.8  <axel.hahn@unibe.ch>      UNDO "docker compose"; update infos
+# 2023-11-15  v1.9  <axel.hahn@unibe.ch>      add help; execute multiple actions by params; new menu item: open app
+# 2023-12-07  v1.10 <www.axel-hahn.de>        simplyfy console command; add php linter
+# 2024-07-01  v1.11 <www.axel-hahn.de>        diff with colored output; suppress errors on port check
+# 2024-07-19  v1.12 <axel.hahn@unibe.ch>      apply shell fixes
+# 2024-07-22  v1.13 <axel.hahn@unibe.ch>      show info if there is no database container; speedup replacements
+# 2024-07-22  v1.14 <axel.hahn@unibe.ch>      show colored boxes with container status
+# 2024-07-24  v1.15 <axel.hahn@unibe.ch>      update menu output
+# 2024-07-26  v1.16 <axel.hahn@unibe.ch>      hide unnecessary menu items (WIP)
+# 2024-07-29  v1.17 <www.axel-hahn.de>        hide unnecessary menu items; reorder functions
+# 2024-08-14  v1.18 <www.axel-hahn.de>        update container view
+# 2024-09-20  v1.19 <www.axel-hahn.de>        detect dockerd-rootless (hides menu item to set permissions)
+# 2024-10-16  v1.20 <axel.hahn@unibe.ch>      add db import and export
+# 2024-10-25  v1.21 <axel.hahn@unibe.ch>      create missing subdir dbdumps
+# 2024-10-30  v1.22 <axel.hahn@unibe.ch>      added: Open Mysql client in container
+# 2024-10-30  v1.23 <axel.hahn@unibe.ch>      added: show menu hints why some menu items are visible
+# 2024-11-20  v1.24 <axel.hahn@unibe.ch>      fix menu with started database less app; apply template permissions on target file; add $WEBURL; remove $frontendurl
+# 2024-11-20  v1.25 <axel.hahn@unibe.ch>      fix menu startup containers
+# 2024-11-21  v1.26 <axel.hahn@unibe.ch>      Reset colors in _checkConfig 
 # ======================================================================
 
-cd $( dirname $0 )
-. $( basename $0 ).cfg
+cd "$( dirname "$0" )" || exit 1
+
+_version="1.26"
+
+# init used vars
+gittarget=
+WEBURL=
+
+_self=$( basename "$0" )
+
+# shellcheck source=/dev/null
+. "${_self}.cfg" || exit 1
+
 
 # git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git
 selfgitrepo="docker-php-starterkit.git"
 
-_version="1.6"
+fgGray="\e[1;30m"
+fgRed="\e[31m"
+fgGreen="\e[32m"
+fgBrown="\e[33m"
+fgBlue="\e[34m"
+
+fgInvert="\e[7m"
+fgReset="\e[0m"
+
+# ----- status varsiables
+# running containers
+DC_WEB_UP=0
+DC_DB_UP=0
+DC_ALL_UP=0
+
+# repo of docker-php-starterkit is here?
+DC_REPO=1
+
+DC_CONFIG_CHANGED=0
+
+# absolute urls for web app
+DC_WEB_URL=""
+
+DC_DUMP_DIR=dbdumps
+DC_SHOW_MENUHINTS=0
+
+isDockerRootless=0
+ps -ef | grep  dockerd-rootless | grep -q $USER && isDockerRootless=1
 
 # ----------------------------------------------------------------------
 # FUNCTIONS
 # ----------------------------------------------------------------------
 
+# check config for changes in newer versions
+function _checkConfig(){
+
+    # --- v1.24
+    if [ -z "$WEBURL" ]; then
+        echo -e "${fgBrown}INFO: add 'WEBURL=\"/\"' in your ${_self}.cfg. It is a new var since v1.24${fgReset}"
+        WEBURL="/"
+    fi
+    if [ -n "$frontendurl" ]; then
+        echo -e "${fgBrown}INFO: Remove frontendurl=$frontendurl in your ${_self}.cfg. It is obsolete since v1.24${fgReset}"
+    fi
+
+}
+# ----------------------------------------------------------------------
+# STATUS FUNCTIONS
+
+# get container status and set global variable DC_REPO
+# DC_REPO = 0 nothing to do - repo was changed to project
+# DC_REPO = 1 if repo is in selfgitrepo (must be deleted)
+function _getStatus_repo(){
+    DC_REPO=0
+    git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo && DC_REPO=1
+}
+
+# check if any of the templates has a change that must be applied
+function _getStatus_template(){
+    _generateFiles "dryrun"
+}
+
+# get container status and set global variables
+# DC_WEB_UP - web container 
+# DC_DB_UP  - database container
+#   0 = down
+#   1 = up
+function _getStatus_docker(){
+    local _out
+    _out=$( docker-compose -p "$APP_NAME" ps)    
+
+    DC_WEB_UP=0
+    DC_DB_UP=0
+    DC_ALL_UP=0
+
+    grep -q "${APP_NAME}-server" <<< "$_out" && DC_WEB_UP=1
+    grep -q "${APP_NAME}-db"     <<< "$_out"  && DC_DB_UP=1
+
+    if [ "$DB_ADD" != "false" ] && [ ! -d "${DC_DUMP_DIR}" ]; then
+        echo "INFO: creating subdir ${DC_DUMP_DIR} to import/ export databases ..."
+        mkdir "${DC_DUMP_DIR}" || exit 1
+        return
+    fi
+
+    if [ "${DC_WEB_UP}" = "1" ] && [ "${DC_DB_UP}" = "1" ]; then
+        DC_ALL_UP=1
+    fi
+
+    if [ "$DB_ADD" = "false" ] && [ "${DC_WEB_UP}" = "1" ]; then
+        DC_ALL_UP=1
+    fi
+
+}
+
+# Get web url of the application
+# It is for support of Nginx Docker Proxy
+# https://github.com/axelhahn/nginx-docker-proxy
+# It returns http://localhost:<port> or a https://<appname> plus $WEBURL
+function _getWebUrl(){
+    if grep -q "^[0-9\.]* ${APP_NAME}-server" /etc/hosts; then
+        DC_WEB_URL="https://${APP_NAME}-server$WEBURL"
+    else
+        DC_WEB_URL=http://localhost:${APP_PORT}$WEBURL
+    fi
+    set +vx
+}
+
+# ----------------------------------------------------------------------
+# OUTPUT
+
 # draw a headline 2
 function h2(){
     echo
-    echo -e "\e[33m>>>>> $*\e[0m"
+    echo -e "$fgBrown>>>>> $*$fgReset"
 }
 
 # draw a headline 3
 function h3(){
     echo
-    echo -e "\e[34m----- $*\e[0m"
+    echo -e "$fgBlue----- $*$fgReset"
+}
+
+# helper for menu: print an inverted key
+function  _key(){
+    echo -en "$fgInvert ${1} $fgReset"
+}
+
+# helper for menu: show hint text
+# param  int      FLag _bAll (i true the txt will be hidden)
+# param  string   message to show
+function menuhint(){
+    local _bAll="$1"
+    shift 1
+    test $DC_SHOW_MENUHINTS -ne 0 && test "$_bAll" -eq "0" && ( echo -e "$fgBlue  $*$fgReset" )
+}
+
+# show menu in interactive mode and list keys in help with param -h
+# param  string  optional: set to "all" to show all menu items
+function showMenu(){
+
+    local _bAll=0
+    test -n "$1" && _bAll=1
+
+    local _spacer="    "
+
+    echo
+    if [ $DC_REPO -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Git data of starterkit were found"
+        echo "${_spacer}$( _key g ) - remove git data of starterkit"
+        echo
+    fi
+
+    if [ $isDockerRootless -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Because rootless docker was found"
+        echo "${_spacer}$( _key i ) - init application: set permissions"
+        echo
+    fi
+
+    if [ $DC_CONFIG_CHANGED -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Config was changed"
+        echo "${_spacer}$( _key t ) - generate files from templates"
+        echo
+    fi
+    if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Config is unchanged"
+        echo "${_spacer}$( _key T ) - remove generated files"
+        echo
+    fi
+    if [ $DC_ALL_UP -eq 0 ] || [ $_bAll -eq 1 \
+    ]; then
+        if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then
+            menuhint $_bAll "A container is down and config is unchanged"
+            echo "${_spacer}$( _key u ) - startup containers    docker-compose ... up -d"
+            echo "${_spacer}$( _key U ) - startup containers    docker-compose ... up -d --build"
+            echo
+            echo "${_spacer}$( _key r ) - remove containers     docker-compose rm -f"
+            echo
+        fi
+    fi
+    if [ $DC_WEB_UP -eq 1 ] || [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "A container is up"
+        echo "${_spacer}$( _key s ) - shutdown containers   docker-compose stop"
+        echo
+        echo "${_spacer}$( _key m ) - more infos"
+        echo "${_spacer}$( _key o ) - open app [${APP_NAME}] $DC_WEB_URL"
+        echo "${_spacer}$( _key c ) - console (bash)"
+        echo
+    fi
+    if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+        menuhint $_bAll "Web container is up"
+        echo "${_spacer}$( _key p ) - console check with php linter"
+        echo
+    fi
+    if [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then
+        echo
+        menuhint $_bAll "Database container is up"
+        echo "${_spacer}$( _key d ) - Dump container database"
+        echo "${_spacer}$( _key D ) - Import Dump into container database"
+        echo "${_spacer}$( _key M ) - Open Mysql client in database container"
+        echo
+    fi
+    menuhint $_bAll "Always available"
+    echo "${_spacer}$( _key q ) - quit"
+
+}
+function showHelp(){
+    cat <<EOH
+
+INITIALIZER FOR DOCKER APP v$_version
+
+A helper script written in Bash to bring up a PHP+Mysql application in docker.
+
+📄 Source : https://git-repo.iml.unibe.ch/iml-open-source/docker-php-starterkit
+📗 Docs   : https://os-docs.iml.unibe.ch/docker-php-starterkit/
+📜 License: GNU GPL 3.0
+(c) Institute for Medical Education; University of Bern
+
+
+SYNTAX:
+  $_self [-h|-v]
+  $_self [menu key [.. menu key N]]
+
+OPTIONS:
+  -h   show this help and exit
+  -v   show version exit
+
+MENU KEYS:
+  In the interactive menu are some keys to init an action.
+  The same keys can be put as parameter to start this action.
+  You can add multiples keys to apply multiple actions.
+
+$( showMenu "all" )
+
+EXAMPLES:
+
+  $_self           starts interactive mode
+  $_self u         bring up docker container(s) and stay in interactive mode
+  $_self i q       set write permissions and quit
+  $_self p q       start php linter and exit
+
+EOH
+}
+
+
+# show urls for app container
+function _showBrowserurl(){
+    echo "In a web browser open:"
+    echo "  $DC_WEB_URL"
+}
+
+# detect + show ports and urls for app container and db container
+function _showInfos(){
+    _showContainers long
+    h2 INFO
+
+    h3 "processes webserver"
+    # docker-compose top
+    docker top "${APP_NAME}-server"
+    if [ ! "$DB_ADD" = "false" ]; then
+        h3 "processes database"
+        docker top "${APP_NAME}-db"
+    fi
+
+    h3 "What to open in browser"
+    if echo >"/dev/tcp/localhost/${APP_PORT}"; then
+        # echo "OK, app port ${APP_PORT} is reachable"
+        # echo
+        _showBrowserurl
+    else
+        echo "ERROR: app port ${APP_PORT} is not available"
+    fi 2>/dev/null
+
+    if [ "$DB_ADD" != "false" ]; then
+        h3 "Check database port"
+        if echo >"/dev/tcp/localhost/${DB_PORT}"; then
+            echo "OK, db port ${DB_PORT} is reachable"
+            echo
+            echo "In a local DB admin tool you can connect it:"
+            echo "  host    : localhost"
+            echo "  port    : ${DB_PORT}"
+            echo "  user    : root"
+            echo "  password: ${MYSQL_ROOT_PASS}"
+        else
+            echo "NO, db port ${DB_PORT} is not available"
+        fi 2>/dev/null
+
+    fi
+    echo
 }
 
-# function _gitinstall(){
-#     h2 "install/ update app from git repo ${gitrepo} in ${gittarget} ..."
-#     test -d ${gittarget} && ( cd ${gittarget}  && git pull )
-#     test -d ${gittarget} || git clone -b ${gitbranch} ${gitrepo} ${gittarget} 
-# }
+# ----------------------------------------------------------------------
+# ACTIONS
 
 # set acl on local directory
 function _setWritepermissions(){
     h2 "set write permissions on ${gittarget} ..."
 
-    local _user=$( id -gn )
-    typeset -i local _user_uid=0
-    test -f /etc/subuid && _user_uid=$( grep $_user /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1
-    typeset -i local DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID
+    local _user; _user=$( id -gn )
+    local _user_uid; typeset -i _user_uid=0
+
+    test -f /etc/subuid && _user_uid=$( grep "$_user" /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1
+    local DOCKER_USER_OUTSIDE; typeset -i DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID
 
     set -vx
 
@@ -62,10 +363,10 @@ function _setWritepermissions(){
         sudo setfacl -bR "${mywritedir}"
 
         # default permissions: both the host user and the user with UID 33 (www-data on many systems) are owners with rwx perms
-        sudo setfacl -dRm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}"
+        sudo setfacl -dRm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}"
 
         # permissions: make both the host user and the user with UID 33 owner with rwx perms for all existing files/directories
-        sudo setfacl -Rm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}"
+        sudo setfacl -Rm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}"
     done
 
     set +vx
@@ -76,11 +377,10 @@ function _removeGitdata(){
     h2 "Remove git data of starterkit"
     echo -n "Current git remote url: "
     git config --get remote.origin.url
-    git config --get remote.origin.url 2>/dev/null | grep $selfgitrepo >/dev/null
-    if [ $? -eq 0 ]; then
+    if git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo; then
         echo
         echo -n "Delete local .git and .gitignore? [y/N] > "
-        read answer
+        read -r answer
         test "$answer" = "y" && ( echo "Deleting ... " && rm -rf ../.git ../.gitignore )
     else
         echo "It was done already - $selfgitrepo was not found."
@@ -92,36 +392,64 @@ function _removeGitdata(){
 # see _generateFiles()
 function _fix_no-db(){
     local _file=$1
-    if [ $DB_ADD = false ]; then
-        typeset -i local iStart=$( cat ${_file} | grep -Fn "$CUTTER_NO_DATABASE" | cut -f 1 -d ':' )-1
+    if [ "$DB_ADD" = "false" ]; then
+        local iStart; typeset -i iStart
+        iStart=$( grep -Fn "$CUTTER_NO_DATABASE" "${_file}" | cut -f 1 -d ':' )-1
         if [ $iStart -gt 0 ]; then
-            sed -ni "1,${iStart}p" ${_file}
+            sed -n "$sed_no_backup" "1,${iStart}p" "${_file}"
         fi
     fi
 }
 
+# helper function to generate replacements using sed
+# it loops over all vars in the config file
+# used in _generateFiles
+function _getreplaces(){
+    # loop over vars to make the replacement
+    grep "^[a-zA-Z]" "$_self.cfg" | while read -r line
+    do
+        # echo replacement: $line
+        mykey=$( echo "$line" | cut -f 1 -d '=' )
+        myvalue="$( eval echo \"\$"$mykey"\" )"
+
+        # TODO: multiline values fail here in replacement with sed 
+        echo -e "s#{{$mykey}}#${myvalue}#g"
+
+    done
+}
+
 # loop over all files in templates subdir make replacements and generate
 # a target file.
 # It skips if 
 #   - 1st line is not starting with "# TARGET: filename"
 #   - target file has no updated lines
+# If the 1st parameter is set to "dryrun" it will not generate files.
+# param string dryrun optional: set to "dryrun" to not generate files
 function _generateFiles(){
 
-    # re-read config vars
-    . $( basename $0 ).cfg
+    local _dryrun="$1"
+    DC_CONFIG_CHANGED=0
+
+    # shellcheck source=/dev/null
+    . "${_self}.cfg" || exit 1    
+
+    params=$( _getreplaces | while read -r line; do echo -n "-e '$line' ";  done )
 
     local _tmpfile=/tmp/newfilecontent$$.tmp
-    h2 "generate files from templates..."
-    for mytpl in $( ls -1 ./templates/* )
+    
+    test "$_dryrun" = "dryrun" || h2 "generate files from templates..."
+    for mytpl in templates/*
     do
         # h3 $mytpl
         local _doReplace=1
 
         # fetch traget file from first line
-        target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
+        target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
 
         if [ -z "$target" ]; then
-            echo SKIP: $mytpl - target was not found in 1st line
+            if [ "$_dryrun" != "dryrun" ]; then
+                echo "SKIP: $mytpl - target was not found in 1st line"
+            fi
             _doReplace=0
         fi
 
@@ -129,39 +457,38 @@ function _generateFiles(){
         if [ $_doReplace -eq 1 ]; then
 
             # write file from line 2 to a tmp file
-            sed -n '2,$p' $mytpl >$_tmpfile
+            sed -n '2,$p' "$mytpl" >"$_tmpfile"
+            chmod "$( stat -c %a "$mytpl" )" "$_tmpfile"
 
             # add generator
             # sed -i "s#{{generator}}#generated by $0 - template: $mytpl - $( date )#g" $_tmpfile
-            local _md5=$( md5sum $_tmpfile | awk '{ print $1 }' )
-            sed -i "s#{{generator}}#GENERATED BY $( basename $0 ) - template: $mytpl - $_md5#g" $_tmpfile
-
-            # loop over vars to make the replacement
-            grep "^[a-zA-Z]" $( basename $0 ).cfg | while read line
-            do
-                # echo replacement: $line
-                mykey=$( echo $line | cut -f 1 -d '=' )
-                myvalue="$( eval echo \"\${$mykey}\" )"
-                # grep "{{$mykey}}" $_tmpfile
-
-                # TODO: multiline values fail here in replacement with sed 
-                sed -i "s#{{$mykey}}#${myvalue}#g" $_tmpfile
-            done
+            local _md5; _md5=$( md5sum $_tmpfile | awk '{ print $1 }' )
+            sed -i "$sed_no_backup" "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile
+
+            # apply all replacements to the tmp file
+            eval sed "$sed_no_backup" "$params" "$_tmpfile" || exit
+
             _fix_no-db $_tmpfile
 
             # echo "changes for $target:"
-            diff  "../$target"  "$_tmpfile" | grep -v "$_md5" | grep -v "^---" | grep .
-            if [ $? -eq 0 -o ! -f "../$target" ]; then
-                echo -n "$mytpl - changes detected - writing [$target] ... "
-                mkdir -p $( dirname  "../$target" ) || exit 2
-                mv "$_tmpfile" "../$target" || exit 2
-                echo OK
+            if diff --color=always "../$target"  "$_tmpfile" 2>/dev/null | grep -v "$_md5" | grep -v "^---" | grep . || [ ! -f "../$target" ]; then
+                if [ "$_dryrun" = "dryrun" ]
+                then
+                    DC_CONFIG_CHANGED=1
+                else
+                    echo -n "$mytpl - changes detected - writing [$target] ... "
+                    mkdir -p "$( dirname  ../"$target" )" || exit 2
+                    mv "$_tmpfile" "../$target" || exit 2
+                    echo -e "${fgGreen}OK${fgReset}"
+                    echo
+                fi
             else
                 rm -f $_tmpfile
-                echo "SKIP: $mytpl - Nothing to do."
+                if [ "$_dryrun" != "dryrun" ]; then
+                    echo "SKIP: $mytpl - Nothing to do."
+                fi
             fi
         fi
-        echo
     done
 
 }
@@ -170,104 +497,200 @@ function _generateFiles(){
 # a traget file.
 function _removeGeneratedFiles(){
     h2 "remove generated files..."
-    for mytpl in $( ls -1 ./templates/* )
+    for mytpl in templates/*
     do
-        h3 $mytpl
+        h3 "$mytpl"
 
         # fetch traget file from first line
-        target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
+        target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
 
-        if [ ! -z "$target" -a -f "../$target" ]; then
+        if [ -n "$target" ] && [ -f "../$target" ]; then
             echo -n "REMOVING "
             ls -l "../$target" || exit 2
             rm -f "../$target" || exit 2
-            echo OK
+            echo -e "${fgGreen}OK${fgReset}"
         else
-            echo SKIP: $target
+            echo "SKIP: $target"
         fi
         
     done
 }
 
+
+# show running containers
 function _showContainers(){
     local bLong=$1
-    h2 CONTAINERS
-    if [ -z "$bLong" ]; then
-        docker-compose -p "$APP_NAME" ps
-    else
-        docker ps | grep $APP_NAME
+
+    local _out
+
+    local sUp=".. UP"
+    local sDown=".. down"
+
+    local Status=
+    local StatusWeb="$sDown"
+    local StatusDb="$sDown"
+    local colWeb=
+    local colDb=
+
+    colDb="$fgRed"
+    colWeb="$fgRed"
+
+    if [ $DC_WEB_UP -eq 1 ]; then
+        colWeb="$fgGreen"
+        StatusWeb="$sUp"
+    fi
+    
+    if [ $DC_DB_UP -eq 1 ]; then
+        colDb="$fgGreen"
+        StatusDb="$sUp"
     fi
-}
 
+    if [ "$DB_ADD" = "false" ]; then
+        colDb="$fgGray"
+        local StatusDb=".. N/A"
+        Status="This app has no database container."
+    fi
 
-# a bit stupid ... i think I need to delete it.
-function _showInfos(){
-    _showContainers long
-    h2 INFO
+    h2 CONTAINERS
 
-    h3 "processes"
-    docker-compose top
+    echo
+    printf "  $colWeb$fgInvert  %-32s  $fgReset   $colDb$fgInvert  %-32s  $fgReset\n"      "WEB ${StatusWeb}"  "DB ${StatusDb}"
+    printf "    %-32s  $fgReset     %-32s  $fgReset\n"      "PHP ${APP_PHP_VERSION}"      "${MYSQL_IMAGE}"
+    printf "    %-32s  $fgReset     %-32s  $fgReset\n"      ":${APP_PORT}"                ":${DB_PORT}"
+
+    echo
 
-    h3 "Check app port"
-    >/dev/tcp/localhost/${APP_PORT} 2>/dev/null && (
-        echo "OK, app port ${APP_PORT} is reachable"
+    if [ -n "$Status" ]; then
+        echo "  $Status"
         echo
-        echo "In a web browser open:"
-        echo "  $frontendurl"
-    )
-    h3 "Check database port"
-    >/dev/tcp/localhost/${DB_PORT} 2>/dev/null && (
-        echo "OK, db port ${DB_PORT} is reachable"
+    fi
+
+    if [ -n "$bLong" ]; then
+        echo "$_out"
+
+        h2 STATS
+        docker stats --no-stream
         echo
-        echo "In a local DB admin tool:"
-        echo "  host    : localhost"
-        echo "  port    : ${DB_PORT}"
-        echo "  user    : root"
-        echo "  password: ${MYSQL_ROOT_PASS}"
-    )
-    echo
-}
+    fi
 
-# helper for menu: print an inverted key
-function  _key(){
-    printf "\e[4;7m ${1} \e[0m"
 }
 
 # helper: wait for a return key
 function _wait(){
-    echo -n "... press RETURN > "; read -r
+    local _wait=15
+    echo -n "... press RETURN ... or wait $_wait sec > "; read -r -t $_wait
+    echo
+}
+
+# DB TOOL - dump db from container
+function _dbDump(){
+    local _iKeepDumps;
+    typeset -i _iKeepDumps=5
+    local _iStart;
+    typeset -i _iStart=$_iKeepDumps+1;
+
+    if [ $DC_DB_UP -eq 0 ]; then
+        echo "Database container is not running. Aborting."
+        return
+    fi
+    outfile=${DC_DUMP_DIR}/${MYSQL_DB}_$( date +%Y%m%d_%H%M%S ).sql
+    echo -n "dumping ${MYSQL_DB} ... "
+    if docker exec -i "${APP_NAME}-db" mysqldump -uroot -p${MYSQL_ROOT_PASS} ${MYSQL_DB} > "$outfile"; then
+        echo -n "OK ... Gzip ... "
+        if gzip "${outfile}"; then
+            echo "OK"
+            ls -l "$outfile.gz"
+
+            # CLEANUP
+            echo
+            echo "--- Cleanup: keep $_iKeepDumps files."
+            ls -1t ${DC_DUMP_DIR}/* | sed -n "$_iStart,\$p" | while read -r delfile
+            do 
+                echo "CLEANUP: Deleting $delfile ... "
+                rm -f "$delfile"
+            done
+            echo
+            echo -n "Size of dump directory: "
+            du -hs ${DC_DUMP_DIR} | awk '{ print $1 }'
+
+        else
+            echo "ERROR"
+            rm -f "$outfile"
+        fi
+    else
+        echo "ERROR"
+        rm -f "$outfile"
+    fi
+}
+
+# DB TOOL - import local database dump into container
+function _dbImport(){
+    echo "--- Available dumps:"
+    ls -ltr ${DC_DUMP_DIR}/*.gz | sed "s#^#    #g"
+    if [ $DC_DB_UP -eq 0 ]; then
+        echo "Database container is not running. Aborting."
+        return
+    fi
+    echo -n "Dump file to import into ${MYSQL_DB} > "
+    read -r dumpfile
+    if [ -z "$dumpfile" ]; then
+        echo "Abort - no value was given."
+        return
+    fi
+    if [ ! -f "$dumpfile" ]; then
+        echo "Abort - wrong filename."
+        return
+    fi
+
+    echo -n "Importing $dumpfile ... "
+
+    # Mac OS compatibility
+    # if zcat "$dumpfile" | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}"
+    if cat "$dumpfile" | zcat | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}"
+    then
+        echo "OK"
+    else
+        echo "ERROR"
+    fi
 }
 
 # ----------------------------------------------------------------------
 # MAIN
 # ----------------------------------------------------------------------
 
-action=$1
+_checkConfig
+
+# Mac OS compatibility
+case "$OSTYPE" in
+  darwin*|bsd*)
+    sed_no_backup=" -i '' "
+    ;; 
+  *)
+    sed_no_backup="-i"
+    ;;
+esac
+
+action=$1; shift 1
 
 while true; do
-    echo
-    echo -e "\e[32m===== INITIALIZER FOR DOCKER APP [$APP_NAME] v$_version ===== \e[0m\n\r"
 
-    if [ -z "$action" ]; then
+    _getStatus_repo
+    _getStatus_docker
+    _getStatus_template
+    _getWebUrl
 
-        _showContainers
+    if [ -z "$action" ]; then
 
-        h2 MENU
-        echo "  $( _key g ) - remove git data of starterkit"
-        echo
-        echo "  $( _key i ) - init application: set permissions"
-        echo "  $( _key t ) - generate files from templates"
-        echo "  $( _key T ) - remove generated files"
+        echo "_______________________________________________________________________________"
         echo
-        echo "  $( _key u ) - startup containers    docker-compose ... up -d"
-        echo "  $( _key U ) - startup containers    docker-compose ... up -d --build"
-        echo "  $( _key s ) - shutdown containers   docker-compose stop"
-        echo "  $( _key r ) - remove containers     docker-compose rm -f"
+        printf "  %-70s ______\n" "${APP_NAME^^}  ::  Initializer for docker"
+        echo "________________________________________________________________________/ $_version"
         echo
-        echo "  $( _key m ) - more infos"
-        echo "  $( _key c ) - console (bash)"
-        echo
-        echo "  $( _key q ) - quit"
+
+        _showContainers
+
+        h2 MENU       
+        showMenu
         echo
         echo -n "  select >"
         read -rn 1 action 
@@ -275,6 +698,8 @@ while true; do
     fi
 
     case "$action" in
+        "-h") showHelp; exit 0 ;;
+        "-v") echo "$_self $_version"; exit 0 ;;
         g)
             _removeGitdata
             ;;
@@ -289,51 +714,96 @@ while true; do
             _removeGeneratedFiles
             rm -rf containers
             ;;
-        # not in the menu
-        # f)
-        #     _removeGeneratedFiles
-        #     _generateFiles
-        #     _wait
-        #     ;;
         m)
             _showInfos
             _wait
             ;;
         u|U)
-            dockerUp="docker-compose -p "$APP_NAME" --verbose up -d --remove-orphans"
+            h2 "Bring up..."
+            dockerUp="docker-compose -p $APP_NAME --verbose up -d --remove-orphans"
             if [ "$action" = "U" ]; then
                 dockerUp+=" --build"
             fi
+            echo "$dockerUp"
             if $dockerUp; then
-                echo "In a web browser:"
-                echo "  $frontendurl"
+                _showBrowserurl
             else
                 echo "ERROR: docker-compose up failed :-/"
                 docker-compose -p "$APP_NAME" logs | tail
             fi
             echo
 
-            _wait
             ;;
         s)
+            h2 "Stopping..."
             docker-compose -p "$APP_NAME" stop
             ;;
         r)
+            h2 "Removing..."
             docker-compose -p "$APP_NAME" rm -f
             ;;
         c)
-            docker ps
-            echo -n "id or name >"
-            read dockerid
-            test -z "$dockerid" || docker exec -it $dockerid /bin/bash
+            h2 "Console"
+            _containers=$( docker-compose -p "$APP_NAME" ps | sed -n "2,\$p" | awk '{ print $1}' )
+            if [ "$DB_ADD" = "false" ]; then
+                dockerid=$_containers
+            else
+                echo "Select a container:"
+                sed "s#^#    #g" <<< "$_containers"
+                echo -n "id or name >"
+                read -r dockerid
+            fi
+            test -z "$dockerid" || (
+                echo
+                echo "> docker exec -it $dockerid /bin/bash     (type 'exit' + Return when finished)"
+                docker exec -it "$dockerid" /bin/bash
+            )
+            ;;
+        p)
+            h2 "PHP $APP_PHP_VERSION linter"
+
+            dockerid="${APP_NAME}-server"
+            echo -n "Scanning ... "
+            typeset -i _iFiles
+            _iFiles=$( docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' " | wc -l )
+
+            if [ $_iFiles -gt 0 ]; then
+                echo "found $_iFiles [*.php] files ... errors from PHP $APP_PHP_VERSION linter:"
+                time if echo "$APP_PHP_VERSION" | grep -E "([567]\.|8\.[012])" >/dev/null ; then
+                    docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' -exec php -l {} \; | grep -v '^No syntax errors detected'"
+                else
+                    docker exec -it "$dockerid" /bin/bash -c "php -l \$( find . -name '*.php' ) | grep -v '^No syntax errors detected' "
+                fi
+                echo
+                _wait
+            else
+                echo "Start your docker container first."
+            fi
+            ;;
+        d) 
+            h2 "DB tools :: dump"
+            _dbDump
+            ;;
+        D) 
+            h2 "DB tools :: import"
+            _dbImport
+            ;;
+        M)
+            h2 "DB tools :: mysql client"
+            docker exec -it "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}"
+            ;;
+        o) 
+            h2 "Open app ..."
+            xdg-open "$DC_WEB_URL"
             ;;
         q)
+            h2 "Bye!"
             exit 0;
             ;;
         *) 
             test -n "$action" && ( echo "  ACTION FOR [$action] NOT IMPLEMENTED."; sleep 1 )
     esac
-    action=
+    action=$1; shift 1
 done
 
 
diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg
index b96727e..7b4f3fc 100644
--- a/docker/init.sh.cfg
+++ b/docker/init.sh.cfg
@@ -15,9 +15,9 @@ APP_PORT=8001
 APP_APT_PACKAGES="git unzip zip libapache2-mod-xsendfile"
 
 #APP_APACHE_MODULES="rewrite"
-APP_APACHE_MODULES="xsendfile"
+APP_APACHE_MODULES="rewrite xsendfile"
 
-APP_PHP_VERSION=8.2
+APP_PHP_VERSION=8.4
 # APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug"
 APP_PHP_MODULES=""
 
@@ -60,9 +60,8 @@ DOCKER_USER_UID=33
 
 # document root inside web-server container 
 WEBROOT=/var/www/${APP_NAME}/public_html
+WEBURL="/"
 
 CUTTER_NO_DATABASE="CUT-HERE-FOR-NO-DATABASE"
 
-frontendurl=http://localhost:${APP_PORT}/
-
 # ----------------------------------------------------------------------
-- 
GitLab