diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..980ef21 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcd10d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.jq-template.awk +.idea +.versions-helper.json diff --git a/10.2/php7.4/apache/Dockerfile b/10.2/php7.4/apache/Dockerfile new file mode 100644 index 0000000..2dfe520 --- /dev/null +++ b/10.2/php7.4/apache/Dockerfile @@ -0,0 +1,178 @@ +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + + +FROM php:7.4-apache +LABEL maintainer="François Jacquet (@francoisjacquet), Llewellyn van der Merwe (@Llewellynvdm)" + +# Disable remote database security requirements. +ENV ROSARIOSIS_INSTALLATION_DISABLE_LOCALHOST_CHECK=1 +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ +# To be able to send mail + sendmail \ + nano \ + locales \ + ; \ + rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need. +RUN set -ex; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libbz2-dev \ + libxml2-dev \ + libgmp-dev \ + libicu-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libldap2-dev \ + libmemcached-dev \ + libmagickwand-dev \ + wkhtmltopdf \ + libpq-dev \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + libonig-dev \ + postgresql-client \ + ; \ + \ + docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + ; \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \ + docker-php-ext-install -j "$(nproc)" \ + bz2 \ + bcmath \ + exif \ + gd \ + mbstring \ + xml \ + xmlrpc \ + gmp \ + intl \ + ldap \ + mysqli \ + pdo_mysql \ + pdo_pgsql \ + pgsql \ + gettext \ + zip \ + ; \ + rm -r /tmp/pear; \ + \ +# some misbehaving extensions end up outputting to stdout + out="$(php -r 'exit(0);')"; \ + [ -z "$out" ]; \ + err="$(php -r 'exit(0);' 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ]; \ + \ + extDir="$(php -r 'echo ini_get("extension_dir");')"; \ + [ -d "$extDir" ]; \ +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-5.1.21; \ + pecl install memcached-3.2.0; \ + \ + docker-php-ext-enable \ + apcu \ + memcached \ + ; \ + rm -r /tmp/pear; \ + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$extDir"/*.so \ + | awk '/=>/ { print $3 }' \ + | sort -u \ + | xargs -r dpkg-query -S \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ + ! { ldd "$extDir"/*.so | grep 'not found'; }; \ +# check for output like "PHP Warning: PHP Startup: Unable to load dynamic library 'foo' (tried: ...) + err="$(php --version 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ] + +# set recommended PHP.ini settings +# see https://secure.php.net/manual/en/opcache.installation.php +RUN set -eux; \ + docker-php-ext-enable opcache; \ + { \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=4000'; \ + echo 'opcache.revalidate_freq=2'; \ + echo 'opcache.fast_shutdown=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini +# set recommended error logging +RUN { \ +# https://www.php.net/manual/en/errorfunc.constants.php + echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \ + echo 'display_errors = Off'; \ + echo 'display_startup_errors = Off'; \ + echo 'log_errors = On'; \ + echo 'error_log = /dev/stderr'; \ + echo 'log_errors_max_len = 1024'; \ + echo 'ignore_repeated_errors = On'; \ + echo 'ignore_repeated_source = Off'; \ + echo 'html_errors = Off'; \ + } > /usr/local/etc/php/conf.d/error-logging.ini + +RUN set -eux; \ + a2enmod rewrite expires; \ + \ +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html + a2enmod remoteip; \ + { \ + echo 'RemoteIPHeader X-Forwarded-For'; \ +# these IP ranges are reserved for "private" use and should thus *usually* be safe inside Docker + echo 'RemoteIPTrustedProxy 10.0.0.0/8'; \ + echo 'RemoteIPTrustedProxy 172.16.0.0/12'; \ + echo 'RemoteIPTrustedProxy 192.168.0.0/16'; \ + echo 'RemoteIPTrustedProxy 169.254.0.0/16'; \ + echo 'RemoteIPTrustedProxy 127.0.0.0/8'; \ + } > /etc/apache2/conf-available/remoteip.conf; \ + a2enconf remoteip; \ +# (replace all instances of "%h" with "%a" in LogFormat) + find /etc/apache2 -type f -name '*.conf' -exec sed -ri 's/([[:space:]]*LogFormat[[:space:]]+"[^"]*)%h([^"]*")/\1%a\2/g' '{}' + + +VOLUME /var/www/html + +# Define Rosariosis version and expected SHA512 signature +ENV ROSARIOSIS_VERSION 10.2 +ENV ROSARIOSIS_SHA512 ffc49aac888c39670d8b30a362c5e8becf71abccd26cd72a7e0392adfd67b6887c2ee365490c4cfd0fad8b87f00ff27573e18d99f530e2d131515894ef044c96 + +# Download package and extract to web volume +RUN set -ex; \ + curl -o rosariosis.tar.bz2 -SL https://gitlab.com/francoisjacquet/rosariosis/-/archive/v10.2/rosariosis-v10.2.tar.bz2; \ + echo "$ROSARIOSIS_SHA512 *rosariosis.tar.bz2" | sha512sum -c -; \ + mkdir -p /usr/src/rosariosis; \ + tar -xf rosariosis.tar.bz2 --strip-components=1 -C /usr/src/rosariosis; \ + rm rosariosis.tar.bz2; \ + chown -R www-data:www-data /usr/src/rosariosis + +# Copy init scripts +COPY conf/docker-entrypoint.sh /entrypoint.sh +# Copy our configuration files. +COPY conf/config.inc.php /usr/src/rosariosis/config.inc.php +COPY conf/htaccess.txt /usr/src/rosariosis/.htaccess + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["apache2-foreground"] + diff --git a/10.2/php7.4/apache/conf/config.inc.php b/10.2/php7.4/apache/conf/config.inc.php new file mode 100644 index 0000000..d110c63 --- /dev/null +++ b/10.2/php7.4/apache/conf/config.inc.php @@ -0,0 +1,90 @@ +/dev/null; then + # get the user name + : "${USER_NAME:=www-data}" + # change the user name + [[ "$USER_NAME" != "www-data" ]] && + usermod -l "$USER_NAME" www-data && + groupmod -n "$USER_NAME" www-data + # update the user ID + groupmod -o -g "$user" "$USER_NAME" + # update the user-group ID + usermod -o -u "$group" "$USER_NAME" + fi + ;; + *) # php-fpm + user='www-data' + group='www-data' + ;; + esac + else + user="$uid" + group="$gid" + fi + + if [ -n "$POSTGRES_PORT" ]; then + if [ -z "$POSTGRES_HOST" ]; then + POSTGRES_HOST='postgres' + else + echo >&2 "warning: both POSTGRES_HOST and POSTGRES_PORT found" + echo >&2 " Connecting to POSTGRES_HOST ($POSTGRES_HOST)" + echo >&2 " instead of the linked postgres container" + fi + fi + + if [ -z "$POSTGRES_HOST" ]; then + echo >&2 "error: missing POSTGRES_HOST and POSTGRES_PORT environment variables" + echo >&2 " Did you forget to --link some_postgres_container:postgres or set an external db" + echo >&2 " with -e POSTGRES_HOST=hostname:port?" + exit 1 + fi + + # If the DB user is 'root' then use the MySQL root password env var + : "${POSTGRES_USER:=root}" + if [ "$POSTGRES_USER" = 'root' ]; then + : ${POSTGRES_PASSWORD:=$POSTGRES_ROOT_PASSWORD} + fi + : "${POSTGRES_DB:=rosariosis}" + + if [ -z "$POSTGRES_PASSWORD" ] && [ "$POSTGRES_PASSWORD_ALLOW_EMPTY" != 'yes' ]; then + echo >&2 "error: missing required POSTGRES_PASSWORD environment variable" + echo >&2 " Did you forget to -e POSTGRES_PASSWORD=... ?" + echo >&2 + echo >&2 " (Also of interest might be POSTGRES_USER and POSTGRES_DB.)" + exit 1 + fi + + if [ ! -e index.php ]; then + # if the directory exists and Rosariosis doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory) + if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then + chown "$user:$group" . + fi + + echo >&2 "Rosariosis not found in $PWD - copying now..." + if [ "$(ls -A)" ]; then + echo >&2 "WARNING: $PWD is not empty - press Ctrl+C now if this is an error!" + ( + set -x + ls -A + sleep 10 + ) + fi + # use full commands + # for clearer intent + sourceTarArgs=( + --create + --file - + --directory /usr/src/rosariosis + --one-file-system + --owner "$user" --group "$group" + ) + targetTarArgs=( + --extract + --file - + ) + if [ "$uid" != '0' ]; then + # avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted" + targetTarArgs+=(--no-overwrite-dir) + fi + + tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}" + + if [ ! -e .htaccess ] && [ -f htaccess.txt ]; then + # NOTE: The "Indexes" option is disabled in the php:apache base image so remove it as we enable .htaccess + sed -r 's/^(Options -Indexes.*)$/#\1/' htaccess.txt >.htaccess + chown "$user":"$group" .htaccess + fi + + echo >&2 "Complete! Rosariosis has been successfully copied to $PWD" + fi + + echo >&2 "========================================================================" + echo >&2 + echo >&2 "This server is now configured to run Rosariosis!" + echo >&2 + echo >&2 "NOTE: You will need your database server address, database name," + echo >&2 "and database user credentials to install Rosariosis." + echo >&2 + echo >&2 "========================================================================" +fi + +exec "$@" diff --git a/conf/.htaccess b/10.2/php7.4/apache/conf/htaccess.txt similarity index 100% rename from conf/.htaccess rename to 10.2/php7.4/apache/conf/htaccess.txt diff --git a/10.2/php7.4/fpm/Dockerfile b/10.2/php7.4/fpm/Dockerfile new file mode 100644 index 0000000..c0a1a9d --- /dev/null +++ b/10.2/php7.4/fpm/Dockerfile @@ -0,0 +1,161 @@ +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + + +FROM php:7.4-fpm +LABEL maintainer="François Jacquet (@francoisjacquet), Llewellyn van der Merwe (@Llewellynvdm)" + +# Disable remote database security requirements. +ENV ROSARIOSIS_INSTALLATION_DISABLE_LOCALHOST_CHECK=1 +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ +# To be able to send mail + sendmail \ + nano \ + locales \ + ; \ + rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need. +RUN set -ex; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libbz2-dev \ + libxml2-dev \ + libgmp-dev \ + libicu-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libldap2-dev \ + libmemcached-dev \ + libmagickwand-dev \ + wkhtmltopdf \ + libpq-dev \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + libonig-dev \ + postgresql-client \ + ; \ + \ + docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + ; \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \ + docker-php-ext-install -j "$(nproc)" \ + bz2 \ + bcmath \ + exif \ + gd \ + mbstring \ + xml \ + xmlrpc \ + gmp \ + intl \ + ldap \ + mysqli \ + pdo_mysql \ + pdo_pgsql \ + pgsql \ + gettext \ + zip \ + ; \ + rm -r /tmp/pear; \ + \ +# some misbehaving extensions end up outputting to stdout + out="$(php -r 'exit(0);')"; \ + [ -z "$out" ]; \ + err="$(php -r 'exit(0);' 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ]; \ + \ + extDir="$(php -r 'echo ini_get("extension_dir");')"; \ + [ -d "$extDir" ]; \ +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-5.1.21; \ + pecl install memcached-3.2.0; \ + \ + docker-php-ext-enable \ + apcu \ + memcached \ + ; \ + rm -r /tmp/pear; \ + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$extDir"/*.so \ + | awk '/=>/ { print $3 }' \ + | sort -u \ + | xargs -r dpkg-query -S \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ + ! { ldd "$extDir"/*.so | grep 'not found'; }; \ +# check for output like "PHP Warning: PHP Startup: Unable to load dynamic library 'foo' (tried: ...) + err="$(php --version 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ] + +# set recommended PHP.ini settings +# see https://secure.php.net/manual/en/opcache.installation.php +RUN set -eux; \ + docker-php-ext-enable opcache; \ + { \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=4000'; \ + echo 'opcache.revalidate_freq=2'; \ + echo 'opcache.fast_shutdown=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini +# set recommended error logging +RUN { \ +# https://www.php.net/manual/en/errorfunc.constants.php + echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \ + echo 'display_errors = Off'; \ + echo 'display_startup_errors = Off'; \ + echo 'log_errors = On'; \ + echo 'error_log = /dev/stderr'; \ + echo 'log_errors_max_len = 1024'; \ + echo 'ignore_repeated_errors = On'; \ + echo 'ignore_repeated_source = Off'; \ + echo 'html_errors = Off'; \ + } > /usr/local/etc/php/conf.d/error-logging.ini + +VOLUME /var/www/html + +# Define Rosariosis version and expected SHA512 signature +ENV ROSARIOSIS_VERSION 10.2 +ENV ROSARIOSIS_SHA512 ffc49aac888c39670d8b30a362c5e8becf71abccd26cd72a7e0392adfd67b6887c2ee365490c4cfd0fad8b87f00ff27573e18d99f530e2d131515894ef044c96 + +# Download package and extract to web volume +RUN set -ex; \ + curl -o rosariosis.tar.bz2 -SL https://gitlab.com/francoisjacquet/rosariosis/-/archive/v10.2/rosariosis-v10.2.tar.bz2; \ + echo "$ROSARIOSIS_SHA512 *rosariosis.tar.bz2" | sha512sum -c -; \ + mkdir -p /usr/src/rosariosis; \ + tar -xf rosariosis.tar.bz2 --strip-components=1 -C /usr/src/rosariosis; \ + rm rosariosis.tar.bz2; \ + chown -R www-data:www-data /usr/src/rosariosis + +# Copy init scripts +COPY conf/docker-entrypoint.sh /entrypoint.sh +# Copy our configuration files. +COPY conf/config.inc.php /usr/src/rosariosis/config.inc.php +COPY conf/htaccess.txt /usr/src/rosariosis/.htaccess + +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["php-fpm"] + diff --git a/10.2/php7.4/fpm/conf/config.inc.php b/10.2/php7.4/fpm/conf/config.inc.php new file mode 100644 index 0000000..d110c63 --- /dev/null +++ b/10.2/php7.4/fpm/conf/config.inc.php @@ -0,0 +1,90 @@ +/dev/null; then + # get the user name + : "${USER_NAME:=www-data}" + # change the user name + [[ "$USER_NAME" != "www-data" ]] && + usermod -l "$USER_NAME" www-data && + groupmod -n "$USER_NAME" www-data + # update the user ID + groupmod -o -g "$user" "$USER_NAME" + # update the user-group ID + usermod -o -u "$group" "$USER_NAME" + fi + ;; + *) # php-fpm + user='www-data' + group='www-data' + ;; + esac + else + user="$uid" + group="$gid" + fi + + if [ -n "$POSTGRES_PORT" ]; then + if [ -z "$POSTGRES_HOST" ]; then + POSTGRES_HOST='postgres' + else + echo >&2 "warning: both POSTGRES_HOST and POSTGRES_PORT found" + echo >&2 " Connecting to POSTGRES_HOST ($POSTGRES_HOST)" + echo >&2 " instead of the linked postgres container" + fi + fi + + if [ -z "$POSTGRES_HOST" ]; then + echo >&2 "error: missing POSTGRES_HOST and POSTGRES_PORT environment variables" + echo >&2 " Did you forget to --link some_postgres_container:postgres or set an external db" + echo >&2 " with -e POSTGRES_HOST=hostname:port?" + exit 1 + fi + + # If the DB user is 'root' then use the MySQL root password env var + : "${POSTGRES_USER:=root}" + if [ "$POSTGRES_USER" = 'root' ]; then + : ${POSTGRES_PASSWORD:=$POSTGRES_ROOT_PASSWORD} + fi + : "${POSTGRES_DB:=rosariosis}" + + if [ -z "$POSTGRES_PASSWORD" ] && [ "$POSTGRES_PASSWORD_ALLOW_EMPTY" != 'yes' ]; then + echo >&2 "error: missing required POSTGRES_PASSWORD environment variable" + echo >&2 " Did you forget to -e POSTGRES_PASSWORD=... ?" + echo >&2 + echo >&2 " (Also of interest might be POSTGRES_USER and POSTGRES_DB.)" + exit 1 + fi + + if [ ! -e index.php ]; then + # if the directory exists and Rosariosis doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory) + if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then + chown "$user:$group" . + fi + + echo >&2 "Rosariosis not found in $PWD - copying now..." + if [ "$(ls -A)" ]; then + echo >&2 "WARNING: $PWD is not empty - press Ctrl+C now if this is an error!" + ( + set -x + ls -A + sleep 10 + ) + fi + # use full commands + # for clearer intent + sourceTarArgs=( + --create + --file - + --directory /usr/src/rosariosis + --one-file-system + --owner "$user" --group "$group" + ) + targetTarArgs=( + --extract + --file - + ) + if [ "$uid" != '0' ]; then + # avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted" + targetTarArgs+=(--no-overwrite-dir) + fi + + tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}" + + if [ ! -e .htaccess ] && [ -f htaccess.txt ]; then + # NOTE: The "Indexes" option is disabled in the php:apache base image so remove it as we enable .htaccess + sed -r 's/^(Options -Indexes.*)$/#\1/' htaccess.txt >.htaccess + chown "$user":"$group" .htaccess + fi + + echo >&2 "Complete! Rosariosis has been successfully copied to $PWD" + fi + + echo >&2 "========================================================================" + echo >&2 + echo >&2 "This server is now configured to run Rosariosis!" + echo >&2 + echo >&2 "NOTE: You will need your database server address, database name," + echo >&2 "and database user credentials to install Rosariosis." + echo >&2 + echo >&2 "========================================================================" +fi + +exec "$@" diff --git a/10.2/php7.4/fpm/conf/htaccess.txt b/10.2/php7.4/fpm/conf/htaccess.txt new file mode 100644 index 0000000..3c6a448 --- /dev/null +++ b/10.2/php7.4/fpm/conf/htaccess.txt @@ -0,0 +1 @@ +php_flag display_errors off diff --git a/10.2/php8.0/apache/Dockerfile b/10.2/php8.0/apache/Dockerfile new file mode 100644 index 0000000..ffd0ae9 --- /dev/null +++ b/10.2/php8.0/apache/Dockerfile @@ -0,0 +1,178 @@ +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + + +FROM php:8.0-apache +LABEL maintainer="François Jacquet (@francoisjacquet), Llewellyn van der Merwe (@Llewellynvdm)" + +# Disable remote database security requirements. +ENV ROSARIOSIS_INSTALLATION_DISABLE_LOCALHOST_CHECK=1 +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ +# To be able to send mail + sendmail \ + nano \ + locales \ + ; \ + rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need. +RUN set -ex; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libbz2-dev \ + libxml2-dev \ + libgmp-dev \ + libicu-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libldap2-dev \ + libmemcached-dev \ + libmagickwand-dev \ + wkhtmltopdf \ + libpq-dev \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + libonig-dev \ + postgresql-client \ + ; \ + \ + docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + ; \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \ + docker-php-ext-install -j "$(nproc)" \ + bz2 \ + bcmath \ + exif \ + gd \ + mbstring \ + xml \ + xmlrpc \ + gmp \ + intl \ + ldap \ + mysqli \ + pdo_mysql \ + pdo_pgsql \ + pgsql \ + gettext \ + zip \ + ; \ + rm -r /tmp/pear; \ + \ +# some misbehaving extensions end up outputting to stdout + out="$(php -r 'exit(0);')"; \ + [ -z "$out" ]; \ + err="$(php -r 'exit(0);' 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ]; \ + \ + extDir="$(php -r 'echo ini_get("extension_dir");')"; \ + [ -d "$extDir" ]; \ +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-5.1.21; \ + pecl install memcached-3.2.0; \ + \ + docker-php-ext-enable \ + apcu \ + memcached \ + ; \ + rm -r /tmp/pear; \ + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$extDir"/*.so \ + | awk '/=>/ { print $3 }' \ + | sort -u \ + | xargs -r dpkg-query -S \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ + ! { ldd "$extDir"/*.so | grep 'not found'; }; \ +# check for output like "PHP Warning: PHP Startup: Unable to load dynamic library 'foo' (tried: ...) + err="$(php --version 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ] + +# set recommended PHP.ini settings +# see https://secure.php.net/manual/en/opcache.installation.php +RUN set -eux; \ + docker-php-ext-enable opcache; \ + { \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=4000'; \ + echo 'opcache.revalidate_freq=2'; \ + echo 'opcache.fast_shutdown=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini +# set recommended error logging +RUN { \ +# https://www.php.net/manual/en/errorfunc.constants.php + echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \ + echo 'display_errors = Off'; \ + echo 'display_startup_errors = Off'; \ + echo 'log_errors = On'; \ + echo 'error_log = /dev/stderr'; \ + echo 'log_errors_max_len = 1024'; \ + echo 'ignore_repeated_errors = On'; \ + echo 'ignore_repeated_source = Off'; \ + echo 'html_errors = Off'; \ + } > /usr/local/etc/php/conf.d/error-logging.ini + +RUN set -eux; \ + a2enmod rewrite expires; \ + \ +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html + a2enmod remoteip; \ + { \ + echo 'RemoteIPHeader X-Forwarded-For'; \ +# these IP ranges are reserved for "private" use and should thus *usually* be safe inside Docker + echo 'RemoteIPTrustedProxy 10.0.0.0/8'; \ + echo 'RemoteIPTrustedProxy 172.16.0.0/12'; \ + echo 'RemoteIPTrustedProxy 192.168.0.0/16'; \ + echo 'RemoteIPTrustedProxy 169.254.0.0/16'; \ + echo 'RemoteIPTrustedProxy 127.0.0.0/8'; \ + } > /etc/apache2/conf-available/remoteip.conf; \ + a2enconf remoteip; \ +# (replace all instances of "%h" with "%a" in LogFormat) + find /etc/apache2 -type f -name '*.conf' -exec sed -ri 's/([[:space:]]*LogFormat[[:space:]]+"[^"]*)%h([^"]*")/\1%a\2/g' '{}' + + +VOLUME /var/www/html + +# Define Rosariosis version and expected SHA512 signature +ENV ROSARIOSIS_VERSION 10.2 +ENV ROSARIOSIS_SHA512 ffc49aac888c39670d8b30a362c5e8becf71abccd26cd72a7e0392adfd67b6887c2ee365490c4cfd0fad8b87f00ff27573e18d99f530e2d131515894ef044c96 + +# Download package and extract to web volume +RUN set -ex; \ + curl -o rosariosis.tar.bz2 -SL https://gitlab.com/francoisjacquet/rosariosis/-/archive/v10.2/rosariosis-v10.2.tar.bz2; \ + echo "$ROSARIOSIS_SHA512 *rosariosis.tar.bz2" | sha512sum -c -; \ + mkdir -p /usr/src/rosariosis; \ + tar -xf rosariosis.tar.bz2 --strip-components=1 -C /usr/src/rosariosis; \ + rm rosariosis.tar.bz2; \ + chown -R www-data:www-data /usr/src/rosariosis + +# Copy init scripts +COPY conf/docker-entrypoint.sh /entrypoint.sh +# Copy our configuration files. +COPY conf/config.inc.php /usr/src/rosariosis/config.inc.php +COPY conf/htaccess.txt /usr/src/rosariosis/.htaccess + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["apache2-foreground"] + diff --git a/10.2/php8.0/apache/conf/config.inc.php b/10.2/php8.0/apache/conf/config.inc.php new file mode 100644 index 0000000..d110c63 --- /dev/null +++ b/10.2/php8.0/apache/conf/config.inc.php @@ -0,0 +1,90 @@ +/dev/null; then + # get the user name + : "${USER_NAME:=www-data}" + # change the user name + [[ "$USER_NAME" != "www-data" ]] && + usermod -l "$USER_NAME" www-data && + groupmod -n "$USER_NAME" www-data + # update the user ID + groupmod -o -g "$user" "$USER_NAME" + # update the user-group ID + usermod -o -u "$group" "$USER_NAME" + fi + ;; + *) # php-fpm + user='www-data' + group='www-data' + ;; + esac + else + user="$uid" + group="$gid" + fi + + if [ -n "$POSTGRES_PORT" ]; then + if [ -z "$POSTGRES_HOST" ]; then + POSTGRES_HOST='postgres' + else + echo >&2 "warning: both POSTGRES_HOST and POSTGRES_PORT found" + echo >&2 " Connecting to POSTGRES_HOST ($POSTGRES_HOST)" + echo >&2 " instead of the linked postgres container" + fi + fi + + if [ -z "$POSTGRES_HOST" ]; then + echo >&2 "error: missing POSTGRES_HOST and POSTGRES_PORT environment variables" + echo >&2 " Did you forget to --link some_postgres_container:postgres or set an external db" + echo >&2 " with -e POSTGRES_HOST=hostname:port?" + exit 1 + fi + + # If the DB user is 'root' then use the MySQL root password env var + : "${POSTGRES_USER:=root}" + if [ "$POSTGRES_USER" = 'root' ]; then + : ${POSTGRES_PASSWORD:=$POSTGRES_ROOT_PASSWORD} + fi + : "${POSTGRES_DB:=rosariosis}" + + if [ -z "$POSTGRES_PASSWORD" ] && [ "$POSTGRES_PASSWORD_ALLOW_EMPTY" != 'yes' ]; then + echo >&2 "error: missing required POSTGRES_PASSWORD environment variable" + echo >&2 " Did you forget to -e POSTGRES_PASSWORD=... ?" + echo >&2 + echo >&2 " (Also of interest might be POSTGRES_USER and POSTGRES_DB.)" + exit 1 + fi + + if [ ! -e index.php ]; then + # if the directory exists and Rosariosis doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory) + if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then + chown "$user:$group" . + fi + + echo >&2 "Rosariosis not found in $PWD - copying now..." + if [ "$(ls -A)" ]; then + echo >&2 "WARNING: $PWD is not empty - press Ctrl+C now if this is an error!" + ( + set -x + ls -A + sleep 10 + ) + fi + # use full commands + # for clearer intent + sourceTarArgs=( + --create + --file - + --directory /usr/src/rosariosis + --one-file-system + --owner "$user" --group "$group" + ) + targetTarArgs=( + --extract + --file - + ) + if [ "$uid" != '0' ]; then + # avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted" + targetTarArgs+=(--no-overwrite-dir) + fi + + tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}" + + if [ ! -e .htaccess ] && [ -f htaccess.txt ]; then + # NOTE: The "Indexes" option is disabled in the php:apache base image so remove it as we enable .htaccess + sed -r 's/^(Options -Indexes.*)$/#\1/' htaccess.txt >.htaccess + chown "$user":"$group" .htaccess + fi + + echo >&2 "Complete! Rosariosis has been successfully copied to $PWD" + fi + + echo >&2 "========================================================================" + echo >&2 + echo >&2 "This server is now configured to run Rosariosis!" + echo >&2 + echo >&2 "NOTE: You will need your database server address, database name," + echo >&2 "and database user credentials to install Rosariosis." + echo >&2 + echo >&2 "========================================================================" +fi + +exec "$@" diff --git a/10.2/php8.0/apache/conf/htaccess.txt b/10.2/php8.0/apache/conf/htaccess.txt new file mode 100644 index 0000000..3c6a448 --- /dev/null +++ b/10.2/php8.0/apache/conf/htaccess.txt @@ -0,0 +1 @@ +php_flag display_errors off diff --git a/10.2/php8.0/fpm/Dockerfile b/10.2/php8.0/fpm/Dockerfile new file mode 100644 index 0000000..0328156 --- /dev/null +++ b/10.2/php8.0/fpm/Dockerfile @@ -0,0 +1,161 @@ +# +# NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# + + +FROM php:8.0-fpm +LABEL maintainer="François Jacquet (@francoisjacquet), Llewellyn van der Merwe (@Llewellynvdm)" + +# Disable remote database security requirements. +ENV ROSARIOSIS_INSTALLATION_DISABLE_LOCALHOST_CHECK=1 +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ +# To be able to send mail + sendmail \ + nano \ + locales \ + ; \ + rm -rf /var/lib/apt/lists/* + +# install the PHP extensions we need. +RUN set -ex; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libbz2-dev \ + libxml2-dev \ + libgmp-dev \ + libicu-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libldap2-dev \ + libmemcached-dev \ + libmagickwand-dev \ + wkhtmltopdf \ + libpq-dev \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + libonig-dev \ + postgresql-client \ + ; \ + \ + docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + ; \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \ + docker-php-ext-install -j "$(nproc)" \ + bz2 \ + bcmath \ + exif \ + gd \ + mbstring \ + xml \ + xmlrpc \ + gmp \ + intl \ + ldap \ + mysqli \ + pdo_mysql \ + pdo_pgsql \ + pgsql \ + gettext \ + zip \ + ; \ + rm -r /tmp/pear; \ + \ +# some misbehaving extensions end up outputting to stdout + out="$(php -r 'exit(0);')"; \ + [ -z "$out" ]; \ + err="$(php -r 'exit(0);' 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ]; \ + \ + extDir="$(php -r 'echo ini_get("extension_dir");')"; \ + [ -d "$extDir" ]; \ +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-5.1.21; \ + pecl install memcached-3.2.0; \ + \ + docker-php-ext-enable \ + apcu \ + memcached \ + ; \ + rm -r /tmp/pear; \ + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$extDir"/*.so \ + | awk '/=>/ { print $3 }' \ + | sort -u \ + | xargs -r dpkg-query -S \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ + ! { ldd "$extDir"/*.so | grep 'not found'; }; \ +# check for output like "PHP Warning: PHP Startup: Unable to load dynamic library 'foo' (tried: ...) + err="$(php --version 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ] + +# set recommended PHP.ini settings +# see https://secure.php.net/manual/en/opcache.installation.php +RUN set -eux; \ + docker-php-ext-enable opcache; \ + { \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=4000'; \ + echo 'opcache.revalidate_freq=2'; \ + echo 'opcache.fast_shutdown=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini +# set recommended error logging +RUN { \ +# https://www.php.net/manual/en/errorfunc.constants.php + echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \ + echo 'display_errors = Off'; \ + echo 'display_startup_errors = Off'; \ + echo 'log_errors = On'; \ + echo 'error_log = /dev/stderr'; \ + echo 'log_errors_max_len = 1024'; \ + echo 'ignore_repeated_errors = On'; \ + echo 'ignore_repeated_source = Off'; \ + echo 'html_errors = Off'; \ + } > /usr/local/etc/php/conf.d/error-logging.ini + +VOLUME /var/www/html + +# Define Rosariosis version and expected SHA512 signature +ENV ROSARIOSIS_VERSION 10.2 +ENV ROSARIOSIS_SHA512 ffc49aac888c39670d8b30a362c5e8becf71abccd26cd72a7e0392adfd67b6887c2ee365490c4cfd0fad8b87f00ff27573e18d99f530e2d131515894ef044c96 + +# Download package and extract to web volume +RUN set -ex; \ + curl -o rosariosis.tar.bz2 -SL https://gitlab.com/francoisjacquet/rosariosis/-/archive/v10.2/rosariosis-v10.2.tar.bz2; \ + echo "$ROSARIOSIS_SHA512 *rosariosis.tar.bz2" | sha512sum -c -; \ + mkdir -p /usr/src/rosariosis; \ + tar -xf rosariosis.tar.bz2 --strip-components=1 -C /usr/src/rosariosis; \ + rm rosariosis.tar.bz2; \ + chown -R www-data:www-data /usr/src/rosariosis + +# Copy init scripts +COPY conf/docker-entrypoint.sh /entrypoint.sh +# Copy our configuration files. +COPY conf/config.inc.php /usr/src/rosariosis/config.inc.php +COPY conf/htaccess.txt /usr/src/rosariosis/.htaccess + +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["php-fpm"] + diff --git a/10.2/php8.0/fpm/conf/config.inc.php b/10.2/php8.0/fpm/conf/config.inc.php new file mode 100644 index 0000000..d110c63 --- /dev/null +++ b/10.2/php8.0/fpm/conf/config.inc.php @@ -0,0 +1,90 @@ +/dev/null; then + # get the user name + : "${USER_NAME:=www-data}" + # change the user name + [[ "$USER_NAME" != "www-data" ]] && + usermod -l "$USER_NAME" www-data && + groupmod -n "$USER_NAME" www-data + # update the user ID + groupmod -o -g "$user" "$USER_NAME" + # update the user-group ID + usermod -o -u "$group" "$USER_NAME" + fi + ;; + *) # php-fpm + user='www-data' + group='www-data' + ;; + esac + else + user="$uid" + group="$gid" + fi + + if [ -n "$POSTGRES_PORT" ]; then + if [ -z "$POSTGRES_HOST" ]; then + POSTGRES_HOST='postgres' + else + echo >&2 "warning: both POSTGRES_HOST and POSTGRES_PORT found" + echo >&2 " Connecting to POSTGRES_HOST ($POSTGRES_HOST)" + echo >&2 " instead of the linked postgres container" + fi + fi + + if [ -z "$POSTGRES_HOST" ]; then + echo >&2 "error: missing POSTGRES_HOST and POSTGRES_PORT environment variables" + echo >&2 " Did you forget to --link some_postgres_container:postgres or set an external db" + echo >&2 " with -e POSTGRES_HOST=hostname:port?" + exit 1 + fi + + # If the DB user is 'root' then use the MySQL root password env var + : "${POSTGRES_USER:=root}" + if [ "$POSTGRES_USER" = 'root' ]; then + : ${POSTGRES_PASSWORD:=$POSTGRES_ROOT_PASSWORD} + fi + : "${POSTGRES_DB:=rosariosis}" + + if [ -z "$POSTGRES_PASSWORD" ] && [ "$POSTGRES_PASSWORD_ALLOW_EMPTY" != 'yes' ]; then + echo >&2 "error: missing required POSTGRES_PASSWORD environment variable" + echo >&2 " Did you forget to -e POSTGRES_PASSWORD=... ?" + echo >&2 + echo >&2 " (Also of interest might be POSTGRES_USER and POSTGRES_DB.)" + exit 1 + fi + + if [ ! -e index.php ]; then + # if the directory exists and Rosariosis doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory) + if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then + chown "$user:$group" . + fi + + echo >&2 "Rosariosis not found in $PWD - copying now..." + if [ "$(ls -A)" ]; then + echo >&2 "WARNING: $PWD is not empty - press Ctrl+C now if this is an error!" + ( + set -x + ls -A + sleep 10 + ) + fi + # use full commands + # for clearer intent + sourceTarArgs=( + --create + --file - + --directory /usr/src/rosariosis + --one-file-system + --owner "$user" --group "$group" + ) + targetTarArgs=( + --extract + --file - + ) + if [ "$uid" != '0' ]; then + # avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted" + targetTarArgs+=(--no-overwrite-dir) + fi + + tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}" + + if [ ! -e .htaccess ] && [ -f htaccess.txt ]; then + # NOTE: The "Indexes" option is disabled in the php:apache base image so remove it as we enable .htaccess + sed -r 's/^(Options -Indexes.*)$/#\1/' htaccess.txt >.htaccess + chown "$user":"$group" .htaccess + fi + + echo >&2 "Complete! Rosariosis has been successfully copied to $PWD" + fi + + echo >&2 "========================================================================" + echo >&2 + echo >&2 "This server is now configured to run Rosariosis!" + echo >&2 + echo >&2 "NOTE: You will need your database server address, database name," + echo >&2 "and database user credentials to install Rosariosis." + echo >&2 + echo >&2 "========================================================================" +fi + +exec "$@" diff --git a/10.2/php8.0/fpm/conf/htaccess.txt b/10.2/php8.0/fpm/conf/htaccess.txt new file mode 100644 index 0000000..3c6a448 --- /dev/null +++ b/10.2/php8.0/fpm/conf/htaccess.txt @@ -0,0 +1 @@ +php_flag display_errors off diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 02dc06d..0000000 --- a/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -# Dockerfile for RosarioSIS -# https://www.rosariosis.org/ -# Best Dockerfile practices: http://crosbymichael.com/dockerfile-best-practices.html - -# https://hub.docker.com/_/php?tab=tags&page=1&name=apache -# TODO When moving to PHP8.0, remove xmlrpc extension! -FROM php:7.4-apache - -LABEL maintainer="François Jacquet " - -ENV PGHOST=db \ - PGUSER=rosario \ - PGPASSWORD=rosariopwd \ - PGDATABASE=rosariosis \ - PGPORT=5432 \ - ROSARIOSIS_YEAR=2022 \ - ROSARIOSIS_LANG='en_US' - -# Upgrade packages. -# Install git, Apache2 + PHP + PostgreSQL webserver, sendmail, wkhtmltopdf & others utilities. -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install postgresql-client wkhtmltopdf libpq-dev libpng-dev libxml2-dev libzip-dev libonig-dev sendmail nano locales -y; - -# Install PHP extensions. -RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr; \ - docker-php-ext-install -j$(nproc) gd mbstring xml pgsql gettext intl xmlrpc zip - -# Download and extract rosariosis -ENV ROSARIOSIS_VERSION 'v10.2' -RUN mkdir /usr/src/rosariosis && \ - curl -L https://gitlab.com/francoisjacquet/rosariosis/-/archive/${ROSARIOSIS_VERSION}/rosariosis-${ROSARIOSIS_VERSION}.tar.gz \ - | tar xz --strip-components=1 -C /usr/src/rosariosis && \ - rm -rf /var/www/html && mkdir -p /var/www && \ - ln -s /usr/src/rosariosis/ /var/www/html && chmod 777 /var/www/html &&\ - chown -R www-data:www-data /usr/src/rosariosis - -# Copy our configuration files. -COPY conf/config.inc.php /usr/src/rosariosis/config.inc.php -COPY conf/.htaccess /usr/src/rosariosis/.htaccess -COPY bin/init /init - -EXPOSE 80 - -ENTRYPOINT ["/init"] -CMD ["apache2-foreground"] diff --git a/Dockerfile.template b/Dockerfile.template new file mode 100644 index 0000000..082f4ac --- /dev/null +++ b/Dockerfile.template @@ -0,0 +1,243 @@ +{{ + def is_alpine: + env.variant | index("alpine") +-}} + +FROM php:{{ env.phpVersion }}-{{ env.variant }} +LABEL maintainer="{{ env.rosariosisMaintainers }}" + +# Disable remote database security requirements. +ENV ROSARIOSIS_INSTALLATION_DISABLE_LOCALHOST_CHECK=1 +{{ if is_alpine then ( -}} +RUN set -eux; \ + apk add --no-cache \ +# in theory, docker-entrypoint.sh is POSIX-compliant, but priority is a working, consistent image + bash \ +# To be able to send mail + sendmail \ + nano \ + locales \ + ; +{{ ) else ( -}} +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ +# To be able to send mail + sendmail \ + nano \ + locales \ + ; \ + rm -rf /var/lib/apt/lists/* +{{ ) end -}} + +# install the PHP extensions we need. +RUN set -ex; \ + \ +{{ if is_alpine then ( -}} + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + autoconf \ + bzip2-dev \ + libxml2-dev \ + gmp-dev \ + icu-dev \ + freetype-dev \ + libjpeg-turbo-dev \ + libmemcached-dev \ + wkhtmltopdf \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + libonig-dev \ + openldap-dev \ + pcre-dev \ + postgresql-dev \ + postgresql-client \ + ; \ +{{ ) else ( -}} + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libbz2-dev \ + libxml2-dev \ + libgmp-dev \ + libicu-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libldap2-dev \ + libmemcached-dev \ + libmagickwand-dev \ + wkhtmltopdf \ + libpq-dev \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + libonig-dev \ + postgresql-client \ + ; \ +{{ ) end -}} + \ + docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + ; \ +{{ if is_alpine then ( -}} + docker-php-ext-configure ldap; \ +{{ ) else ( -}} + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + docker-php-ext-configure ldap --with-libdir="lib/$debMultiarch"; \ +{{ ) end -}} + docker-php-ext-install -j "$(nproc)" \ + bz2 \ + bcmath \ + exif \ + gd \ + mbstring \ + xml \ + xmlrpc \ + gmp \ + intl \ + ldap \ + mysqli \ + pdo_mysql \ + pdo_pgsql \ + pgsql \ + gettext \ + zip \ + ; \ + rm -r /tmp/pear; \ + \ +# some misbehaving extensions end up outputting to stdout + out="$(php -r 'exit(0);')"; \ + [ -z "$out" ]; \ + err="$(php -r 'exit(0);' 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ]; \ + \ + extDir="$(php -r 'echo ini_get("extension_dir");')"; \ + [ -d "$extDir" ]; \ +{{ if is_alpine then ( -}} + \ +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-{{ env.pecl_APCu }}; \ + pecl install memcached-{{ env.pecl_memcached }}; \ + \ + docker-php-ext-enable \ + apcu \ + memcached \ + ; \ + rm -r /tmp/pear; \ + \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive "$extDir" \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-network --virtual .rosariosis-phpexts-rundeps $runDeps; \ + apk del --no-network .build-deps; \ +{{ ) else ( -}} +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu-{{ env.pecl_APCu }}; \ + pecl install memcached-{{ env.pecl_memcached }}; \ + \ + docker-php-ext-enable \ + apcu \ + memcached \ + ; \ + rm -r /tmp/pear; \ + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$extDir"/*.so \ + | awk '/=>/ { print $3 }' \ + | sort -u \ + | xargs -r dpkg-query -S \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ +{{ ) end -}} + \ + ! { ldd "$extDir"/*.so | grep 'not found'; }; \ +# check for output like "PHP Warning: PHP Startup: Unable to load dynamic library 'foo' (tried: ...) + err="$(php --version 3>&1 1>&2 2>&3)"; \ + [ -z "$err" ] + +# set recommended PHP.ini settings +# see https://secure.php.net/manual/en/opcache.installation.php +RUN set -eux; \ + docker-php-ext-enable opcache; \ + { \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=4000'; \ + echo 'opcache.revalidate_freq=2'; \ + echo 'opcache.fast_shutdown=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini +# set recommended error logging +RUN { \ +# https://www.php.net/manual/en/errorfunc.constants.php + echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \ + echo 'display_errors = Off'; \ + echo 'display_startup_errors = Off'; \ + echo 'log_errors = On'; \ + echo 'error_log = /dev/stderr'; \ + echo 'log_errors_max_len = 1024'; \ + echo 'ignore_repeated_errors = On'; \ + echo 'ignore_repeated_source = Off'; \ + echo 'html_errors = Off'; \ + } > /usr/local/etc/php/conf.d/error-logging.ini +{{ if env.variant == "apache" then ( -}} + +RUN set -eux; \ + a2enmod rewrite expires; \ + \ +# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html + a2enmod remoteip; \ + { \ + echo 'RemoteIPHeader X-Forwarded-For'; \ +# these IP ranges are reserved for "private" use and should thus *usually* be safe inside Docker + echo 'RemoteIPTrustedProxy 10.0.0.0/8'; \ + echo 'RemoteIPTrustedProxy 172.16.0.0/12'; \ + echo 'RemoteIPTrustedProxy 192.168.0.0/16'; \ + echo 'RemoteIPTrustedProxy 169.254.0.0/16'; \ + echo 'RemoteIPTrustedProxy 127.0.0.0/8'; \ + } > /etc/apache2/conf-available/remoteip.conf; \ + a2enconf remoteip; \ +# (replace all instances of "%h" with "%a" in LogFormat) + find /etc/apache2 -type f -name '*.conf' -exec sed -ri 's/([[:space:]]*LogFormat[[:space:]]+"[^"]*)%h([^"]*")/\1%a\2/g' '{}' + +{{ ) else "" end -}} + +VOLUME /var/www/html + +# Define Rosariosis version and expected SHA512 signature +ENV ROSARIOSIS_VERSION {{ env.rosariosisVersion }} +ENV ROSARIOSIS_SHA512 {{ env.rosariosisSha512 }} + +# Download package and extract to web volume +RUN set -ex; \ + curl -o rosariosis.tar.bz2 -SL {{ env.rosariosisPackage }}; \ + echo "$ROSARIOSIS_SHA512 *rosariosis.tar.bz2" | sha512sum -c -; \ + mkdir -p /usr/src/rosariosis; \ + tar -xf rosariosis.tar.bz2 --strip-components=1 -C /usr/src/rosariosis; \ + rm rosariosis.tar.bz2; \ + chown -R www-data:www-data /usr/src/rosariosis + +# Copy init scripts +COPY conf/docker-entrypoint.sh /entrypoint.sh +# Copy our configuration files. +COPY conf/config.inc.php /usr/src/rosariosis/config.inc.php +COPY conf/htaccess.txt /usr/src/rosariosis/.htaccess + +ENTRYPOINT ["/entrypoint.sh"] +{{ if env.variant == "apache" then ( -}} +CMD ["apache2-foreground"] +{{ ) else ( }} +CMD ["php-fpm"] +{{ ) end -}} + diff --git a/apply-templates.sh b/apply-templates.sh new file mode 100755 index 0000000..80997de --- /dev/null +++ b/apply-templates.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +[ -f versions.json ] # run "versions.sh" first + +jqt='.jq-template.awk' +if [ -n "${BASHBREW_SCRIPTS:-}" ]; then + jqt="$BASHBREW_SCRIPTS/jq-template.awk" +elif [ "$BASH_SOURCE" -nt "$jqt" ]; then + wget -qO "$jqt" 'https://github.com/docker-library/bashbrew/raw/5f0c26381fb7cc78b2d217d58007800bdcfbcfa1/scripts/jq-template.awk' +fi + +# if no versions passed we load from versions.json +if [ "$#" -eq 0 ]; then + versions="$(jq -r 'keys | map(@sh) | join(" ")' versions.json)" + eval "set -- $versions" +fi + +# the warning message to not update the docker files directly +generated_warning() { + cat <<-EOH + # + # NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh" + # + # PLEASE DO NOT EDIT IT DIRECTLY. + # + + EOH +} + +# set the Rosariosis maintainers of these docker images +rosariosisMaintainers="$( jq -cr '. | map(.firstname + " " + .lastname + " <" + .email + "> (@" + .github + ")") | join(", ")' maintainers.json)" +export rosariosisMaintainers + +# loop over the version set above +for version; do + export version + # get this Rosariosis version details + rosariosisVersionDetails="$(jq -r '.[env.version]' versions.json)" + # get this Rosariosis version + rosariosisVersion="$(echo "${rosariosisVersionDetails}" | jq -r '.version')" + export rosariosisVersion + # get the PHP version + phpVersions="$(echo "${rosariosisVersionDetails}" | jq -r '.phpVersions | map(@sh) | join(" ")')" + eval "phpVersions=( $phpVersions )" + # get the variants + variants="$(echo "${rosariosisVersionDetails}" | jq -r '.variants | map(@sh) | join(" ")')" + eval "variants=( $variants )" + # get this version Rosariosis Sha512 + rosariosisSha512="$(echo "${rosariosisVersionDetails}" | jq -r '.sha512')" + export rosariosisSha512 + # get this version Rosariosis Package URL + rosariosisPackage="$(echo "${rosariosisVersionDetails}" | jq -r '.package')" + export rosariosisPackage + + for phpVersion in "${phpVersions[@]}"; do + export phpVersion + + # get the pecl values (we may want to move this to versions.json) + peclValues="$(jq -r '.[env.version].phpVersions[env.phpVersion].pecl' versions-helper.json)" + # get the APCu values + pecl_APCu="$(echo "${peclValues}" | jq -r '.APCu')" + export pecl_APCu + # get the memcached values + pecl_memcached="$(echo "${peclValues}" | jq -r '.memcached')" + export pecl_memcached + # get the redis values + pecl_redis="$(echo "${peclValues}" | jq -r '.redis')" + export pecl_redis + # get the mcrypt values + pecl_mcrypt="$(echo "${peclValues}" | jq -r '.mcrypt')" + export pecl_mcrypt + + for variant in "${variants[@]}"; do + export variant + + # the path to this variant folder + dir="$version/php$phpVersion/$variant" + mkdir -p "$dir" + + echo "processing $dir ..." + + # move the entrypoint file into place + mkdir -p "$dir/conf" + cp -a "docker-entrypoint.sh" "$dir/conf/docker-entrypoint.sh" + cp -a "conf/config.inc.php" "$dir/conf/config.inc.php" + cp -a "conf/htaccess.txt" "$dir/conf/htaccess.txt" + + { + generated_warning + gawk -f "$jqt" Dockerfile.template + } >"$dir/Dockerfile" + done + done +done diff --git a/bin/init b/bin/init deleted file mode 100755 index 89f3a00..0000000 --- a/bin/init +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -echo "Configuring locale" -if [ "$ROSARIOSIS_LANG" == "en_US" ]; then - echo "Found "$ROSARIOSIS_LANG -else - echo "Installing "$ROSARIOSIS_LANG - apt-get install locales - echo $ROSARIOSIS_LANG'.UTF-8 UTF-8' >> /etc/locale.gen - locale-gen -fi - -set -e - -# first arg is `-f` or `--some-option` -if [ "${1#-}" != "$1" ]; then - set -- php "$@" -fi - -exec "$@" diff --git a/bin/start-apache2 b/bin/start-apache2 deleted file mode 100755 index 7d9c707..0000000 --- a/bin/start-apache2 +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -source /etc/apache2/envvars -exec apache2ctl -D FOREGROUND diff --git a/conf/config.inc.php b/conf/config.inc.php index 25140ee..d110c63 100644 --- a/conf/config.inc.php +++ b/conf/config.inc.php @@ -15,19 +15,19 @@ // Database server hostname: use localhost if on same server. -$DatabaseServer = getenv( 'PGHOST' ); +$DatabaseServer = getenv( 'POSTGRES_HOST' ); // Database username. -$DatabaseUsername = getenv( 'PGUSER' ); +$DatabaseUsername = getenv( 'POSTGRES_USER' ); // Database password. -$DatabasePassword = getenv( 'PGPASSWORD' );; +$DatabasePassword = getenv( 'POSTGRES_PASSWORD' );; // Database name. -$DatabaseName = getenv( 'PGDATABASE' );; +$DatabaseName = getenv( 'POSTGRES_DB' );; // Database port: default is 5432. -$DatabasePort = getenv( 'PGPORT' ); +$DatabasePort = getenv( 'POSTGRES_PORT' ); /** diff --git a/conf/htaccess.txt b/conf/htaccess.txt new file mode 100644 index 0000000..3c6a448 --- /dev/null +++ b/conf/htaccess.txt @@ -0,0 +1 @@ +php_flag display_errors off diff --git a/conf/supervisord.conf b/conf/supervisord.conf deleted file mode 100644 index e1f982b..0000000 --- a/conf/supervisord.conf +++ /dev/null @@ -1,15 +0,0 @@ -[supervisord] -nodaemon=true - -[program:apache2] -command=/start-apache2 -numprocs=1 -autostart=true -autorestart=true - -[program:ncat] -command=/usr/bin/ncat -k -l 25 -e '/usr/bin/ncat 172.17.42.1 25' -numprocs=1 -autostart=1 -autorestart=1 - diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 35a6dcb..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -# https://riptutorial.com/docker-compose -version: '3.1' - -services: - - db: - image: postgres - environment: - POSTGRES_USER: rosario - POSTGRES_PASSWORD: rosariopwd - POSTGRES_DB: rosariosis - - web: - build: . - ports: - - "80:80" - depends_on: - - db - environment: - PGHOST: db - PGUSER: rosario - PGPASSWORD: rosariopwd - PGDATABASE: rosariosis - ROSARIOSIS_YEAR: 2022 -# ROSARIOSIS_LANG: 'es_ES' diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..14b5ac3 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,133 @@ +#!/bin/bash + +set -e + +if [ -n "$POSTGRES_PASSWORD_FILE" ] && [ -f "$POSTGRES_PASSWORD_FILE" ]; then + POSTGRES_PASSWORD=$(cat "$POSTGRES_PASSWORD_FILE") +fi + +if [[ "$1" == apache2* ]] || [ "$1" == php-fpm ]; then + uid="$(id -u)" + gid="$(id -g)" + if [ "$uid" = '0' ]; then + case "$1" in + apache2*) + user="${APACHE_RUN_USER:-www-data}" + group="${APACHE_RUN_GROUP:-www-data}" + + # strip off any '#' symbol ('#1000' is valid syntax for Apache) + pound='#' + user="${user#$pound}" + group="${group#$pound}" + + # set user if not exist + if ! id "$user" &>/dev/null; then + # get the user name + : "${USER_NAME:=www-data}" + # change the user name + [[ "$USER_NAME" != "www-data" ]] && + usermod -l "$USER_NAME" www-data && + groupmod -n "$USER_NAME" www-data + # update the user ID + groupmod -o -g "$user" "$USER_NAME" + # update the user-group ID + usermod -o -u "$group" "$USER_NAME" + fi + ;; + *) # php-fpm + user='www-data' + group='www-data' + ;; + esac + else + user="$uid" + group="$gid" + fi + + if [ -n "$POSTGRES_PORT" ]; then + if [ -z "$POSTGRES_HOST" ]; then + POSTGRES_HOST='postgres' + else + echo >&2 "warning: both POSTGRES_HOST and POSTGRES_PORT found" + echo >&2 " Connecting to POSTGRES_HOST ($POSTGRES_HOST)" + echo >&2 " instead of the linked postgres container" + fi + fi + + if [ -z "$POSTGRES_HOST" ]; then + echo >&2 "error: missing POSTGRES_HOST and POSTGRES_PORT environment variables" + echo >&2 " Did you forget to --link some_postgres_container:postgres or set an external db" + echo >&2 " with -e POSTGRES_HOST=hostname:port?" + exit 1 + fi + + # If the DB user is 'root' then use the MySQL root password env var + : "${POSTGRES_USER:=root}" + if [ "$POSTGRES_USER" = 'root' ]; then + : ${POSTGRES_PASSWORD:=$POSTGRES_ROOT_PASSWORD} + fi + : "${POSTGRES_DB:=rosariosis}" + + if [ -z "$POSTGRES_PASSWORD" ] && [ "$POSTGRES_PASSWORD_ALLOW_EMPTY" != 'yes' ]; then + echo >&2 "error: missing required POSTGRES_PASSWORD environment variable" + echo >&2 " Did you forget to -e POSTGRES_PASSWORD=... ?" + echo >&2 + echo >&2 " (Also of interest might be POSTGRES_USER and POSTGRES_DB.)" + exit 1 + fi + + if [ ! -e index.php ]; then + # if the directory exists and Rosariosis doesn't appear to be installed AND the permissions of it are root:root, let's chown it (likely a Docker-created directory) + if [ "$uid" = '0' ] && [ "$(stat -c '%u:%g' .)" = '0:0' ]; then + chown "$user:$group" . + fi + + echo >&2 "Rosariosis not found in $PWD - copying now..." + if [ "$(ls -A)" ]; then + echo >&2 "WARNING: $PWD is not empty - press Ctrl+C now if this is an error!" + ( + set -x + ls -A + sleep 10 + ) + fi + # use full commands + # for clearer intent + sourceTarArgs=( + --create + --file - + --directory /usr/src/rosariosis + --one-file-system + --owner "$user" --group "$group" + ) + targetTarArgs=( + --extract + --file - + ) + if [ "$uid" != '0' ]; then + # avoid "tar: .: Cannot utime: Operation not permitted" and "tar: .: Cannot change mode to rwxr-xr-x: Operation not permitted" + targetTarArgs+=(--no-overwrite-dir) + fi + + tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}" + + if [ ! -e .htaccess ] && [ -f htaccess.txt ]; then + # NOTE: The "Indexes" option is disabled in the php:apache base image so remove it as we enable .htaccess + sed -r 's/^(Options -Indexes.*)$/#\1/' htaccess.txt >.htaccess + chown "$user":"$group" .htaccess + fi + + echo >&2 "Complete! Rosariosis has been successfully copied to $PWD" + fi + + echo >&2 "========================================================================" + echo >&2 + echo >&2 "This server is now configured to run Rosariosis!" + echo >&2 + echo >&2 "NOTE: You will need your database server address, database name," + echo >&2 "and database user credentials to install Rosariosis." + echo >&2 + echo >&2 "========================================================================" +fi + +exec "$@" diff --git a/maintainers.json b/maintainers.json new file mode 100644 index 0000000..f45c961 --- /dev/null +++ b/maintainers.json @@ -0,0 +1,14 @@ +[ + { + "firstname": "François", + "lastname": "Jacquet", + "email": "francoisjacquet@users.noreply.github.com", + "github": "francoisjacquet" + }, + { + "firstname": "Llewellyn", + "lastname": "van der Merwe", + "email": "Llewellynvdm@users.noreply.github.com", + "github": "Llewellynvdm" + } +] diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..0748af1 --- /dev/null +++ b/update.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +./versions.sh "$@" +./apply-templates.sh "$@" \ No newline at end of file diff --git a/versions-helper.json b/versions-helper.json new file mode 100644 index 0000000..2effbbd --- /dev/null +++ b/versions-helper.json @@ -0,0 +1,26 @@ +{ + "10.2": { + "version": "10.2", + "php": "7.4", + "aliases": ["10", "latest"], + "phpVersions": { + "8.0": { + "pecl": { + "APCu": "5.1.21", + "memcached": "3.2.0" + } + }, + "7.4": { + "pecl": { + "APCu": "5.1.21", + "memcached": "3.2.0" + } + } + }, + "variant": "apache", + "variants": [ + "apache", + "fpm" + ] + } +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..e25415f --- /dev/null +++ b/versions.json @@ -0,0 +1,21 @@ +{ + "10.2": { + "aliases": [ + "10", + "latest" + ], + "package": "https://gitlab.com/francoisjacquet/rosariosis/-/archive/v10.2/rosariosis-v10.2.tar.bz2", + "php": "7.4", + "phpVersions": [ + "7.4", + "8.0" + ], + "sha512": "ffc49aac888c39670d8b30a362c5e8becf71abccd26cd72a7e0392adfd67b6887c2ee365490c4cfd0fad8b87f00ff27573e18d99f530e2d131515894ef044c96", + "variant": "apache", + "variants": [ + "apache", + "fpm" + ], + "version": "10.2" + } +} diff --git a/versions.sh b/versions.sh new file mode 100755 index 0000000..6e4038a --- /dev/null +++ b/versions.sh @@ -0,0 +1,111 @@ +#!/bin/bash +set -Eeuo pipefail + +cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" + +# you can pass the versions (of Rosariosis) to this file for the initial/re-build +versions=("$@") +if [ ${#versions[@]} -eq 0 ]; then + # get the folders from the current root directory of the project as the versions + # if no versions where passed to the file + # this is the standard (2021) way fo doing this + # see (https://github.com/docker-library/php/blob/master/versions.sh#L38) + versions=(*/) + # was assume the folders to be correct + # and will serve as the rule + # so the json is build from that + json='{}' +else + # with the initial/re-build the versions.json is manually set + json="$(/dev/null + # get the hash + sha512=$(sha512sum rosariosis.tar.bz2 | cut -d " " -f 1) + # remove the file + rm rosariosis.tar.bz2 + + # set the hash to the JSON + if [ -n "$sha512" ] && [ -n "$package" ]; then + export sha512 + export package + doc="$(jq <<<"$doc" -c '.sha512 = env.sha512')" + doc="$(jq <<<"$doc" -c '.package = env.package')" + fi + + # get the default php version + defaultPHP=$(echo $versionsHelper | jq -r '.[env.version].php') + # get the PHP versions + phpVersions=$(echo $versionsHelper | jq -r '.[env.version].phpVersions | keys[]' | jq -R -s -c '. / "\n" - [""]') + # get the default variant + defaultVariant=$(echo $versionsHelper | jq -r '.[env.version].variant') + # get the variants + variants=$(echo $versionsHelper | jq -r '.[env.version].variants') + # get the aliases + aliases=$(echo $versionsHelper | jq -r '.[env.version].aliases') + + # echo some version details + echo "### Rosariosis $version.x details" + echo "# Version => $fullVersion" + echo "# PHP => $phpVersions" + echo "# sha512 => $sha512" + + # build this fullVersion matrix + # and add it to the JSON + json="$( + jq <<<"$json" -c \ + --argjson doc "$doc" \ + --argjson phpVersions "$phpVersions" \ + --argjson aliases "$aliases" \ + --argjson variants "$variants" \ + --arg defaultPHP "$defaultPHP" \ + --arg defaultVariant "$defaultVariant" ' + .[env.version] = { + version: env.fullVersion, + php: $defaultPHP, + phpVersions: $phpVersions, + variant: $defaultVariant, + variants: $variants, + aliases: $aliases, + } + $doc + ' + )" +done + +# store the JSON to the file system +jq <<<"$json" -S . >versions.json