Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EspoCRM official image #7603

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

tmachyshyn
Copy link

We would like to add the EspoCRM to the official image repository.


Checklist for Review

NOTE: This checklist is intended for the use of the Official Images maintainers both to track the status of your PR and to help inform you and others of where we're at. As such, please leave the "checking" of items to the repository maintainers. If there is a point below for which you would like to provide additional information or note completion, please do so by commenting on the PR. Thanks! (and thanks for staying patient with us ❤️)

  • associated with or contacted upstream?
  • does it fit into one of the common categories? ("service", "language stack", "base distribution")
    • service
  • is it reasonably popular, or does it solve a particular use case well?
  • does a documentation PR exist? (should be reviewed and merged at roughly the same time so that we don't have an empty image page on the Hub for very long)
  • dockerization review for best practices and cache gotchas/improvements (ala the official review guidelines)?
  • 2+ dockerization review?
  • existing official images have been considered as a base? (ie, if foobar needs Node.js, has FROM node:... instead of grabbing node via other means been considered?)
  • if FROM scratch, tarballs only exist in a single commit within the associated history?
  • passes current tests? any simple new tests that might be appropriate to add? (https://github.com/docker-library/official-images/tree/master/test)

@tmachyshyn
Copy link
Author

@tianon please have a look.
Thanks

@tmachyshyn
Copy link
Author

@tianon, @yosifkit, please take a look.

I would like to say that while creating the docker image we followed all the recommendations. For example:

  • we created the Dockerfile easy to understand/read.
  • using SHA256 checksum in the Dockerfile while image build.
  • added and tested on different architectures.
  • implemented recommendation about tags.
  • updating the docker image after new versions are released.

@tmachyshyn
Copy link
Author

@tianon, @yosifkit
Is there any chance to review the image?

@tmachyshyn
Copy link
Author

Is there any chance to review the image?

@AlexAv
Copy link

AlexAv commented Jul 20, 2020

Hi @tianon, @yosifkit, thank you for your attention, if there something wrong, please write and we will fix it.
Is there any possibility to speed up the process?

@AlexAv
Copy link

AlexAv commented Jul 28, 2020

@tianon, @yosifkit, look the unofficial image has been installed 50K+ times, it makes sense to create official image

Docker

@tianon
Copy link
Member

tianon commented Aug 25, 2020

    apk update; \
    apk add --no-cache --virtual .build-deps \

The apk update here is redundant -- --no-cache implies updating the package lists.


        php7-gd \
        php7-exif \

This is definitely not right -- when you're FROM php:xxx, installing php-related packages via apk or apt-get will give you two versions of PHP (the version provided by the base image, and the version from Alpine or Debian). What you intended instead is probably to make use of docker-php-ext-install (and possibly docker-php-ext-configure) to get gd and exif installed for the PHP provided by the base image (exactly as is done lower down in this same Dockerfile).


# Install php-zmq
    cd /usr; \
    curl -fSL https://github.com/zeromq/libzmq/releases/download/v4.3.2/zeromq-4.3.2.tar.gz -o zeromq-4.3.2.tar.gz; \
    tar -xvzf zeromq-4.3.2.tar.gz; \
    rm zeromq-4.3.2.tar.gz; \
    cd /usr/zeromq-4.3.2; \
    ./configure; \
    make; \
    make install; \
    cd /usr; \
    git clone https://github.com/zeromq/php-zmq.git; \
    cd /usr/php-zmq; \
    phpize && ./configure; \
    make; \
    make install; \

This seems odd to me -- why are we building zeromq from source? Alpine has a libzmq package (and the corresponding zeromq-dev package), and in Alpine 3.12 the version is the same 4.3.2 being installed here.

Additionally, why are we grabbing the latest master branch of https://github.com/zeromq/php-zmq.git ? I see the latest release on that repository is from 2013, so I could understand if that's not quite new enough, but just pulling directly from master is not going to be very reliable over time, and pulling from a specific commit would be better (likely also giving zeromq/php-zmq#203 some attention -- it hasn't had a release because they're looking for new maintainers, so it might not be the best platform to be based on right now).


ENV DOCUMENT_ROOT /var/www/html
ENV DEFAULT_OWNER www-data
ENV DEFAULT_GROUP www-data

What is the intent of these variables? As a user reading these, it feels like I can adjust these and expect the webserver configuration to adjust as well, but I don't think that's going to be the case. It seems like perhaps a local variable is what was intended instead? In the entrypoint, you probably don't want to use DEFAULT_OWNER but rather do some detection based on id -u / id -g so that users can run the image as an arbitrary user (especially since OpenStack and some Kubernetes configurations will require that ability).


ENV ESPOCRM_UPGRADE_VERSION 5.8.5
ENV ESPOCRM_UPGRADE_URL https://www.espocrm.com/downloads/upgrades/EspoCRM-upgrade-5.8.5-to-5.9.3.zip
ENV ESPOCRM_UPGRADE_SHA256 a04d4fdadd877d684abd885dc7356ea2f0593e8a805851d79d4a6f68b6168700

This is interesting -- can you elaborate on what this is for, especially since it's only downloaded/used in the entrypoint and doesn't appear to be part of the image itself?


VOLUME ${DOCUMENT_ROOT}

This and the (fairly complex) entrypoint script have me a little bit concerned/confused. This appears to be copied from the general pattern of the WordPress official image (or from another PHP-based image which copied the WordPress pattern), wherein WordPress considers the code of itself to be mutable state that it manages (by doing automatic upgrades especially for security fixes, for example). From what I can find, EspoCRM is not similar in the sense that upgrades to EspoCRM are always performed manually (https://www.espocrm.com/documentation/administration/upgrading/), and updating from the UI even appears to be highly discouraged.

Given this, I have to wonder why EspoCRM is not instead copied/installed directly to /var/www/html in the image, and the VOLUME then defined to be only the specific directory or directories where EspoCRM actually stores mutable state, thus encouraging users to adopt the appropriate container-upgrade paradigms to update their instance of EspoCRM from version to version by updating to newer versions of the container image instead of having the image include complex logic to try and maintain and old-style installation of EspoCRM within a VOLUME for them?


CMD ["sh", "-c", "cron && php-fpm"]

I understand the intent of this (to run both Cron and PHP simultaneously), but this is not acceptable in the official images. What I would recommend is documenting for users how to run crond as a completely separate process/container (there are several other PHP-based images that are good examples of what I mean, such as nextcloud).


# Set timezone
RUN echo "UTC" > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata

This is odd to me -- the default timezone of all images in the official images is already UTC, and we even have an integration test to validate that.

@yosifkit
Copy link
Member

Related to VOLUME, we've seen that many users have been fairly vocal about them existing in many images since they can't be removed, so it could make sense to not have any volume and just document the mutable folders that should be saved -- e.g. drupal.

@tmachyshyn
Copy link
Author

Thanks @tianon, @yosifkit for your reviews. We will implement your recommendations soon.

@tmachyshyn
Copy link
Author

@tianon, @yosifkit thanks again for your reviews. We made changes to your recommendations. Please check it again. Thanks

    apk update; \
    apk add --no-cache --virtual .build-deps \

The apk update here is redundant -- --no-cache implies updating the package lists.

Fixed

        php7-gd \
        php7-exif \

This is definitely not right -- when you're FROM php:xxx, installing php-related packages via apk or apt-get will give you two versions of PHP (the version provided by the base image, and the version from Alpine or Debian). What you intended instead is probably to make use of docker-php-ext-install (and possibly docker-php-ext-configure) to get gd and exif installed for the PHP provided by the base image (exactly as is done lower down in this same Dockerfile).

Fixed

# Install php-zmq
    cd /usr; \
    curl -fSL https://github.com/zeromq/libzmq/releases/download/v4.3.2/zeromq-4.3.2.tar.gz -o zeromq-4.3.2.tar.gz; \
    tar -xvzf zeromq-4.3.2.tar.gz; \
    rm zeromq-4.3.2.tar.gz; \
    cd /usr/zeromq-4.3.2; \
    ./configure; \
    make; \
    make install; \
    cd /usr; \
    git clone https://github.com/zeromq/php-zmq.git; \
    cd /usr/php-zmq; \
    phpize && ./configure; \
    make; \
    make install; \

This seems odd to me -- why are we building zeromq from source? Alpine has a libzmq package (and the corresponding zeromq-dev package), and in Alpine 3.12 the version is the same 4.3.2 being installed here.

Additionally, why are we grabbing the latest master branch of https://github.com/zeromq/php-zmq.git ? I see the latest release on that repository is from 2013, so I could understand if that's not quite new enough, but just pulling directly from master is not going to be very reliable over time, and pulling from a specific commit would be better (likely also giving zeromq/php-zmq#203 some attention -- it hasn't had a release because they're looking for new maintainers, so it might not be the best platform to be based on right now).

The installation of php-zmq is optimized. We cannot completely abandon its use, because it's required in EspoCRM.

ENV DOCUMENT_ROOT /var/www/html
ENV DEFAULT_OWNER www-data
ENV DEFAULT_GROUP www-data

What is the intent of these variables? As a user reading these, it feels like I can adjust these and expect the webserver configuration to adjust as well, but I don't think that's going to be the case. It seems like perhaps a local variable is what was intended instead? In the entrypoint, you probably don't want to use DEFAULT_OWNER but rather do some detection based on id -u / id -g so that users can run the image as an arbitrary user (especially since OpenStack and some Kubernetes configurations will require that ability).

Fixed. Now, detection of user/group is moved to the entrypoint as you recommended, https://github.com/espocrm/docker/blob/master/docker-entrypoint.sh#L188.

ENV ESPOCRM_UPGRADE_VERSION 5.8.5
ENV ESPOCRM_UPGRADE_URL https://www.espocrm.com/downloads/upgrades/EspoCRM-upgrade-5.8.5-to-5.9.3.zip
ENV ESPOCRM_UPGRADE_SHA256 a04d4fdadd877d684abd885dc7356ea2f0593e8a805851d79d4a6f68b6168700

This is interesting -- can you elaborate on what this is for, especially since it's only downloaded/used in the entrypoint and doesn't appear to be part of the image itself?

This is required for automatic upgrade EspoCRM depending on the version of EspoCRM docker image and the version of EspoCRM files / database. For example, if the version of EspoCRM docker container is 5.9.3, but the EspoCRM files / database is 5.8.3, then the entrypoint will upgrade EspoCRM files / database to the version of the docker container 5.9.3. EspoCRM has own upgrade system, but this ESPOCRM_UPGRADE_VERSION and ESPOCRM_UPGRADE_URL is required only if we need to upgrade the EspoCRM files / database to the specified version ESPOCRM_VERSION.
Currently, we are saving a link of this upgrade. Also, we can download this upgrade while building the docker image itself. What way would you recommend?

VOLUME ${DOCUMENT_ROOT}

This and the (fairly complex) entrypoint script have me a little bit concerned/confused. This appears to be copied from the general pattern of the WordPress official image (or from another PHP-based image which copied the WordPress pattern), wherein WordPress considers the code of itself to be mutable state that it manages (by doing automatic upgrades especially for security fixes, for example). From what I can find, EspoCRM is not similar in the sense that upgrades to EspoCRM are always performed manually (https://www.espocrm.com/documentation/administration/upgrading/), and updating from the UI even appears to be highly discouraged.

Given this, I have to wonder why EspoCRM is not instead copied/installed directly to /var/www/html in the image, and the VOLUME then defined to be only the specific directory or directories where EspoCRM actually stores mutable state, thus encouraging users to adopt the appropriate container-upgrade paradigms to update their instance of EspoCRM from version to version by updating to newer versions of the container image instead of having the image include complex logic to try and maintain and old-style installation of EspoCRM within a VOLUME for them?

Related to VOLUME, we've seen that many users have been fairly vocal about them existing in many images since they can't be removed, so it could make sense to not have any volume and just document the mutable folders that should be saved -- e.g. drupal.

Based on that fact that EspoCRM files / database can be automatically upgraded to the version of EspoCRM docker image, we need to mount the entire EspoCRM directory. What would you recommend this this case?

CMD ["sh", "-c", "cron && php-fpm"]

I understand the intent of this (to run both Cron and PHP simultaneously), but this is not acceptable in the official images. What I would recommend is documenting for users how to run crond as a completely separate process/container (there are several other PHP-based images that are good examples of what I mean, such as nextcloud).

Fixed, thanks for the example.

# Set timezone
RUN echo "UTC" > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata

This is odd to me -- the default timezone of all images in the official images is already UTC, and we even have an integration test to validate that.

Fixed

@AlexAv
Copy link

AlexAv commented Sep 26, 2020

We highly appreciate the feedback and recommendations you’ve previously shared with us. Since we have made all the changes you’ve mentioned, we would like to ask you to review the image again @tianon, @yosifkit.

@tianon
Copy link
Member

tianon commented Nov 5, 2020

Thank you for your patience. 🙇

The php-zmq installation looks a lot better now; I'd just add that it looks like /usr/php-zmq is unnecessary at the end of it, and could probably be deleted. 👍

(You could also possibly optimize a tiny bit by downloading a source archive from GitHub at that commit such as https://github.com/zeromq/php-zmq/archive/e0db82c3286da81fa8945894dd10125a528299e4.tar.gz instead of using git clone, but that's not strictly necessary.)

Based on that fact that EspoCRM files / database can be automatically upgraded to the version of EspoCRM docker image, we need to mount the entire EspoCRM directory. What would you recommend this this case?

Let me explain this slightly differently to see if I can do a better job. Right now, the upgrade behavior is not something EspoCRM itself does automatically, but rather is a function of this image based on how EspoCRM is deployed and upgraded outside containers. What I'm proposing is that if EspoCRM itself was not stored in the data volume, then perhaps that upgrade bundle wouldn't even be necessary (nor a large part of the entrypoint code/logic), because the way to upgrade EspoCRM itself would instead just be recreating the container.

In other words, the current flow (oversimplified) is something like:

  1. run container
    1. copies EspoCRM code to volume
    2. performs database/application initialization
    3. runs Apache/EspoCRM
  2. recreate container with newer version
    1. downloads upgrade delta
    2. applies upgrade delta patching to code in volume (ignoring the already upgraded code that exists in the image)
    3. performs applicable database upgrades
    4. runs Apache/EspoCRM

Proposed flow:

  1. run container
    1. performs database/application initialization
    2. runs Apache/EspoCRM
  2. recreate container with newer version
    1. performs applicable database upgrades
    2. runs Apache/EspoCRM

In this simpler flow, instead of having all of /var/www/html be a volume, you would have that be part of the image and only specific subdirectories with content that needs to be persisted (user uploads, etc) would be explicit volumes. This way, the image provides a more "container-native" workflow of the image being the source-of-truth for which version of the application we're running instead of our data volume (which in Wordpress's case is a consequence of how Wordpress works / expects to work).

Does that make more sense? Do you think that workflow makes sense for EspoCRM? If it's possible to accomplish, I think it could pretty dramatically simplify the logic in your container entrypoint script, and the way that users use/reason about your container image.

@tmachyshyn
Copy link
Author

Thanks for your reply.
We will try to implement your recommendations soon.

@tmachyshyn
Copy link
Author

@tianon thanks again for your thoughts.

The php-zmq installation is optimized due to your recommendations.

The current upgrade process is optimized and moved to EspoCRM side. The "entrypoint" script just checking the version and run the upgrade on EspoCRM side if needed.

EspoCRM has the directories for user's customizations:

  • application/Espo/Modules
  • client/modules
  • custom/Espo/Custom
  • client/custom

At this point I see 2 issues with the new process. For successful upgrade EspoCRM we have to do:

  1. Updating files. Copy new files and delete old ones.

Upgrade process deletes files that have been removed from EspoCRM. This will work fine only if a user will not have any additional extensions of EspoCRM. Add-ons can be installed on EspoCRM. All they are stored in application/Espo/Modules and client/modules directories. By default these directories are not empty and contain internal module Crm. So, when only mount the volumes of modules directories, the files of Crm module might not be updated or deleted.

  1. Updating database, configuration parameters via upgrade script.

In some cases, we need to run some actions with the database, configuration parameters, etc. For such purposed we can run the upgrade script which is included in the upgrade package of EspoCRM. So, when only mount the volumes of custom and modules directories, the script will not be executed.

For example, the new process can be:

  • We have espocrm-docker v6.0.0 container.
  • Run espocrm-docker v6.1.0 container. In this case, our files in Crm module and data in the database can be out of date.

The current process implemented in "entrypoint" script with the mounted volume /var/www/html are:

  • We have espocrm-docker v6.0.0 container
  • Run espocrm-docker v6.1.0 container. Files will be checked at the start and run an upgrade 6.1.0. The files will be updated and all scripts will be executed.

It would be great if you have any suggestions for resolving these issues.
Thanks

@AlexAv
Copy link

AlexAv commented Jan 11, 2021

@tianon , @yosifkit is there anything you need?

@github-actions

This comment has been minimized.

@tmachyshyn tmachyshyn requested a review from a team as a code owner April 3, 2024 07:59

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@LaurentGoderre
Copy link
Member

@tmachyshyn I opened a PR to optimize the layers and the size a bit: espocrm/espocrm-docker#42

@LaurentGoderre
Copy link
Member

@tmachyshyn I added some comments to the docs. Would be possible to address those and then squash the commits in both PRs?

@tmachyshyn
Copy link
Author

@LaurentGoderre thanks for your recommendations. That's all fixed.

This comment has been minimized.

@LaurentGoderre
Copy link
Member

Would you be able to squash down to 1 commit for each repo?

This comment has been minimized.

@tmachyshyn
Copy link
Author

tmachyshyn commented May 14, 2024

@LaurentGoderre Done for this and for the documentation repositories.
If any other action is required, please let me know.

LaurentGoderre
LaurentGoderre previously approved these changes May 14, 2024

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link

Diff for 5d926a3:
diff --git a/_bashbrew-arches b/_bashbrew-arches
index 8b13789..79acf81 100644
--- a/_bashbrew-arches
+++ b/_bashbrew-arches
@@ -1 +1,7 @@
-
+amd64
+arm32v6
+arm32v7
+arm64v8
+i386
+ppc64le
+s390x
diff --git a/_bashbrew-cat b/_bashbrew-cat
index bdfae4a..ceb1aea 100644
--- a/_bashbrew-cat
+++ b/_bashbrew-cat
@@ -1 +1,17 @@
-Maintainers: New Image! :D (@docker-library-bot)
+Maintainers: Taras Machyshyn <[email protected]> (@tmachyshyn)
+GitRepo: https://github.com/espocrm/espocrm-docker.git
+
+Tags: apache, 8.4.1-apache, 8.4-apache, 8-apache, latest, 8.4.1, 8.4, 8
+Architectures: amd64, arm32v7, arm64v8, i386, s390x
+GitCommit: 7b933d4fc7036f1a037e5c20b5e88fd2537c669f
+Directory: apache
+
+Tags: fpm, 8.4.1-fpm, 8.4-fpm, 8-fpm
+Architectures: amd64, arm32v7, arm64v8, i386, s390x
+GitCommit: 7b933d4fc7036f1a037e5c20b5e88fd2537c669f
+Directory: fpm
+
+Tags: fpm-alpine, 8.4.1-fpm-alpine, 8.4-fpm-alpine, 8-fpm-alpine
+Architectures: amd64, arm32v6, arm32v7, arm64v8, i386, ppc64le, s390x
+GitCommit: 7b933d4fc7036f1a037e5c20b5e88fd2537c669f
+Directory: fpm-alpine
diff --git a/_bashbrew-list b/_bashbrew-list
index e69de29..eb61898 100644
--- a/_bashbrew-list
+++ b/_bashbrew-list
@@ -0,0 +1,16 @@
+espocrm:8
+espocrm:8-apache
+espocrm:8-fpm
+espocrm:8-fpm-alpine
+espocrm:8.4
+espocrm:8.4-apache
+espocrm:8.4-fpm
+espocrm:8.4-fpm-alpine
+espocrm:8.4.1
+espocrm:8.4.1-apache
+espocrm:8.4.1-fpm
+espocrm:8.4.1-fpm-alpine
+espocrm:apache
+espocrm:fpm
+espocrm:fpm-alpine
+espocrm:latest
diff --git a/_bashbrew-list-build-order b/_bashbrew-list-build-order
index e69de29..ef7107e 100644
--- a/_bashbrew-list-build-order
+++ b/_bashbrew-list-build-order
@@ -0,0 +1,3 @@
+espocrm:8
+espocrm:8-fpm
+espocrm:8-fpm-alpine
diff --git a/espocrm_8-fpm-alpine/Dockerfile b/espocrm_8-fpm-alpine/Dockerfile
new file mode 100644
index 0000000..f021806
--- /dev/null
+++ b/espocrm_8-fpm-alpine/Dockerfile
@@ -0,0 +1,81 @@
+FROM php:8.2-fpm-alpine
+
+LABEL org.opencontainers.image.source=https://github.com/espocrm/espocrm
+LABEL org.opencontainers.image.description="EspoCRM is an Open Source CRM. Try for Free."
+
+# Install php libs
+RUN set -ex; \
+    apk add --no-cache --virtual .build-deps \
+        postgresql-dev \
+        libzip-dev \
+        libpng-dev \
+        libjpeg-turbo-dev \
+        libwebp-dev \
+        freetype-dev \
+        openldap-dev \
+        imap-dev \
+        libzmq \
+        zeromq-dev \
+        bash \
+        $PHPIZE_DEPS \
+    ; \
+    \
+# Install php-zmq
+    cd /usr; \
+    curl -fSL https://github.com/zeromq/php-zmq/archive/ee5fbc693f07b2d6f0d9fd748f131be82310f386.tar.gz -o php-zmq.tar.gz; \
+    tar -zxf php-zmq.tar.gz; \
+    cd php-zmq*; \
+    phpize && ./configure; \
+    make; \
+    make install; \
+    cd .. && rm -rf php-zmq*; \
+# END: Install php-zmq
+    \
+    docker-php-ext-configure gd --with-jpeg --with-webp --with-freetype; \
+    \
+    docker-php-ext-install \
+        pdo_pgsql \
+        pdo_mysql \
+        zip \
+        gd \
+        imap \
+        ldap \
+        exif \
+        pcntl \
+        posix \
+        bcmath \
+    ; \
+    docker-php-ext-enable zmq
+
+# php.ini
+RUN { \
+	echo 'expose_php = Off'; \
+	echo 'display_errors = Off'; \
+	echo 'display_startup_errors = Off'; \
+	echo 'log_errors = On'; \
+	echo 'memory_limit=256M'; \
+	echo 'max_execution_time=180'; \
+	echo 'max_input_time=180'; \
+	echo 'post_max_size=30M'; \
+	echo 'upload_max_filesize=30M'; \
+	echo 'date.timezone=UTC'; \
+} > ${PHP_INI_DIR}/conf.d/espocrm.ini
+
+ENV ESPOCRM_VERSION 8.4.1
+ENV ESPOCRM_SHA256 1681a2f68c0fc37bd46bbb9725765ed0cf16fab48a283820efb90265a7e8301d
+
+WORKDIR /var/www/html
+
+RUN set -ex; \
+    curl -fSL "https://www.espocrm.com/downloads/EspoCRM-8.4.1.zip" -o EspoCRM.zip; \
+	echo "${ESPOCRM_SHA256} *EspoCRM.zip" | sha256sum -c -; \
+    unzip -q EspoCRM.zip -d /usr/src; \
+    mv "/usr/src/EspoCRM-8.4.1" /usr/src/espocrm; \
+	rm EspoCRM.zip; \
+    chown -R www-data:www-data /usr/src/espocrm
+
+COPY ./docker-*.sh  /usr/local/bin/
+
+ENTRYPOINT [ "docker-entrypoint.sh" ]
+
+CMD ["php-fpm"]
diff --git a/espocrm_8-fpm-alpine/docker-daemon.sh b/espocrm_8-fpm-alpine/docker-daemon.sh
new file mode 100755
index 0000000..83a2b4f
--- /dev/null
+++ b/espocrm_8-fpm-alpine/docker-daemon.sh
@@ -0,0 +1,324 @@
+#!/bin/bash
+
+set -eu
+
+DOCUMENT_ROOT="/var/www/html"
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+checkInstanceReady
+
+applyConfigEnvironments
+
+/usr/local/bin/php /var/www/html/daemon.php
+
+exec "$@"
\ No newline at end of file
diff --git a/espocrm_8-fpm-alpine/docker-entrypoint.sh b/espocrm_8-fpm-alpine/docker-entrypoint.sh
new file mode 100755
index 0000000..acc13a4
--- /dev/null
+++ b/espocrm_8-fpm-alpine/docker-entrypoint.sh
@@ -0,0 +1,565 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+installationType() {
+    if [ -f "$DOCUMENT_ROOT/data/config.php" ]; then
+        local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+        if [ -n "$isInstalled" ] && [ "$isInstalled" = 1 ]; then
+            local installedVersion=$(getConfigParamFromFile "version")
+            local isVersionGreater=$(compareVersion "$ESPOCRM_VERSION" "$installedVersion" ">")
+
+            if [ -n "$isVersionGreater" ]; then
+                echo "upgrade"
+                return
+            fi
+
+            echo "skip"
+            return
+        fi
+
+        echo "reinstall"
+        return
+    fi
+
+    echo "install"
+}
+
+actionInstall() {
+    if [ ! -d "$SOURCE_FILES" ]; then
+        echo >&2 "error: Source files [$SOURCE_FILES] are not found."
+        exit 1
+    fi
+
+    cp -a "$SOURCE_FILES/." "$DOCUMENT_ROOT/"
+
+    installEspocrm
+}
+
+actionReinstall() {
+    if [ -f "$DOCUMENT_ROOT/install/config.php" ]; then
+        sed -i "s/'isInstalled' => true/'isInstalled' => false/g" "$DOCUMENT_ROOT/install/config.php"
+    fi
+
+    installEspocrm
+}
+
+actionUpgrade() {
+    UPGRADE_NUMBER=$((UPGRADE_NUMBER+1))
+
+    if [ $UPGRADE_NUMBER -gt $MAX_UPGRADE_COUNT ];then
+        echo >&2 "The MAX_UPGRADE_COUNT exceded. The upgrading process has been stopped."
+        return
+    fi
+
+    local installedVersion=$(getConfigParamFromFile "version")
+    local isVersionEqual=$(compareVersion "$installedVersion" "$ESPOCRM_VERSION" ">=")
+
+    if [ -n "$isVersionEqual" ]; then
+        echo >&2 "Upgrade process is finished. EspoCRM version is $installedVersion."
+        return
+    fi
+
+    echo >&2 "Start upgrading process from version $installedVersion."
+
+    if ! runUpgradeStep ; then
+        return
+    fi
+
+    actionUpgrade
+}
+
+runUpgradeStep() {
+    local result=$(php command.php upgrade -y --toVersion="$ESPOCRM_VERSION")
+
+    if [[ "$result" == *"Error:"* ]]; then
+        echo >&2 "error: Upgrade error, more details:"
+        echo >&2 "$result"
+
+        return 1 #false
+    fi
+
+    return 0 #true
+}
+
+installEspocrm() {
+    echo >&2 "Start EspoCRM installation"
+
+    find . -type d -exec chmod 755 {} + && find . -type f -exec chmod 644 {} +
+    find data custom/Espo/Custom client/custom -type d -exec chmod 775 {} + && find data custom/Espo/Custom client/custom -type f -exec chmod 664 {} +
+    chmod 775 application/Espo/Modules client/modules
+
+    declare -a preferences=()
+    for optionName in "${!OPTIONAL_PARAMS[@]}"
+    do
+        local varName="${OPTIONAL_PARAMS[$optionName]}"
+        if [ -n "${!varName-}" ]; then
+            preferences+=("${optionName}=${!varName}")
+        fi
+    done
+
+    runInstallationStep "step1" "user-lang=${ESPOCRM_LANGUAGE}"
+
+    local databaseHost="${ESPOCRM_DATABASE_HOST}"
+
+    if [ -n "$ESPOCRM_DATABASE_PORT" ]; then
+        databaseHost="${ESPOCRM_DATABASE_HOST}:${ESPOCRM_DATABASE_PORT}"
+    fi
+
+    for i in {1..20}
+    do
+        settingsTestResult=$(runInstallationStep "settingsTest" "dbPlatform=${ESPOCRM_DATABASE_PLATFORM}&hostName=${databaseHost}&dbName=${ESPOCRM_DATABASE_NAME}&dbUserName=${ESPOCRM_DATABASE_USER}&dbUserPass=${ESPOCRM_DATABASE_PASSWORD}" true 2>&1)
+
+        if [[ ! "$settingsTestResult" == *"Error:"* ]]; then
+            break
+        fi
+
+        sleep 5
+    done
+
+    if [[ "$settingsTestResult" == *"Error:"* ]] && [[ "$settingsTestResult" == *"[errorCode] => 2002"* ]]; then
+        echo >&2 "warning: Unable connect to Database server. Continuing anyway"
+        return
+    fi
+
+    runInstallationStep "setupConfirmation" "db-platform=${ESPOCRM_DATABASE_PLATFORM}&host-name=${databaseHost}&db-name=${ESPOCRM_DATABASE_NAME}&db-user-name=${ESPOCRM_DATABASE_USER}&db-user-password=${ESPOCRM_DATABASE_PASSWORD}"
+    runInstallationStep "checkPermission"
+    runInstallationStep "saveSettings" "site-url=${ESPOCRM_SITE_URL}&default-permissions-user=${DEFAULT_OWNER}&default-permissions-group=${DEFAULT_GROUP}"
+    runInstallationStep "buildDatabase"
+    runInstallationStep "createUser" "user-name=${ESPOCRM_ADMIN_USERNAME}&user-pass=${ESPOCRM_ADMIN_PASSWORD}"
+    runInstallationStep "savePreferences" "$(join '&' "${preferences[@]}")"
+    runInstallationStep "finish"
+
+    saveConfigParam "jobRunInParallel" "true"
+
+    echo >&2 "End EspoCRM installation"
+}
+
+runInstallationStep() {
+    local actionName="$1"
+    local returnResult=${3-false}
+
+    if [ -n "${2-}" ]; then
+        local data="$2"
+        local result=$(php install/cli.php -a "$actionName" -d "$data")
+    else
+        local result=$(php install/cli.php -a "$actionName")
+    fi
+
+    if [ "$returnResult" = true ]; then
+        echo >&2 "$result"
+        return
+    fi
+
+    if [[ "$result" == *"Error:"* ]]; then
+        echo >&2 "error: Installation error, more details:"
+        echo >&2 "$result"
+        exit 1
+    fi
+}
+
+# ------------------------- START -------------------------------------
+# Global variables
+DOCUMENT_ROOT="/var/www/html"
+SOURCE_FILES="/usr/src/espocrm"
+MAX_UPGRADE_COUNT=20
+DEFAULT_OWNER="www-data"
+DEFAULT_GROUP="www-data"
+
+if [ "$(id -u)" = '0' ]; then
+    if [[ "$1" == "apache2"* ]]; then
+        wrongSymbol='#'
+        DEFAULT_OWNER="${APACHE_RUN_USER:-www-data}"
+        DEFAULT_OWNER="${DEFAULT_OWNER#$wrongSymbol}"
+
+        DEFAULT_GROUP="${APACHE_RUN_GROUP:-www-data}"
+        DEFAULT_GROUP="${DEFAULT_GROUP#$wrongSymbol}"
+    fi
+else
+	DEFAULT_OWNER="$(id -u)"
+	DEFAULT_GROUP="$(id -g)"
+fi
+
+declare -A DEFAULTS=(
+    ['ESPOCRM_DATABASE_PLATFORM']='Mysql'
+    ['ESPOCRM_DATABASE_HOST']='mysql'
+    ['ESPOCRM_DATABASE_PORT']=''
+    ['ESPOCRM_DATABASE_NAME']='espocrm'
+    ['ESPOCRM_DATABASE_USER']='root'
+    ['ESPOCRM_DATABASE_PASSWORD']='password'
+    ['ESPOCRM_ADMIN_USERNAME']='admin'
+    ['ESPOCRM_ADMIN_PASSWORD']='password'
+    ['ESPOCRM_LANGUAGE']='en_US'
+    ['ESPOCRM_SITE_URL']='http://localhost'
+)
+
+declare -A OPTIONAL_PARAMS=(
+    ['language']='ESPOCRM_LANGUAGE'
+    ['dateFormat']='ESPOCRM_DATE_FORMAT'
+    ['timeFormat']='ESPOCRM_TIME_FORMAT'
+    ['timeZone']='ESPOCRM_TIME_ZONE'
+    ['weekStart']='ESPOCRM_WEEK_START'
+    ['defaultCurrency']='ESPOCRM_DEFAULT_CURRENCY'
+    ['thousandSeparator']='ESPOCRM_THOUSAND_SEPARATOR'
+    ['decimalMark']='ESPOCRM_DECIMAL_MARK'
+)
+
+for defaultParam in "${!DEFAULTS[@]}"
+do
+    if [ -z "${!defaultParam-}" ]; then
+        declare "${defaultParam}"="${DEFAULTS[$defaultParam]}"
+    fi
+done
+
+installationType=$(installationType)
+
+case $installationType in
+    install)
+        echo >&2 "Run \"install\" action."
+        actionInstall
+        chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        ;;
+
+    reinstall)
+        echo >&2 "Run \"reinstall\" action."
+        actionReinstall
+        chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        ;;
+
+    upgrade)
+        echo >&2 "Run \"upgrade\" action."
+
+        if verifyDatabaseReady ; then
+            UPGRADE_NUMBER=0
+            actionUpgrade
+            chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        else
+            echo "error: Unable to upgrade the instance. Starting the current version."
+        fi
+        ;;
+
+    skip)
+        ;;
+
+    *)
+        echo >&2 "error: Unknown installation type [$installationType]"
+        exit 1
+        ;;
+esac
+
+applyConfigEnvironments
+# ------------------------- END -------------------------------------
+
+exec "$@"
diff --git a/espocrm_8-fpm-alpine/docker-websocket.sh b/espocrm_8-fpm-alpine/docker-websocket.sh
new file mode 100755
index 0000000..acad517
--- /dev/null
+++ b/espocrm_8-fpm-alpine/docker-websocket.sh
@@ -0,0 +1,324 @@
+#!/bin/bash
+
+set -eu
+
+DOCUMENT_ROOT="/var/www/html"
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+checkInstanceReady
+
+applyConfigEnvironments
+
+/usr/local/bin/php /var/www/html/websocket.php
+
+exec "$@"
\ No newline at end of file
diff --git a/espocrm_8-fpm/Dockerfile b/espocrm_8-fpm/Dockerfile
new file mode 100644
index 0000000..bed9d47
--- /dev/null
+++ b/espocrm_8-fpm/Dockerfile
@@ -0,0 +1,110 @@
+FROM php:8.2-fpm
+
+LABEL org.opencontainers.image.source=https://github.com/espocrm/espocrm
+LABEL org.opencontainers.image.description="EspoCRM is an Open Source CRM. Try for Free."
+
+RUN set -ex; \
+    \
+    aptMarkList="$(apt-mark showmanual)"; \
+    \
+    apt-get update; \
+    # Install php libs
+    apt-get install -y --no-install-recommends \
+        libpq-dev \
+        libjpeg-dev \
+        libpng-dev \
+        libmagickwand-dev \
+        libwebp-dev \
+        libfreetype6-dev \
+        libzip-dev \
+        libxml2-dev \
+        libc-client-dev \
+        libkrb5-dev \
+        libldap2-dev \
+        libzmq5-dev \
+        zlib1g-dev \
+    ; \
+    \
+# Install php-zmq
+    cd /usr; \
+    curl -fSL https://github.com/zeromq/php-zmq/archive/ee5fbc693f07b2d6f0d9fd748f131be82310f386.tar.gz -o php-zmq.tar.gz; \
+    tar -zxf php-zmq.tar.gz; \
+    cd php-zmq*; \
+    phpize && ./configure; \
+    make; \
+    make install; \
+    cd .. && rm -rf php-zmq*; \
+# END: Install php-zmq
+    \
+    debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \
+    docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \
+    docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp; \
+    PHP_OPENSSL=yes docker-php-ext-configure imap --with-kerberos --with-imap-ssl; \
+    \
+    docker-php-ext-install \
+        pdo_pgsql \
+        pdo_mysql \
+        zip \
+        gd \
+        imap \
+        ldap \
+        exif \
+        pcntl \
+        posix \
+        bcmath \
+    ; \
+    docker-php-ext-enable \
+        zmq \
+    ; \
+    \
+# reset a list of apt-mark
+    apt-mark auto '.*' > /dev/null; \
+    apt-mark manual $aptMarkList; \
+    ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \
+        | awk '/=>/ { print $3 }' \
+        | sort -u \
+        | xargs -r realpath | xargs -r dpkg-query --search \
+        | cut -d: -f1 \
+        | sort -u \
+        | xargs -rt apt-mark manual; \
+    \
+    apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
+    # Install required libs
+    apt-get install -y --no-install-recommends \
+        unzip \
+        libldap-common \
+    ; \
+    rm -rf /var/lib/apt/lists/*
+
+# php.ini
+RUN { \
+	echo 'expose_php = Off'; \
+	echo 'display_errors = Off'; \
+	echo 'display_startup_errors = Off'; \
+	echo 'log_errors = On'; \
+	echo 'memory_limit=256M'; \
+	echo 'max_execution_time=180'; \
+	echo 'max_input_time=180'; \
+	echo 'post_max_size=30M'; \
+	echo 'upload_max_filesize=30M'; \
+	echo 'date.timezone=UTC'; \
+} > ${PHP_INI_DIR}/conf.d/espocrm.ini
+
+ENV ESPOCRM_VERSION 8.4.1
+ENV ESPOCRM_SHA256 1681a2f68c0fc37bd46bbb9725765ed0cf16fab48a283820efb90265a7e8301d
+
+WORKDIR /var/www/html
+
+RUN set -ex; \
+    curl -fSL "https://www.espocrm.com/downloads/EspoCRM-8.4.1.zip" -o EspoCRM.zip; \
+	echo "${ESPOCRM_SHA256} *EspoCRM.zip" | sha256sum -c -; \
+    unzip -q EspoCRM.zip -d /usr/src; \
+    mv "/usr/src/EspoCRM-8.4.1" /usr/src/espocrm; \
+	rm EspoCRM.zip; \
+    chown -R www-data:www-data /usr/src/espocrm
+
+COPY ./docker-*.sh  /usr/local/bin/
+
+ENTRYPOINT [ "docker-entrypoint.sh" ]
+
+CMD ["php-fpm"]
diff --git a/espocrm_8-fpm/docker-daemon.sh b/espocrm_8-fpm/docker-daemon.sh
new file mode 100755
index 0000000..83a2b4f
--- /dev/null
+++ b/espocrm_8-fpm/docker-daemon.sh
@@ -0,0 +1,324 @@
+#!/bin/bash
+
+set -eu
+
+DOCUMENT_ROOT="/var/www/html"
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+checkInstanceReady
+
+applyConfigEnvironments
+
+/usr/local/bin/php /var/www/html/daemon.php
+
+exec "$@"
\ No newline at end of file
diff --git a/espocrm_8-fpm/docker-entrypoint.sh b/espocrm_8-fpm/docker-entrypoint.sh
new file mode 100755
index 0000000..acc13a4
--- /dev/null
+++ b/espocrm_8-fpm/docker-entrypoint.sh
@@ -0,0 +1,565 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+installationType() {
+    if [ -f "$DOCUMENT_ROOT/data/config.php" ]; then
+        local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+        if [ -n "$isInstalled" ] && [ "$isInstalled" = 1 ]; then
+            local installedVersion=$(getConfigParamFromFile "version")
+            local isVersionGreater=$(compareVersion "$ESPOCRM_VERSION" "$installedVersion" ">")
+
+            if [ -n "$isVersionGreater" ]; then
+                echo "upgrade"
+                return
+            fi
+
+            echo "skip"
+            return
+        fi
+
+        echo "reinstall"
+        return
+    fi
+
+    echo "install"
+}
+
+actionInstall() {
+    if [ ! -d "$SOURCE_FILES" ]; then
+        echo >&2 "error: Source files [$SOURCE_FILES] are not found."
+        exit 1
+    fi
+
+    cp -a "$SOURCE_FILES/." "$DOCUMENT_ROOT/"
+
+    installEspocrm
+}
+
+actionReinstall() {
+    if [ -f "$DOCUMENT_ROOT/install/config.php" ]; then
+        sed -i "s/'isInstalled' => true/'isInstalled' => false/g" "$DOCUMENT_ROOT/install/config.php"
+    fi
+
+    installEspocrm
+}
+
+actionUpgrade() {
+    UPGRADE_NUMBER=$((UPGRADE_NUMBER+1))
+
+    if [ $UPGRADE_NUMBER -gt $MAX_UPGRADE_COUNT ];then
+        echo >&2 "The MAX_UPGRADE_COUNT exceded. The upgrading process has been stopped."
+        return
+    fi
+
+    local installedVersion=$(getConfigParamFromFile "version")
+    local isVersionEqual=$(compareVersion "$installedVersion" "$ESPOCRM_VERSION" ">=")
+
+    if [ -n "$isVersionEqual" ]; then
+        echo >&2 "Upgrade process is finished. EspoCRM version is $installedVersion."
+        return
+    fi
+
+    echo >&2 "Start upgrading process from version $installedVersion."
+
+    if ! runUpgradeStep ; then
+        return
+    fi
+
+    actionUpgrade
+}
+
+runUpgradeStep() {
+    local result=$(php command.php upgrade -y --toVersion="$ESPOCRM_VERSION")
+
+    if [[ "$result" == *"Error:"* ]]; then
+        echo >&2 "error: Upgrade error, more details:"
+        echo >&2 "$result"
+
+        return 1 #false
+    fi
+
+    return 0 #true
+}
+
+installEspocrm() {
+    echo >&2 "Start EspoCRM installation"
+
+    find . -type d -exec chmod 755 {} + && find . -type f -exec chmod 644 {} +
+    find data custom/Espo/Custom client/custom -type d -exec chmod 775 {} + && find data custom/Espo/Custom client/custom -type f -exec chmod 664 {} +
+    chmod 775 application/Espo/Modules client/modules
+
+    declare -a preferences=()
+    for optionName in "${!OPTIONAL_PARAMS[@]}"
+    do
+        local varName="${OPTIONAL_PARAMS[$optionName]}"
+        if [ -n "${!varName-}" ]; then
+            preferences+=("${optionName}=${!varName}")
+        fi
+    done
+
+    runInstallationStep "step1" "user-lang=${ESPOCRM_LANGUAGE}"
+
+    local databaseHost="${ESPOCRM_DATABASE_HOST}"
+
+    if [ -n "$ESPOCRM_DATABASE_PORT" ]; then
+        databaseHost="${ESPOCRM_DATABASE_HOST}:${ESPOCRM_DATABASE_PORT}"
+    fi
+
+    for i in {1..20}
+    do
+        settingsTestResult=$(runInstallationStep "settingsTest" "dbPlatform=${ESPOCRM_DATABASE_PLATFORM}&hostName=${databaseHost}&dbName=${ESPOCRM_DATABASE_NAME}&dbUserName=${ESPOCRM_DATABASE_USER}&dbUserPass=${ESPOCRM_DATABASE_PASSWORD}" true 2>&1)
+
+        if [[ ! "$settingsTestResult" == *"Error:"* ]]; then
+            break
+        fi
+
+        sleep 5
+    done
+
+    if [[ "$settingsTestResult" == *"Error:"* ]] && [[ "$settingsTestResult" == *"[errorCode] => 2002"* ]]; then
+        echo >&2 "warning: Unable connect to Database server. Continuing anyway"
+        return
+    fi
+
+    runInstallationStep "setupConfirmation" "db-platform=${ESPOCRM_DATABASE_PLATFORM}&host-name=${databaseHost}&db-name=${ESPOCRM_DATABASE_NAME}&db-user-name=${ESPOCRM_DATABASE_USER}&db-user-password=${ESPOCRM_DATABASE_PASSWORD}"
+    runInstallationStep "checkPermission"
+    runInstallationStep "saveSettings" "site-url=${ESPOCRM_SITE_URL}&default-permissions-user=${DEFAULT_OWNER}&default-permissions-group=${DEFAULT_GROUP}"
+    runInstallationStep "buildDatabase"
+    runInstallationStep "createUser" "user-name=${ESPOCRM_ADMIN_USERNAME}&user-pass=${ESPOCRM_ADMIN_PASSWORD}"
+    runInstallationStep "savePreferences" "$(join '&' "${preferences[@]}")"
+    runInstallationStep "finish"
+
+    saveConfigParam "jobRunInParallel" "true"
+
+    echo >&2 "End EspoCRM installation"
+}
+
+runInstallationStep() {
+    local actionName="$1"
+    local returnResult=${3-false}
+
+    if [ -n "${2-}" ]; then
+        local data="$2"
+        local result=$(php install/cli.php -a "$actionName" -d "$data")
+    else
+        local result=$(php install/cli.php -a "$actionName")
+    fi
+
+    if [ "$returnResult" = true ]; then
+        echo >&2 "$result"
+        return
+    fi
+
+    if [[ "$result" == *"Error:"* ]]; then
+        echo >&2 "error: Installation error, more details:"
+        echo >&2 "$result"
+        exit 1
+    fi
+}
+
+# ------------------------- START -------------------------------------
+# Global variables
+DOCUMENT_ROOT="/var/www/html"
+SOURCE_FILES="/usr/src/espocrm"
+MAX_UPGRADE_COUNT=20
+DEFAULT_OWNER="www-data"
+DEFAULT_GROUP="www-data"
+
+if [ "$(id -u)" = '0' ]; then
+    if [[ "$1" == "apache2"* ]]; then
+        wrongSymbol='#'
+        DEFAULT_OWNER="${APACHE_RUN_USER:-www-data}"
+        DEFAULT_OWNER="${DEFAULT_OWNER#$wrongSymbol}"
+
+        DEFAULT_GROUP="${APACHE_RUN_GROUP:-www-data}"
+        DEFAULT_GROUP="${DEFAULT_GROUP#$wrongSymbol}"
+    fi
+else
+	DEFAULT_OWNER="$(id -u)"
+	DEFAULT_GROUP="$(id -g)"
+fi
+
+declare -A DEFAULTS=(
+    ['ESPOCRM_DATABASE_PLATFORM']='Mysql'
+    ['ESPOCRM_DATABASE_HOST']='mysql'
+    ['ESPOCRM_DATABASE_PORT']=''
+    ['ESPOCRM_DATABASE_NAME']='espocrm'
+    ['ESPOCRM_DATABASE_USER']='root'
+    ['ESPOCRM_DATABASE_PASSWORD']='password'
+    ['ESPOCRM_ADMIN_USERNAME']='admin'
+    ['ESPOCRM_ADMIN_PASSWORD']='password'
+    ['ESPOCRM_LANGUAGE']='en_US'
+    ['ESPOCRM_SITE_URL']='http://localhost'
+)
+
+declare -A OPTIONAL_PARAMS=(
+    ['language']='ESPOCRM_LANGUAGE'
+    ['dateFormat']='ESPOCRM_DATE_FORMAT'
+    ['timeFormat']='ESPOCRM_TIME_FORMAT'
+    ['timeZone']='ESPOCRM_TIME_ZONE'
+    ['weekStart']='ESPOCRM_WEEK_START'
+    ['defaultCurrency']='ESPOCRM_DEFAULT_CURRENCY'
+    ['thousandSeparator']='ESPOCRM_THOUSAND_SEPARATOR'
+    ['decimalMark']='ESPOCRM_DECIMAL_MARK'
+)
+
+for defaultParam in "${!DEFAULTS[@]}"
+do
+    if [ -z "${!defaultParam-}" ]; then
+        declare "${defaultParam}"="${DEFAULTS[$defaultParam]}"
+    fi
+done
+
+installationType=$(installationType)
+
+case $installationType in
+    install)
+        echo >&2 "Run \"install\" action."
+        actionInstall
+        chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        ;;
+
+    reinstall)
+        echo >&2 "Run \"reinstall\" action."
+        actionReinstall
+        chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        ;;
+
+    upgrade)
+        echo >&2 "Run \"upgrade\" action."
+
+        if verifyDatabaseReady ; then
+            UPGRADE_NUMBER=0
+            actionUpgrade
+            chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        else
+            echo "error: Unable to upgrade the instance. Starting the current version."
+        fi
+        ;;
+
+    skip)
+        ;;
+
+    *)
+        echo >&2 "error: Unknown installation type [$installationType]"
+        exit 1
+        ;;
+esac
+
+applyConfigEnvironments
+# ------------------------- END -------------------------------------
+
+exec "$@"
diff --git a/espocrm_8-fpm/docker-websocket.sh b/espocrm_8-fpm/docker-websocket.sh
new file mode 100755
index 0000000..acad517
--- /dev/null
+++ b/espocrm_8-fpm/docker-websocket.sh
@@ -0,0 +1,324 @@
+#!/bin/bash
+
+set -eu
+
+DOCUMENT_ROOT="/var/www/html"
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+checkInstanceReady
+
+applyConfigEnvironments
+
+/usr/local/bin/php /var/www/html/websocket.php
+
+exec "$@"
\ No newline at end of file
diff --git a/espocrm_8/Dockerfile b/espocrm_8/Dockerfile
new file mode 100644
index 0000000..1d5ba38
--- /dev/null
+++ b/espocrm_8/Dockerfile
@@ -0,0 +1,112 @@
+FROM php:8.2-apache
+
+LABEL org.opencontainers.image.source=https://github.com/espocrm/espocrm
+LABEL org.opencontainers.image.description="EspoCRM is an Open Source CRM. Try for Free."
+
+RUN set -ex; \
+    \
+    aptMarkList="$(apt-mark showmanual)"; \
+    \
+    apt-get update; \
+    # Install php libs
+    apt-get install -y --no-install-recommends \
+        libpq-dev \
+        libjpeg-dev \
+        libpng-dev \
+        libmagickwand-dev \
+        libwebp-dev \
+        libfreetype6-dev \
+        libzip-dev \
+        libxml2-dev \
+        libc-client-dev \
+        libkrb5-dev \
+        libldap2-dev \
+        libzmq5-dev \
+        zlib1g-dev \
+    ; \
+    \
+# Install php-zmq
+    cd /usr; \
+    curl -fSL https://github.com/zeromq/php-zmq/archive/ee5fbc693f07b2d6f0d9fd748f131be82310f386.tar.gz -o php-zmq.tar.gz; \
+    tar -zxf php-zmq.tar.gz; \
+    cd php-zmq*; \
+    phpize && ./configure; \
+    make; \
+    make install; \
+    cd .. && rm -rf php-zmq*; \
+# END: Install php-zmq
+    \
+    debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \
+    docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \
+    docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp; \
+    PHP_OPENSSL=yes docker-php-ext-configure imap --with-kerberos --with-imap-ssl; \
+    \
+    docker-php-ext-install \
+        pdo_pgsql \
+        pdo_mysql \
+        zip \
+        gd \
+        imap \
+        ldap \
+        exif \
+        pcntl \
+        posix \
+        bcmath \
+    ; \
+    docker-php-ext-enable \
+        zmq \
+    ; \
+    \
+# reset a list of apt-mark
+    apt-mark auto '.*' > /dev/null; \
+    apt-mark manual $aptMarkList; \
+    ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \
+        | awk '/=>/ { print $3 }' \
+        | sort -u \
+        | xargs -r realpath | xargs -r dpkg-query --search \
+        | cut -d: -f1 \
+        | sort -u \
+        | xargs -rt apt-mark manual; \
+    \
+    apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
+    # Install required libs
+    apt-get install -y --no-install-recommends \
+        unzip \
+        libldap-common \
+    ; \
+    rm -rf /var/lib/apt/lists/*
+
+# php.ini
+RUN { \
+	echo 'expose_php = Off'; \
+	echo 'display_errors = Off'; \
+	echo 'display_startup_errors = Off'; \
+	echo 'log_errors = On'; \
+	echo 'memory_limit=256M'; \
+	echo 'max_execution_time=180'; \
+	echo 'max_input_time=180'; \
+	echo 'post_max_size=30M'; \
+	echo 'upload_max_filesize=30M'; \
+	echo 'date.timezone=UTC'; \
+} > ${PHP_INI_DIR}/conf.d/espocrm.ini
+
+RUN a2enmod rewrite;
+
+ENV ESPOCRM_VERSION 8.4.1
+ENV ESPOCRM_SHA256 1681a2f68c0fc37bd46bbb9725765ed0cf16fab48a283820efb90265a7e8301d
+
+WORKDIR /var/www/html
+
+RUN set -ex; \
+    curl -fSL "https://www.espocrm.com/downloads/EspoCRM-8.4.1.zip" -o EspoCRM.zip; \
+	echo "${ESPOCRM_SHA256} *EspoCRM.zip" | sha256sum -c -; \
+    unzip -q EspoCRM.zip -d /usr/src; \
+    mv "/usr/src/EspoCRM-8.4.1" /usr/src/espocrm; \
+	rm EspoCRM.zip; \
+    chown -R www-data:www-data /usr/src/espocrm
+
+COPY ./docker-*.sh  /usr/local/bin/
+
+ENTRYPOINT [ "docker-entrypoint.sh" ]
+
+CMD ["apache2-foreground"]
diff --git a/espocrm_8/docker-daemon.sh b/espocrm_8/docker-daemon.sh
new file mode 100755
index 0000000..83a2b4f
--- /dev/null
+++ b/espocrm_8/docker-daemon.sh
@@ -0,0 +1,324 @@
+#!/bin/bash
+
+set -eu
+
+DOCUMENT_ROOT="/var/www/html"
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+checkInstanceReady
+
+applyConfigEnvironments
+
+/usr/local/bin/php /var/www/html/daemon.php
+
+exec "$@"
\ No newline at end of file
diff --git a/espocrm_8/docker-entrypoint.sh b/espocrm_8/docker-entrypoint.sh
new file mode 100755
index 0000000..acc13a4
--- /dev/null
+++ b/espocrm_8/docker-entrypoint.sh
@@ -0,0 +1,565 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+installationType() {
+    if [ -f "$DOCUMENT_ROOT/data/config.php" ]; then
+        local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+        if [ -n "$isInstalled" ] && [ "$isInstalled" = 1 ]; then
+            local installedVersion=$(getConfigParamFromFile "version")
+            local isVersionGreater=$(compareVersion "$ESPOCRM_VERSION" "$installedVersion" ">")
+
+            if [ -n "$isVersionGreater" ]; then
+                echo "upgrade"
+                return
+            fi
+
+            echo "skip"
+            return
+        fi
+
+        echo "reinstall"
+        return
+    fi
+
+    echo "install"
+}
+
+actionInstall() {
+    if [ ! -d "$SOURCE_FILES" ]; then
+        echo >&2 "error: Source files [$SOURCE_FILES] are not found."
+        exit 1
+    fi
+
+    cp -a "$SOURCE_FILES/." "$DOCUMENT_ROOT/"
+
+    installEspocrm
+}
+
+actionReinstall() {
+    if [ -f "$DOCUMENT_ROOT/install/config.php" ]; then
+        sed -i "s/'isInstalled' => true/'isInstalled' => false/g" "$DOCUMENT_ROOT/install/config.php"
+    fi
+
+    installEspocrm
+}
+
+actionUpgrade() {
+    UPGRADE_NUMBER=$((UPGRADE_NUMBER+1))
+
+    if [ $UPGRADE_NUMBER -gt $MAX_UPGRADE_COUNT ];then
+        echo >&2 "The MAX_UPGRADE_COUNT exceded. The upgrading process has been stopped."
+        return
+    fi
+
+    local installedVersion=$(getConfigParamFromFile "version")
+    local isVersionEqual=$(compareVersion "$installedVersion" "$ESPOCRM_VERSION" ">=")
+
+    if [ -n "$isVersionEqual" ]; then
+        echo >&2 "Upgrade process is finished. EspoCRM version is $installedVersion."
+        return
+    fi
+
+    echo >&2 "Start upgrading process from version $installedVersion."
+
+    if ! runUpgradeStep ; then
+        return
+    fi
+
+    actionUpgrade
+}
+
+runUpgradeStep() {
+    local result=$(php command.php upgrade -y --toVersion="$ESPOCRM_VERSION")
+
+    if [[ "$result" == *"Error:"* ]]; then
+        echo >&2 "error: Upgrade error, more details:"
+        echo >&2 "$result"
+
+        return 1 #false
+    fi
+
+    return 0 #true
+}
+
+installEspocrm() {
+    echo >&2 "Start EspoCRM installation"
+
+    find . -type d -exec chmod 755 {} + && find . -type f -exec chmod 644 {} +
+    find data custom/Espo/Custom client/custom -type d -exec chmod 775 {} + && find data custom/Espo/Custom client/custom -type f -exec chmod 664 {} +
+    chmod 775 application/Espo/Modules client/modules
+
+    declare -a preferences=()
+    for optionName in "${!OPTIONAL_PARAMS[@]}"
+    do
+        local varName="${OPTIONAL_PARAMS[$optionName]}"
+        if [ -n "${!varName-}" ]; then
+            preferences+=("${optionName}=${!varName}")
+        fi
+    done
+
+    runInstallationStep "step1" "user-lang=${ESPOCRM_LANGUAGE}"
+
+    local databaseHost="${ESPOCRM_DATABASE_HOST}"
+
+    if [ -n "$ESPOCRM_DATABASE_PORT" ]; then
+        databaseHost="${ESPOCRM_DATABASE_HOST}:${ESPOCRM_DATABASE_PORT}"
+    fi
+
+    for i in {1..20}
+    do
+        settingsTestResult=$(runInstallationStep "settingsTest" "dbPlatform=${ESPOCRM_DATABASE_PLATFORM}&hostName=${databaseHost}&dbName=${ESPOCRM_DATABASE_NAME}&dbUserName=${ESPOCRM_DATABASE_USER}&dbUserPass=${ESPOCRM_DATABASE_PASSWORD}" true 2>&1)
+
+        if [[ ! "$settingsTestResult" == *"Error:"* ]]; then
+            break
+        fi
+
+        sleep 5
+    done
+
+    if [[ "$settingsTestResult" == *"Error:"* ]] && [[ "$settingsTestResult" == *"[errorCode] => 2002"* ]]; then
+        echo >&2 "warning: Unable connect to Database server. Continuing anyway"
+        return
+    fi
+
+    runInstallationStep "setupConfirmation" "db-platform=${ESPOCRM_DATABASE_PLATFORM}&host-name=${databaseHost}&db-name=${ESPOCRM_DATABASE_NAME}&db-user-name=${ESPOCRM_DATABASE_USER}&db-user-password=${ESPOCRM_DATABASE_PASSWORD}"
+    runInstallationStep "checkPermission"
+    runInstallationStep "saveSettings" "site-url=${ESPOCRM_SITE_URL}&default-permissions-user=${DEFAULT_OWNER}&default-permissions-group=${DEFAULT_GROUP}"
+    runInstallationStep "buildDatabase"
+    runInstallationStep "createUser" "user-name=${ESPOCRM_ADMIN_USERNAME}&user-pass=${ESPOCRM_ADMIN_PASSWORD}"
+    runInstallationStep "savePreferences" "$(join '&' "${preferences[@]}")"
+    runInstallationStep "finish"
+
+    saveConfigParam "jobRunInParallel" "true"
+
+    echo >&2 "End EspoCRM installation"
+}
+
+runInstallationStep() {
+    local actionName="$1"
+    local returnResult=${3-false}
+
+    if [ -n "${2-}" ]; then
+        local data="$2"
+        local result=$(php install/cli.php -a "$actionName" -d "$data")
+    else
+        local result=$(php install/cli.php -a "$actionName")
+    fi
+
+    if [ "$returnResult" = true ]; then
+        echo >&2 "$result"
+        return
+    fi
+
+    if [[ "$result" == *"Error:"* ]]; then
+        echo >&2 "error: Installation error, more details:"
+        echo >&2 "$result"
+        exit 1
+    fi
+}
+
+# ------------------------- START -------------------------------------
+# Global variables
+DOCUMENT_ROOT="/var/www/html"
+SOURCE_FILES="/usr/src/espocrm"
+MAX_UPGRADE_COUNT=20
+DEFAULT_OWNER="www-data"
+DEFAULT_GROUP="www-data"
+
+if [ "$(id -u)" = '0' ]; then
+    if [[ "$1" == "apache2"* ]]; then
+        wrongSymbol='#'
+        DEFAULT_OWNER="${APACHE_RUN_USER:-www-data}"
+        DEFAULT_OWNER="${DEFAULT_OWNER#$wrongSymbol}"
+
+        DEFAULT_GROUP="${APACHE_RUN_GROUP:-www-data}"
+        DEFAULT_GROUP="${DEFAULT_GROUP#$wrongSymbol}"
+    fi
+else
+	DEFAULT_OWNER="$(id -u)"
+	DEFAULT_GROUP="$(id -g)"
+fi
+
+declare -A DEFAULTS=(
+    ['ESPOCRM_DATABASE_PLATFORM']='Mysql'
+    ['ESPOCRM_DATABASE_HOST']='mysql'
+    ['ESPOCRM_DATABASE_PORT']=''
+    ['ESPOCRM_DATABASE_NAME']='espocrm'
+    ['ESPOCRM_DATABASE_USER']='root'
+    ['ESPOCRM_DATABASE_PASSWORD']='password'
+    ['ESPOCRM_ADMIN_USERNAME']='admin'
+    ['ESPOCRM_ADMIN_PASSWORD']='password'
+    ['ESPOCRM_LANGUAGE']='en_US'
+    ['ESPOCRM_SITE_URL']='http://localhost'
+)
+
+declare -A OPTIONAL_PARAMS=(
+    ['language']='ESPOCRM_LANGUAGE'
+    ['dateFormat']='ESPOCRM_DATE_FORMAT'
+    ['timeFormat']='ESPOCRM_TIME_FORMAT'
+    ['timeZone']='ESPOCRM_TIME_ZONE'
+    ['weekStart']='ESPOCRM_WEEK_START'
+    ['defaultCurrency']='ESPOCRM_DEFAULT_CURRENCY'
+    ['thousandSeparator']='ESPOCRM_THOUSAND_SEPARATOR'
+    ['decimalMark']='ESPOCRM_DECIMAL_MARK'
+)
+
+for defaultParam in "${!DEFAULTS[@]}"
+do
+    if [ -z "${!defaultParam-}" ]; then
+        declare "${defaultParam}"="${DEFAULTS[$defaultParam]}"
+    fi
+done
+
+installationType=$(installationType)
+
+case $installationType in
+    install)
+        echo >&2 "Run \"install\" action."
+        actionInstall
+        chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        ;;
+
+    reinstall)
+        echo >&2 "Run \"reinstall\" action."
+        actionReinstall
+        chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        ;;
+
+    upgrade)
+        echo >&2 "Run \"upgrade\" action."
+
+        if verifyDatabaseReady ; then
+            UPGRADE_NUMBER=0
+            actionUpgrade
+            chown -R $DEFAULT_OWNER:$DEFAULT_GROUP "$DOCUMENT_ROOT"
+        else
+            echo "error: Unable to upgrade the instance. Starting the current version."
+        fi
+        ;;
+
+    skip)
+        ;;
+
+    *)
+        echo >&2 "error: Unknown installation type [$installationType]"
+        exit 1
+        ;;
+esac
+
+applyConfigEnvironments
+# ------------------------- END -------------------------------------
+
+exec "$@"
diff --git a/espocrm_8/docker-websocket.sh b/espocrm_8/docker-websocket.sh
new file mode 100755
index 0000000..acad517
--- /dev/null
+++ b/espocrm_8/docker-websocket.sh
@@ -0,0 +1,324 @@
+#!/bin/bash
+
+set -eu
+
+DOCUMENT_ROOT="/var/www/html"
+
+# entrypoint-utils.sh
+configPrefix="ESPOCRM_CONFIG_"
+
+declare -A configPrefixArrayList=(
+    [logger]="ESPOCRM_CONFIG_LOGGER_"
+    [database]="ESPOCRM_CONFIG_DATABASE_"
+)
+
+compareVersion() {
+    local version1="$1"
+    local version2="$2"
+    local operator="$3"
+
+    echo $(php -r "echo version_compare('$version1', '$version2', '$operator');")
+}
+
+join() {
+    local sep="$1"; shift
+    local out; printf -v out "${sep//%/%%}%s" "$@"
+    echo "${out#$sep}"
+}
+
+getConfigParamFromFile() {
+    local name="$1"
+
+    php -r "
+        if (file_exists('$DOCUMENT_ROOT/data/config-internal.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config-internal.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+
+        if (file_exists('$DOCUMENT_ROOT/data/config.php')) {
+            \$config=include('$DOCUMENT_ROOT/data/config.php');
+
+            if (array_key_exists('$name', \$config)) {
+                die(\$config['$name']);
+            }
+        }
+    "
+}
+
+getConfigParam() {
+    local name="$1"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        echo \$config->get('$name');
+    "
+}
+
+# Bool: saveConfigParam "jobRunInParallel" "true"
+# String: saveConfigParam "language" "'en_US'"
+saveConfigParam() {
+    local name="$1"
+    local value="$2"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        if (\$config->get('$name') === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$configWriter->set('$name', $value);
+        \$configWriter->save();
+    "
+}
+
+saveConfigArrayParam() {
+    local key1="$1"
+    local key2="$2"
+    local value="$3"
+
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$arrayValue = \$config->get('$key1') ?? [];
+
+        if (!is_array(\$arrayValue)) {
+            return;
+        }
+
+        if (array_key_exists('$key2', \$arrayValue) && \$arrayValue['$key2'] === $value) {
+            return;
+        }
+
+        \$injectableFactory = \$app->getContainer()->get('injectableFactory');
+        \$configWriter = \$injectableFactory->create('\\Espo\\Core\\Utils\\Config\\ConfigWriter');
+
+        \$arrayValue['$key2'] = $value;
+
+        \$configWriter->set('$key1', \$arrayValue);
+        \$configWriter->save();
+    "
+}
+
+checkInstanceReady() {
+    local isInstalled=$(getConfigParamFromFile "isInstalled")
+
+    if [ -z "$isInstalled" ] || [ "$isInstalled" != 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the installation"
+        exit 0
+    fi
+
+    local maintenanceMode=$(getConfigParamFromFile "maintenanceMode")
+
+    if [ -n "$maintenanceMode" ] && [ "$maintenanceMode" = 1 ]; then
+        echo >&2 "Instance is not ready: waiting for the upgrade"
+        exit 0
+    fi
+
+    if ! verifyDatabaseReady ; then
+        exit 0
+    fi
+}
+
+isDatabaseReady() {
+    php -r "
+        require_once('$DOCUMENT_ROOT/bootstrap.php');
+
+        \$app = new \Espo\Core\Application();
+        \$config = \$app->getContainer()->get('config');
+
+        \$helper = new \Espo\Core\Utils\Database\Helper(\$config);
+
+        try {
+            \$helper->createPdoConnection();
+        }
+        catch (Exception \$e) {
+            die(false);
+        }
+
+        die(true);
+    "
+}
+
+verifyDatabaseReady() {
+    for i in {1..40}
+    do
+        isReady=$(isDatabaseReady 2>&1)
+
+        if [ -n "$isReady" ]; then
+            return 0 #true
+        fi
+
+        echo >&2 "Waiting Database for receiving connections..."
+        sleep 3
+    done
+
+    echo >&2 "error: Database is not available"
+    return 1 #false
+}
+
+applyConfigEnvironments() {
+    local envName
+    local envValue
+    local configParamName
+    local configParamValue
+
+    compgen -v | while read -r envName; do
+
+        if [[ $envName != "$configPrefix"* ]]; then
+            continue
+        fi
+
+        envValue="${!envName}"
+
+        if isConfigArrayParam "$envName" ; then
+            saveConfigArrayValue "$envName" "$envValue"
+            continue
+        fi
+
+        saveConfigValue "$envName" "$envValue"
+
+    done
+}
+
+isValueQuoted() {
+    local value="$1"
+
+    php -r "
+        echo isQuote('$value');
+
+        function isQuote (\$value) {
+            \$value = trim(\$value);
+
+            if (\$value === '0') {
+                return false;
+            }
+
+            if (empty(\$value)) {
+                return true;
+            }
+
+            if (filter_var(\$value, FILTER_VALIDATE_IP)) {
+                return true;
+            }
+
+            if (!preg_match('/[^0-9.]+/', \$value)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['null', '1'], true)) {
+                return false;
+            }
+
+            if (in_array(\$value, ['true', 'false'])) {
+                return false;
+            }
+
+            return true;
+        }
+    "
+}
+
+normalizeConfigParamName() {
+    local value="$1"
+    local prefix=${2:-"$configPrefix"}
+
+    php -r "
+        \$value = str_ireplace('$prefix', '', '$value');
+        \$value = strtolower(\$value);
+
+        \$value = preg_replace_callback(
+            '/_([a-zA-Z])/',
+            function (\$matches) {
+                return strtoupper(\$matches[1]);
+            },
+            \$value
+        );
+
+        echo \$value;
+    "
+}
+
+normalizeConfigParamValue() {
+    local value=${1//\'/\\\'}
+
+    local isValueQuoted=$(isValueQuoted "$value")
+
+    if [ -n "$isValueQuoted" ] && [ "$isValueQuoted" = 1 ]; then
+        echo "'$value'"
+        return
+    fi
+
+    echo "$value"
+}
+
+isConfigArrayParam() {
+    local envName="$1"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        return 0 #true
+    done
+
+    return 1 #false
+}
+
+saveConfigValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    local key=$(normalizeConfigParamName "$envName")
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigParam "$key" "$value"
+}
+
+saveConfigArrayValue() {
+    local envName="$1"
+    local envValue="$2"
+
+    for i in "${!configPrefixArrayList[@]}"
+    do
+        if [[ "$envName" != ${configPrefixArrayList[$i]}* ]]; then
+            continue
+        fi
+
+        local key1="$i"
+        local key2=$(normalizeConfigParamName "$envName" "${configPrefixArrayList[$i]}")
+
+        break
+    done
+
+    local value=$(normalizeConfigParamValue "$envValue")
+
+    saveConfigArrayParam "$key1" "$key2" "$value"
+}
+# END: entrypoint-utils.sh
+
+checkInstanceReady
+
+applyConfigEnvironments
+
+/usr/local/bin/php /var/www/html/websocket.php
+
+exec "$@"
\ No newline at end of file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants