diff --git a/config/redirects_domain.example.com.json.dist b/config/redirects_www.example.com.json.dist
similarity index 100%
rename from config/redirects_domain.example.com.json.dist
rename to config/redirects_www.example.com.json.dist
diff --git a/docker/.env b/docker/.env
new file mode 100644
index 0000000000000000000000000000000000000000..31d8948d624acd82a12edd2a83bccaa1315cac0e
--- /dev/null
+++ b/docker/.env
@@ -0,0 +1,16 @@
+# ======================================================================
+#
+# GENERATED BY init.sh - template: ./templates/dot_env - e2cde05722688ff85d3a93e9cd55787e
+# values to be used in docker-composer.yml
+#
+# ======================================================================
+
+# ----- application
+APP_NAME=my_new_app
+
+# uid of www-data in the docker container
+DOCKER_USER_UID=33
+
+APP_PORT=8008
+WEBROOT=/var/www/my_new_app/public_html
+
diff --git a/docker/containers/db-server/mariadb/my.cnf b/docker/containers/db-server/mariadb/my.cnf
new file mode 100644
index 0000000000000000000000000000000000000000..cc3b80d295df31125a4e5c600a7301e2d44b9d2f
--- /dev/null
+++ b/docker/containers/db-server/mariadb/my.cnf
@@ -0,0 +1,3 @@
+[mysqld]
+; collation-server = utf8mb4_unicode_ci
+; character-set-server = utf8mb4
\ No newline at end of file
diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..31263885c9f8b327a89409308bd918e7787a82f4
--- /dev/null
+++ b/docker/containers/web-server/Dockerfile
@@ -0,0 +1,14 @@
+#
+# GENERATED BY init.sh - template: ./templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15
+#
+FROM php:8.2-apache
+
+# install packages
+RUN apt-get update && apt-get install -y git unzip zip
+
+# enable apache modules
+RUN a2enmod 
+
+# install php packages
+COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
+RUN install-php-extensions xdebug
diff --git a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
new file mode 100644
index 0000000000000000000000000000000000000000..7ed4d6fc28954f0e749328a68bc927f268fa8b3c
--- /dev/null
+++ b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf
@@ -0,0 +1,17 @@
+#
+# GENERATED BY init.sh - template: ./templates/vhost_app.conf - 50f337db404bc73530e3340a8f2f1af9
+#
+<VirtualHost *:80>
+  DocumentRoot /var/www/my_new_app/public_html
+  <Directory /var/www/my_new_app/public_html>
+      AllowOverride None
+      Order Allow,Deny
+      Allow from All
+  </Directory>
+
+  # example to prevent access with http
+  <Location "/no-access">
+    Require all denied
+  </Location>
+
+</VirtualHost>
\ No newline at end of file
diff --git a/docker/containers/web-server/php/extra-php-config.ini b/docker/containers/web-server/php/extra-php-config.ini
new file mode 100644
index 0000000000000000000000000000000000000000..aa13bd779afa40bbfa25f10adef9baeae6d14f7d
--- /dev/null
+++ b/docker/containers/web-server/php/extra-php-config.ini
@@ -0,0 +1,24 @@
+;
+; GENERATED BY init.sh - template: ./templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2
+;
+[PHP]
+
+error_reporting=E_ALL
+display_errors=1
+
+; ----------------------------------------------------------------------
+; XDEBUG STUFF BELOW
+; ----------------------------------------------------------------------
+; 
+; error_reporting=E_ALL
+; 
+; [xdebug]
+; xdebug.mode=develop,debug
+; ; xdebug.client_host=localhost
+; xdebug.start_with_request=yes
+; ; xdebug.start_with_request=trigger
+; 
+; xdebug.log=/tmp/xdebug.log
+; xdebug.discover_client_host = 1
+; ; xdebug.client_port=9003
+; xdebug.idekey="netbeans-xdebug"
\ No newline at end of file
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9b5d9940d606b8c37485cad39e0a4393efc06120
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,44 @@
+#
+# GENERATED BY init.sh - template: ./templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b
+#
+# ======================================================================
+#
+# (1) see .env for set variables
+# (2) run "docker-compose up" to startup
+# 
+# ======================================================================
+version: '3.9'
+
+networks:
+  my_new_app-network:
+
+services:
+
+  # ----- apache httpd + php
+  my_new_app-web-server:
+    build:
+      context: .
+      dockerfile: ./containers/web-server/Dockerfile
+    image: "php:8.2-apache"
+    container_name: 'my_new_app-server'
+    ports:
+      - '${APP_PORT}:80'
+
+    working_dir: ${WEBROOT}
+    
+    volumes:
+      - ../:/var/www/${APP_NAME}
+      - ./containers/web-server/apache/sites-enabled:/etc/apache2/sites-enabled
+      - ./containers/web-server/php/extra-php-config.ini:/usr/local/etc/php/conf.d/extra-php-config.ini
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost"]
+      interval: 10s
+      timeout: 3s
+      retries: 5
+      # start_period: 40s
+
+    networks:
+      - my_new_app-network
+
+    user: ${DOCKER_USER_UID}
+
diff --git a/docker/init.sh b/docker/init.sh
new file mode 100755
index 0000000000000000000000000000000000000000..060db24942ad10e1bb0f3abca96443d85f55d289
--- /dev/null
+++ b/docker/init.sh
@@ -0,0 +1,340 @@
+#!/bin/bash
+# ======================================================================
+#
+# 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)
+# ======================================================================
+
+cd $( dirname $0 )
+. $( basename $0 ).cfg
+
+# git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git
+selfgitrepo="docker-php-starterkit.git"
+
+_version="1.6"
+
+# ----------------------------------------------------------------------
+# FUNCTIONS
+# ----------------------------------------------------------------------
+
+# draw a headline 2
+function h2(){
+    echo
+    echo -e "\e[33m>>>>> $*\e[0m"
+}
+
+# draw a headline 3
+function h3(){
+    echo
+    echo -e "\e[34m----- $*\e[0m"
+}
+
+# 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} 
+# }
+
+# 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
+
+    set -vx
+
+    for mywritedir in ${WRITABLEDIR}
+    do 
+
+        echo "--- ${mywritedir}"
+        # remove current acl
+        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}"
+
+        # 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}"
+    done
+
+    set +vx
+}
+
+# cleanup starterkit git data
+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
+        echo
+        echo -n "Delete local .git and .gitignore? [y/N] > "
+        read answer
+        test "$answer" = "y" && ( echo "Deleting ... " && rm -rf ../.git ../.gitignore )
+    else
+        echo "It was done already - $selfgitrepo was not found."
+    fi
+
+}
+
+# helper function: cut a text file starting from database start marker
+# 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 [ $iStart -gt 0 ]; then
+            sed -ni "1,${iStart}p" ${_file}
+        fi
+    fi
+}
+
+# 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
+function _generateFiles(){
+
+    # re-read config vars
+    . $( basename $0 ).cfg
+
+    local _tmpfile=/tmp/newfilecontent$$.tmp
+    h2 "generate files from templates..."
+    for mytpl in $( ls -1 ./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 }' )
+
+        if [ -z "$target" ]; then
+            echo SKIP: $mytpl - target was not found in 1st line
+            _doReplace=0
+        fi
+
+        # write generated files to target
+        if [ $_doReplace -eq 1 ]; then
+
+            # write file from line 2 to a tmp file
+            sed -n '2,$p' $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
+            _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
+            else
+                rm -f $_tmpfile
+                echo "SKIP: $mytpl - Nothing to do."
+            fi
+        fi
+        echo
+    done
+
+}
+
+# loop over all files in templates subdir make replacements and generate
+# a traget file.
+function _removeGeneratedFiles(){
+    h2 "remove generated files..."
+    for mytpl in $( ls -1 ./templates/* )
+    do
+        h3 $mytpl
+
+        # fetch traget file from first line
+        target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' )
+
+        if [ ! -z "$target" -a -f "../$target" ]; then
+            echo -n "REMOVING "
+            ls -l "../$target" || exit 2
+            rm -f "../$target" || exit 2
+            echo OK
+        else
+            echo SKIP: $target
+        fi
+        
+    done
+}
+
+function _showContainers(){
+    local bLong=$1
+    h2 CONTAINERS
+    if [ -z "$bLong" ]; then
+        docker-compose -p "$APP_NAME" ps
+    else
+        docker ps | grep $APP_NAME
+    fi
+}
+
+
+# a bit stupid ... i think I need to delete it.
+function _showInfos(){
+    _showContainers long
+    h2 INFO
+
+    h3 "processes"
+    docker-compose top
+
+    h3 "Check app port"
+    >/dev/tcp/localhost/${APP_PORT} 2>/dev/null && (
+        echo "OK, app port ${APP_PORT} is reachable"
+        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"
+        echo
+        echo "In a local DB admin tool:"
+        echo "  host    : localhost"
+        echo "  port    : ${DB_PORT}"
+        echo "  user    : root"
+        echo "  password: ${MYSQL_ROOT_PASS}"
+    )
+    echo
+}
+
+# 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
+}
+
+# ----------------------------------------------------------------------
+# MAIN
+# ----------------------------------------------------------------------
+
+action=$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
+
+        _showContainers
+
+        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 "  $( _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"
+        echo
+        echo "  $( _key m ) - more infos"
+        echo "  $( _key c ) - console (bash)"
+        echo
+        echo "  $( _key q ) - quit"
+        echo
+        echo -n "  select >"
+        read -rn 1 action 
+        echo
+    fi
+
+    case "$action" in
+        g)
+            _removeGitdata
+            ;;
+        i)
+            # _gitinstall
+            _setWritepermissions
+            ;;
+        t)
+            _generateFiles
+            ;;
+        T)
+            _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"
+            if [ "$action" = "U" ]; then
+                dockerUp+=" --build"
+            fi
+            if $dockerUp; then
+                echo "In a web browser:"
+                echo "  $frontendurl"
+            else
+                echo "ERROR: docker-compose up failed :-/"
+                docker-compose -p "$APP_NAME" logs | tail
+            fi
+            echo
+
+            _wait
+            ;;
+        s)
+            docker-compose -p "$APP_NAME" stop
+            ;;
+        r)
+            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
+            ;;
+        q)
+            exit 0;
+            ;;
+        *) 
+            test -n "$action" && ( echo "  ACTION FOR [$action] NOT IMPLEMENTED."; sleep 1 )
+    esac
+    action=
+done
+
+
+# ----------------------------------------------------------------------
diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..7c781c5f4288fe3acf990cd7ad6db2241f78a9bd
--- /dev/null
+++ b/docker/init.sh.cfg
@@ -0,0 +1,69 @@
+# ======================================================================
+#
+# settings for init.sh and base values for replacements in template files
+# This script is sourced by init.sh ... this file is bash syntax
+# 
+# ----------------------------------------------------------------------
+# 2021-12-17  <axel.hahn@iml.unibe.ch>
+# ======================================================================
+
+APP_NAME=my_new_app
+
+# web port 80 in container is seen on localhost as ...
+APP_PORT=8008
+
+APP_APT_PACKAGES="git unzip zip"
+
+#APP_APACHE_MODULES="rewrite"
+APP_APACHE_MODULES=""
+
+APP_PHP_VERSION=8.2
+# APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug"
+# APP_PHP_MODULES="curl mbstring xml zip xdebug"
+APP_PHP_MODULES="xdebug"
+
+# optional exec command after container was started with init.sh script
+# APP_ONSTARTUP="php /var/www/${APP_NAME}/public_html/myservice.php"
+APP_ONSTARTUP=""
+
+# ----------------------------------------------------------------------
+
+# add a container with database?
+DB_ADD=false
+
+# ----------------------------------------------------------------------
+# for an optional database server
+
+DB_PORT=13306
+
+# ----- database settings
+MYSQL_IMAGE=mariadb:10.5.9
+MYSQL_RANDOM_ROOT_PASSWORD=0
+MYSQL_ALLOW_EMPTY_PASSWORD=0
+MYSQL_ROOT_PASS=12345678
+MYSQL_USER=${APP_NAME}
+MYSQL_PASS=mypassword
+MYSQL_DB=${APP_NAME}
+
+
+
+# ======================================================================
+# ignore things below
+
+
+# where to set acl where local user and web user in container
+# can write simultanously
+WRITABLEDIR=../public_html
+
+
+# web service user in container
+DOCKER_USER_UID=33
+
+# document root inside web-server container 
+WEBROOT=/var/www/${APP_NAME}/public_html
+
+CUTTER_NO_DATABASE="CUT-HERE-FOR-NO-DATABASE"
+
+frontendurl=http://localhost:${APP_PORT}/
+
+# ----------------------------------------------------------------------
diff --git a/docker/templates/docker-compose.yml b/docker/templates/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3e039a763699162c9f53e84e5e7a78ddfa34d717
--- /dev/null
+++ b/docker/templates/docker-compose.yml
@@ -0,0 +1,71 @@
+# TARGET: docker/docker-compose.yml
+#
+# {{generator}}
+#
+# ======================================================================
+#
+# (1) see .env for set variables
+# (2) run "docker-compose up" to startup
+# 
+# ======================================================================
+version: '3.9'
+
+networks:
+  {{APP_NAME}}-network:
+
+services:
+
+  # ----- apache httpd + php
+  {{APP_NAME}}-web-server:
+    build:
+      context: .
+      dockerfile: ./containers/web-server/Dockerfile
+    image: "php:{{APP_PHP_VERSION}}-apache"
+    container_name: '{{APP_NAME}}-server'
+    ports:
+      - '${APP_PORT}:80'
+
+    working_dir: ${WEBROOT}
+    
+    volumes:
+      - ../:/var/www/${APP_NAME}
+      - ./containers/web-server/apache/sites-enabled:/etc/apache2/sites-enabled
+      - ./containers/web-server/php/extra-php-config.ini:/usr/local/etc/php/conf.d/extra-php-config.ini
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost"]
+      interval: 10s
+      timeout: 3s
+      retries: 5
+      # start_period: 40s
+
+    networks:
+      - {{APP_NAME}}-network
+
+    user: ${DOCKER_USER_UID}
+
+    # --- 8< --- {{CUTTER_NO_DATABASE}} --- 8< ---
+
+    depends_on:
+      - {{APP_NAME}}-db-server
+
+  # ----- mariadb
+  {{APP_NAME}}-db-server:
+    image: {{MYSQL_IMAGE}}
+    container_name: '${APP_NAME}-db'
+    # restart: always
+    ports:
+      - '${DB_PORT}:3306'
+    environment:
+      MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASS}'
+      MYSQL_USER: '${MYSQL_USER}'
+      MYSQL_PASSWORD: '${MYSQL_PASS}'
+      MYSQL_DATABASE: '${MYSQL_DB}'
+    volumes:
+      # - ./containers/db-server/db_data:/var/lib/mysql
+      - ./containers/db-server/mariadb/my.cnf:/etc/mysql/conf.d/my.cnf
+    healthcheck:
+      test: mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD
+      interval: 5s
+      retries: 5
+    networks:
+      - {{APP_NAME}}-network
diff --git a/docker/templates/dot_env b/docker/templates/dot_env
new file mode 100644
index 0000000000000000000000000000000000000000..bc8af1d5372ca0731b4e2eebd0a3e95cc2d1c78b
--- /dev/null
+++ b/docker/templates/dot_env
@@ -0,0 +1,28 @@
+# TARGET: docker/.env
+# ======================================================================
+#
+# {{generator}}
+# values to be used in docker-composer.yml
+#
+# ======================================================================
+
+# ----- application
+APP_NAME={{APP_NAME}}
+
+# uid of www-data in the docker container
+DOCKER_USER_UID={{DOCKER_USER_UID}}
+
+APP_PORT={{APP_PORT}}
+WEBROOT={{WEBROOT}}
+
+# --- 8< --- {{CUTTER_NO_DATABASE}} --- 8< ---
+
+DB_PORT={{DB_PORT}}
+
+# ----- database settings
+MYSQL_RANDOM_ROOT_PASSWORD={{MYSQL_RANDOM_ROOT_PASSWORD}}
+MYSQL_ALLOW_EMPTY_PASSWORD={{MYSQL_ALLOW_EMPTY_PASSWORD}}
+MYSQL_ROOT_PASS={{MYSQL_ROOT_PASS}}
+MYSQL_USER={{APP_NAME}}
+MYSQL_PASS={{MYSQL_PASS}}
+MYSQL_DB={{APP_NAME}}
diff --git a/docker/templates/extra-php-config.ini b/docker/templates/extra-php-config.ini
new file mode 100644
index 0000000000000000000000000000000000000000..d3f53279bbc03ee66876efaeec97cfdd207f3c94
--- /dev/null
+++ b/docker/templates/extra-php-config.ini
@@ -0,0 +1,25 @@
+# TARGET: docker/containers/web-server/php/extra-php-config.ini
+;
+; {{generator}}
+;
+[PHP]
+
+error_reporting=E_ALL
+display_errors=1
+
+; ----------------------------------------------------------------------
+; XDEBUG STUFF BELOW
+; ----------------------------------------------------------------------
+; 
+; error_reporting=E_ALL
+; 
+; [xdebug]
+; xdebug.mode=develop,debug
+; ; xdebug.client_host=localhost
+; xdebug.start_with_request=yes
+; ; xdebug.start_with_request=trigger
+; 
+; xdebug.log=/tmp/xdebug.log
+; xdebug.discover_client_host = 1
+; ; xdebug.client_port=9003
+; xdebug.idekey="netbeans-xdebug"
\ No newline at end of file
diff --git a/docker/templates/my.cnf b/docker/templates/my.cnf
new file mode 100644
index 0000000000000000000000000000000000000000..3692f17bf2799b45b3ce9746e31e3e8d1fcb5d64
--- /dev/null
+++ b/docker/templates/my.cnf
@@ -0,0 +1,4 @@
+# TARGET: docker/containers/db-server/mariadb/my.cnf
+[mysqld]
+; collation-server = utf8mb4_unicode_ci
+; character-set-server = utf8mb4
\ No newline at end of file
diff --git a/docker/templates/readme.md b/docker/templates/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..34c2c57c1ad0518f5cc00d12148cdddc0720187f
--- /dev/null
+++ b/docker/templates/readme.md
@@ -0,0 +1,7 @@
+# Templates
+
+## Rules
+
+* in the first line must be a line `# TARGET: [name of target file]` to define the target file
+* Placeholdrs have the syntax variable in double brackets, i.e. `{{VARNAME}}`
+* variables to be replaced are those in docker/init.sh.cfg and `{{genrator}}`
diff --git a/docker/templates/vhost_app.conf b/docker/templates/vhost_app.conf
new file mode 100644
index 0000000000000000000000000000000000000000..ae25f2f19107a610cea871b83b55cd78ebd4d274
--- /dev/null
+++ b/docker/templates/vhost_app.conf
@@ -0,0 +1,18 @@
+# TARGET: docker/containers/web-server/apache/sites-enabled/vhost_app.conf
+#
+# {{generator}}
+#
+<VirtualHost *:80>
+  DocumentRoot {{WEBROOT}}
+  <Directory {{WEBROOT}}>
+      AllowOverride None
+      Order Allow,Deny
+      Allow from All
+  </Directory>
+
+  # example to prevent access with http
+  <Location "/no-access">
+    Require all denied
+  </Location>
+
+</VirtualHost>
\ No newline at end of file
diff --git a/docker/templates/web-server-Dockerfile b/docker/templates/web-server-Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..d2443af6363a7d5ad2194d9c02349fb8fc3e218d
--- /dev/null
+++ b/docker/templates/web-server-Dockerfile
@@ -0,0 +1,15 @@
+# TARGET: docker/containers/web-server/Dockerfile
+#
+# {{generator}}
+#
+FROM php:{{APP_PHP_VERSION}}-apache
+
+# install packages
+RUN apt-get update && apt-get install -y {{APP_APT_PACKAGES}}
+
+# enable apache modules
+RUN a2enmod {{APP_APACHE_MODULES}}
+
+# install php packages
+COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
+RUN install-php-extensions {{APP_PHP_MODULES}}
diff --git "a/docs/10_\360\237\223\221_Description.md" "b/docs/10_\360\237\223\221_Description.md"
index f98e2e196f855dec69aaf2f14762abe7bcc305f2..99773cfd6df1ecfd8aa573663ab5c07fa1af4797 100644
--- "a/docs/10_\360\237\223\221_Description.md"
+++ "b/docs/10_\360\237\223\221_Description.md"
@@ -1,6 +1,6 @@
 ## Requirements
 
-* PHP 7+
+* PHP 7+ (up to PHP 8.2)
 * Webserver (docs describe usage for Apache httpd)
 
 ## Features
diff --git "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md" "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md"
index 2c7480e5b0c6190a9bdaa3cf9ae896c27325ecc7..c69d22a1be4209d3f63264bd8fdd9896b8b4b67c 100644
--- "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md"
+++ "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md"
@@ -19,6 +19,8 @@ Then you can open **/admin** in your webbrowser, eg.  `http(s)://servername/admi
 
 You get a list with all defined domains and its redirects + aliases.
 
+![Admin ui](images/admin_ui.png)
+
 In the table you see the columns
 
 * **Host** - the hostname/ FQDN that has a redirect rule
diff --git a/docs/images/admin_ui.png b/docs/images/admin_ui.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7f9671a7a69da673f55571a7e127a94848f8b09
Binary files /dev/null and b/docs/images/admin_ui.png differ
diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index 9b52942c85b5d9f0938fd54efdce480f6d24b159..74c931cf6babd09c289c1569cc8aa4abda0a9943 100644
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -13,6 +13,7 @@
  * ----------------------------------------------------------------------
  * 2022-02-03  v0.1  <axel.hahn@iml.unibe.ch>  initial version 
  * 2022-05-31  v0.2  <axel.hahn@iml.unibe.ch>  optical changes; use debugredirect=1 if url is a local domain
+ * 2023-08-28  v1.0  <axel.hahn@unibe.ch>      Welcome message if there is no config yet
  * ----------------------------------------------------------------------
  */
 
@@ -37,6 +38,7 @@ $aIco=[
     'type_alias'=>'◻️',
 
     'url'=>'🌐',
+    'welcome'=>'🪄',
 ];
 
 // ----------------------------------------------------------------------
@@ -52,7 +54,12 @@ function getId($sDomain){
 // ----------------------------------------------------------------------
 
 if (!$oR->isEnabled()){
-    $sHtml.='<div class="error">Admin is disabled.</div>';
+    $sHtml.='<div class="content">
+        <h3>Nothing to see here.</h3>
+        <div class="error">
+            The Admin interface is disabled.
+        </div>
+        </div>';
 } else {
 
 
@@ -99,32 +106,7 @@ if (!$oR->isEnabled()){
 
     // ---------- LOOP OVER ALL ENTRIES
 
-
-    $sHtml.='
-    <!--
-    <h2>'.$aIco['h2_head'].' Http head tester</h2>
-    <div class="content">
-    <form>
-        '.$aIco['url'].' <input type="text" name="url" size="100" value="'.$sUrl.'" placeholder="Enter url or click a link in the table below."/>
-        <button>Http HEAD</button>
-    </form>
-    </div>
-    <br>
-    -->
-    <h2>'.$aIco['h2_config'].' Domains and their redirects</h2>
-    <div class="content">
-
-    <table class="mydatatable"><thead>
-    <tr>
-    <th>Host</th>
-    <th>Ip address</th>
-    <th>Setup</th>
-    <th>Type</th>
-    <th>From</th>
-    <th>Code</th>
-    <th>Target</th>
-    </tr>
-    </thead><tbody>';
+    $sTable='';
     foreach($aHosts as $sHost => $aCfg){
         $sTdFirst='<tr class="cfgtype-'.$aCfg['type'].'">'
             .'<td>'
@@ -159,7 +141,7 @@ if (!$oR->isEnabled()){
                 if (count($aCfg['redirects'][$sType])){
                     foreach($aCfg['redirects'][$sType] as $sFrom=>$aTo){
                         $iCount++;
-                        $sHtml.=$sTdFirst
+                        $sTable.=$sTdFirst
                             .'<td class="type-'.$sType.'">'.$sType.'</td>'
                             .'<td class="type-'.$sType.'">'
                                 .($sType == 'direct' 
@@ -177,29 +159,24 @@ if (!$oR->isEnabled()){
         } else {
             // type = alias
             // $sHtml.='<tr>'.$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <a href="#'.getId($aCfg['target']).'">'.$aCfg['target'].'</a>' : '').'</td></tr>';
-            $sHtml.=$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <em>'.$aCfg['target'].'</em>' : '').'</td></tr>';
+            $sTable.=$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <em>'.$aCfg['target'].'</em>' : '').'</td></tr>';
         }
         
     }
-    $sHtml.='</tbody></table></div>'
-        /*
-        .'<h2>Config array</h2>
-        <pre>'.print_r($aHosts, 1).'</pre>'
-        */
-        ;
 
-    $sErrors = $sErrors 
-        ? '<h2>'.$aIco['h2_err'].' Found errors</h2>'
-            .'<div class="content">'
-                .'<ol class="error">'
-                .$sErrors
-                .'</ol>'
-            .'</div>'
-        : ''
-        ;
-
-        $sHtml.=''
-        .'<br><br>'
+    $sTable=$sTable
+        ? '<table class="mydatatable"><thead>
+        <tr>
+        <th>Host</th>
+        <th>Ip address</th>
+        <th>Setup</th>
+        <th>Type</th>
+        <th>From</th>
+        <th>Code</th>
+        <th>Target</th>
+        </tr>
+        </thead><tbody>'.$sTable.'</tbody></table></div>'
+        . '<br><br>'
         .'<div class="content legend">'
             . '<strong>Legend</strong>:<br>'
             . '<table><tbody>'
@@ -230,7 +207,54 @@ if (!$oR->isEnabled()){
             .'</tbody></table>'
         .'</div>'
 
-        .'<footer><a href="'.$oR->urlRepo.'">Source</a> | <a href="'.$oR->urlDocs.'">Docs</a></footer>'
+        : '<h3>'.$aIco['welcome'].' Welcome!</h3>
+            <p>
+                Thank you for the installation!<br>
+                Now is a good moment to create your first config.
+            </p>
+            <ul>
+                <li>Go to the directory ./config/</li>
+                <li>Watch the *.dist files - make a copy of them to *.json (without .dist)</li>
+                <li>Relaod this page</li>
+            </ul>
+            <p>
+                See the <a href="'.$oR->urlDocs.'Configuration.html" target="_blank">Docs</a> for details.
+            </p>
+            '
+        ;
+
+    $sHtml.='
+    <!--
+    <h2>'.$aIco['h2_head'].' Http head tester</h2>
+    <div class="content">
+    <form>
+        '.$aIco['url'].' <input type="text" name="url" size="100" value="'.$sUrl.'" placeholder="Enter url or click a link in the table below."/>
+        <button>Http HEAD</button>
+    </form>
+    </div>
+    <br>
+    -->
+    <h2>'.$aIco['h2_config'].' Domains and their redirects</h2>
+    <div class="content">'
+        
+        /*
+        .'<h2>Config array</h2>
+        <pre>'.print_r($aHosts, 1).'</pre>'
+        */
+        .$sTable
+        ;
+
+    $sErrors = $sErrors 
+        ? '<h2>'.$aIco['h2_err'].' Found errors</h2>'
+            .'<div class="content">'
+                .'<ol class="error">'
+                .$sErrors
+                .'</ol>'
+            .'</div>'
+        : ''
+        ;
+
+    $sHtml.='<footer><a href="'.$oR->urlRepo.'">Source</a> | <a href="'.$oR->urlDocs.'">Docs</a></footer>'
         ;
 }
 
diff --git a/public_html/admin/main.css b/public_html/admin/main.css
index a09355ae30946e6cecc4543fe85d3e6fe99262db..224eebe01ee21135fc46250bd46fceb738f863d4 100644
--- a/public_html/admin/main.css
+++ b/public_html/admin/main.css
@@ -3,6 +3,7 @@ body{background: #f8f8f8; color: #234; font-family: arial; margin: 0;}
 h1{background:rgba(0,0,0,0.05); margin: 0 0 1em;; padding: 0.5em;}
 h1 a{color:#234; text-decoration: none;}
 h2{background: #d0e0e8;  color:#458; margin: 1em 0 0.5em; border-top: 2px solid #fff; border-left: 5px solid #fff; border-top-left-radius: 0.5em; padding: 0.5em; margin: 0 0 1em;}
+h3{color:#ccc; font-size: 250%}
 
 pre{background: rgba(0,0,0,0.02);padding: 0.3em 1em; border: 1px solid rgba(0,0,0,0.1); margin: 2em 0 3em;; border-bottom: 2px solid rgba(0,0,0,0.2);}
 
@@ -11,8 +12,8 @@ footer{background:rgba(0,0,0,0.03); margin-top: 4em; text-align: right;padding:
 
 .content{margin: 0 1em;}
 .legend{background: #fff; padding: 1em;}
-.error{background: #fcc;}
-.warning{color:#651; background:#fec;}
+.error{background: #fcc; padding: 0.2em 1em;}
+.warning{color:#651; background:#fec; padding: 0.2em 1em;}
 
 .cfgtype-alias{color:#89a;}
 .http-301::after{color:#a55; content: ' (Moved Permanently)'}
diff --git a/public_html/classes/redirect.admin.class.php b/public_html/classes/redirect.admin.class.php
index abd763837213890d7c8c8a56d41bde9eeee9e27c..7513e18e30ed4e2157056a8b230c3cb51c91f146 100644
--- a/public_html/classes/redirect.admin.class.php
+++ b/public_html/classes/redirect.admin.class.php
@@ -17,6 +17,7 @@ require_once 'redirect.class.php';
  * 2022-02-03  v1.5  ah  add method isEnabled
  * 2022-05-23  v1.6  ah  add http head check+render output; 
  * 2022-05-31  v1.7  ah  optical changes
+ * 2023-08-28  v1.8  ah  remove php warning if there is no config yet
  */
 
 /**
@@ -149,21 +150,23 @@ class redirectadmin extends redirect {
             }
         }
         $aAliases=$this->_getAliases();
-        foreach($aAliases as $sAlias=>$sConfig){
-            if(isset($aReturn[$sAlias])){
-                $aErrors[]="alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority).";
-            } else {
-                if(!isset($aReturn[$sConfig])){
-                    $aErrors[]="alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist.";
+        if(is_array($aAliases) && count($aAliases)){
+            foreach($aAliases as $sAlias=>$sConfig){
+                if(isset($aReturn[$sAlias])){
+                    $aErrors[]="alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority).";
                 } else {
-                    $aReturn[$sConfig]['aliases'][]=$sAlias;
-                    $aReturn[$sAlias]=array(
-                        'type'=>'alias',
-                        'target'=>$sConfig,
-                        'ip'=> $this->_getIp($sAlias),
-                    );
-                    if (!$aReturn[$sAlias]['ip']){
-                        $aErrors[]='alias.json: The hostname was not found in DNS: '.$sAlias;
+                    if(!isset($aReturn[$sConfig])){
+                        $aErrors[]="alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist.";
+                    } else {
+                        $aReturn[$sConfig]['aliases'][]=$sAlias;
+                        $aReturn[$sAlias]=array(
+                            'type'=>'alias',
+                            'target'=>$sConfig,
+                            'ip'=> $this->_getIp($sAlias),
+                        );
+                        if (!$aReturn[$sAlias]['ip']){
+                            $aErrors[]='alias.json: The hostname was not found in DNS: '.$sAlias;
+                        }
                     }
                 }
             }
diff --git a/public_html/classes/redirect.class.php b/public_html/classes/redirect.class.php
index 7d9231d5ce910c331abd37bee4109d8d432ef320..c04108a2279046efa59d944753aa28b4d45c725b 100644
--- a/public_html/classes/redirect.class.php
+++ b/public_html/classes/redirect.class.php
@@ -23,6 +23,7 @@
  * 2019-04-25  v1.2  ah  use REQUEST_URI (works on Win and Linux)
  * 2020-05-06  v1.3  ah  added aliases for multiple domains with the same config
  * 2020-05-06  v1.4  ah  rewrite as class
+ * 2023-08-28  v1.5  ah  fix loop over config with missing regex section.
  */
 
 /**
@@ -229,13 +230,14 @@ class redirect {
             $aRedirect = $this->aConfig['direct'][$this->sRequest];
         } else {
             $this->_wd("no direct match ... scanning regex");
-            foreach (array_keys($this->aConfig['regex']) as $sRegex) {
-
-                $this->_wd("check if regex [$sRegex] matches $this->sRequest");
-                if (preg_match('#' . $sRegex . '#', $this->sRequest)) {
-                    $this->_wd("REGEX MATCH! aborting tests");
-                    $aRedirect = $this->aConfig['regex'][$sRegex];
-                    break;
+            if(isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])){
+                foreach (array_keys($this->aConfig['regex']) as $sRegex) {
+                    $this->_wd("check if regex [$sRegex] matches $this->sRequest");
+                    if (preg_match('#' . $sRegex . '#', $this->sRequest)) {
+                        $this->_wd("REGEX MATCH! aborting tests");
+                        $aRedirect = $this->aConfig['regex'][$sRegex];
+                        break;
+                    }
                 }
             }
         }
diff --git a/readme.md b/readme.md
index 71ad0b2ece2760ad69c4c4b2984b5d407d8dff6f..da271843ca6810186ea9b715a5dafc14612803c4 100644
--- a/readme.md
+++ b/readme.md
@@ -8,3 +8,7 @@ Author: Axel Hahn; Institute for Medical Education; University of Bern
 📄 Source: <https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler> \
 📜 License: GNU GPL 3.0 \
 📖 Docs: <https://os-docs.iml.unibe.ch/redirect-handler/> or see the [docs](./docs)
+
+- - -
+
+![Admin ui](docs/images/admin_ui.png)
\ No newline at end of file