diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 43065d0..05580ba 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -89,6 +89,45 @@ jobs: --set "*.cache-from=type=gha,scope=php" \ --set "*.cache-to=type=gha,scope=php,mode=max" + app: + name: App images + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + with: + platforms: amd64,arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve PHP FPM minor versions + id: versions + run: | + echo "php83_minor=$(bin/helper php-fpm-minor 8.3)" >> "$GITHUB_OUTPUT" + echo "php84_minor=$(bin/helper php-fpm-minor 8.4)" >> "$GITHUB_OUTPUT" + echo "php85_minor=$(bin/helper php-fpm-minor 8.5)" >> "$GITHUB_OUTPUT" + + - name: Build and push App images + env: + PHP83_MINOR: ${{ steps.versions.outputs.php83_minor }} + PHP84_MINOR: ${{ steps.versions.outputs.php84_minor }} + PHP85_MINOR: ${{ steps.versions.outputs.php85_minor }} + run: | + docker buildx bake -f app/docker-bake.hcl --pull --push \ + --set "*.cache-from=type=gha,scope=app" \ + --set "*.cache-to=type=gha,scope=app,mode=max" + nginx: name: Nginx images runs-on: ubuntu-24.04 diff --git a/CLAUDE.md b/CLAUDE.md index 190056a..6fd8b9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,8 @@ make run-frankenphp-tests # Run FrankenPHP image tests PHP 8.5 images exist but are not marked `latest` — PHP 8.5 is not yet considered ready for Drupal projects. +See [CONFIGURATION.md](CONFIGURATION.md) for PHP extensions, binaries, certificates, and PHP ini settings across all images. + ### Helper & Sidecar Images #### Nginx (`nginx/`) diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..5697216 --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,126 @@ +# PHP Image Configuration + +Reference for PHP extensions, binaries, certificates, and PHP ini settings across all PHP images. + +## PHP Settings + +### Static Settings + +Hardcoded in `php/rootfs/etc/php/conf.d/98_custom.ini` (`php/*` images) and `app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-zzz-custom.ini` (`druidfi/app`). + +| Setting | Value | Notes | +|---------|-------|-------| +| `expose_php` | `Off` | | +| `memory_limit` | `512M` | Overridable via env var | +| `date.timezone` | `Europe/Helsinki` | | +| `realpath_cache_size` | `8M` | Default: 4096K | +| `opcache.memory_consumption` | `512` | Default: 128 | +| `opcache.interned_strings_buffer` | `64` | Default: 8 | +| `opcache.max_accelerated_files` | `30000` | Default: 10000 | +| `opcache.enable_file_override` | `1` | Default: 0 | +| `opcache.validate_timestamps` | `1` (dev) / `0` (prod) | Disabled in prod via `99_production.ini` | +| `apc.shm_size` | `64M` | Default: 32M | + +### Dynamic Settings (Environment Variables) + +Set via env vars at container startup. Template files processed by `ep` (envplate). + +| Env Var | Default | PHP Setting | `php/*` images | `druidfi/app` | +|---------|---------|-------------|:--------------:|:-------------:| +| `PHP_MEMORY_LIMIT` | `512M` | `memory_limit` | ✓ | ✓ | +| `PHP_POST_MAX_SIZE` | `32M` | `post_max_size` | ✓ | ✓ | +| `PHP_UPLOAD_MAX_FILESIZE` | `32M` | `upload_max_filesize` | ✓ | ✓ | +| `PHP_MAX_EXECUTION_TIME` | `180` | `max_execution_time` | — | ✓ | +| `PHP_MAX_INPUT_VARS` | `2000` | `max_input_vars` | — | ✓ | +| `PHP_DISPLAY_ERRORS` | `On` | `display_errors` | — | ✓ | +| `PHP_SENDMAIL_PATH` | `msmtp -t` / `sendmail -S host.docker.internal:1025 -t` | `sendmail_path` | ✓ | ✓ | + +### Xdebug + +Disabled by default. Set `XDEBUG_ENABLE=true` to enable at container startup. + +| Setting | Value | +|---------|-------| +| `xdebug.mode` | `debug` | +| `xdebug.client_host` | `host.docker.internal` | +| `xdebug.idekey` | `PHPSTORM` | +| `xdebug.log` | `/tmp/xdebug.log` | + +## PHP Extensions + +Extensions accumulate across image layers (`php` → `php-fpm` → `drupal` → `drupal-web`). `druidfi/app` builds on the official `php:fpm-alpine` base and uses `mlocati/php-extension-installer`. + +| Extension | Type | `druidfi/php` | `druidfi/php-fpm` | `druidfi/drupal` | `druidfi/drupal-web` | `druidfi/app` | +|-----------|------|:-------------:|:-----------------:|:----------------:|:--------------------:|:-------------:| +| curl | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| fileinfo | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| iconv | core (gnu-libiconv) | ✓ | ✓ | ✓ | ✓ | ✓ | +| mbstring | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| opcache | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| openssl | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| phar | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| session | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| zip | core | ✓ | ✓ | ✓ | ✓ | ✓ | +| apcu | PECL | ✓ | ✓ | ✓ | ✓ | ✓ | +| imagick | PECL | ✓ | ✓ | ✓ | ✓ | ✓ | +| redis | PECL | ✓ | ✓ | ✓ | ✓ | ✓ | +| uploadprogress | PECL | ✓ | ✓ | ✓ | ✓ | ✓ | +| xdebug | PECL | ✓ | ✓ | ✓ | ✓ | ✓ | +| bcmath | core | — | — | ✓ | ✓ | ✓ | +| ctype | core | — | — | ✓ | ✓ | ✓ | +| dom | core | — | — | ✓ | ✓ | ✓ | +| exif | core | — | — | ✓ | ✓ | ✓ | +| gd | core | — | — | ✓ | ✓ | ✓ | +| intl | core | — | — | ✓ | ✓ | — | +| pdo | core | — | — | ✓ | ✓ | ✓ | +| pdo_mysql | core | — | — | ✓ | ✓ | ✓ | +| simplexml | core | — | — | ✓ | ✓ | ✓ | +| soap | core | — | — | ✓ | ✓ | — | +| sockets | core | — | — | ✓ | ✓ | ✓ | +| sodium | core | — | — | ✓ | ✓ | ✓ | +| tokenizer | core | — | — | ✓ | ✓ | ✓ | +| xml | core | — | — | ✓ | ✓ | ✓ | +| xmlreader | core | — | — | ✓ | ✓ | ✓ | +| xmlwriter | core | — | — | ✓ | ✓ | ✓ | +| igbinary | PECL | — | — | ✓ | ✓ | ✓ | + +> PHP 8.5 omits `opcache` (not yet in Alpine edge/testing). `intl` and `soap` are absent in `druidfi/app` — not in the official `php:fpm-alpine` base and not explicitly installed. `igbinary` enables binary serialization for the Redis/Valkey phpredis extension (used with the Drupal redis module). + +## Installed Binaries + +| Binary | Source | `druidfi/php` | `druidfi/php-fpm` | `druidfi/drupal` | `druidfi/drupal-web` | `druidfi/app` | +|--------|--------|:-------------:|:-----------------:|:----------------:|:--------------------:|:-------------:| +| `php` | Alpine apk / official base | ✓ | ✓ | ✓ | ✓ | ✓ | +| `composer` | `composer/composer:2.9-bin` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `ep` | `amazeeio/envplate:26.3.0` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `entrypoint` | `rootfs/usr/local/bin/` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `fix-permissions` | `rootfs/usr/local/bin/` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `bash` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `curl` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `git` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `make` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `msmtp` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `ssmtp` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `nano` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `tar` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `tini` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `fastfetch` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `patch` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `unzip` | Alpine apk | ✓ | ✓ | ✓ | ✓ | ✓ | +| `sudo` | Alpine apk | ✓ | ✓ | ✓ | ✓ | — | +| `doas` | Alpine apk | — | — | — | — | ✓ | +| `php-fpm` | Alpine apk / official base | — | ✓ | ✓ | ✓ | ✓ | +| `openssh` / `openssh-client` | Alpine apk | — | — | ✓ | ✓ | ✓ | +| `rsync` | Alpine apk | — | — | ✓ | ✓ | ✓ | +| `mariadb-client` | Alpine apk | — | — | ✓ | ✓ | ✓ | +| `install-php-extensions` | `mlocati/php-extension-installer` | — | — | — | — | ✓ | +| `nginx` | Alpine apk | — | — | — | ✓ | ✓ | +| `gdpr-dump` | GitHub releases (Smile-SA) | — | — | — | ✓ | ✓ | + +## Certificates + +Installed to `/opt/ssl/` in all images (from `rootfs/opt/ssl/` in the repo). + +| File | `druidfi/php` | `druidfi/php-fpm` | `druidfi/drupal` | `druidfi/drupal-web` | `druidfi/app` | Purpose | +|------|:-------------:|:-----------------:|:----------------:|:--------------------:|:-------------:|---------| +| `DigiCertGlobalRootCA.crt.pem` | ✓ | ✓ | ✓ | ✓ | ✓ | Azure MySQL Flexible Server | diff --git a/Makefile b/Makefile index 51b3f69..f9f712d 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ else CURRENT_ARCH := amd64 endif +PHP_VERSIONS := 8.3 8.4 8.5 + include $(PROJECT_DIR)/make/*.mk PHONY += help @@ -39,6 +41,10 @@ define get_php_minor $(shell bin/helper phpminor $1) endef +define get_php_fpm_minor +$(shell bin/helper php-fpm-minor $1) +endef + define get_frankenphp_version $(shell bin/helper frankenphpversion) endef diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..7bcf81f --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,61 @@ +# syntax=docker/dockerfile:1 + +# +# App image +# +FROM php-base + +ENV KIND=druid-docker-image \ + APP_PATH=/app \ + DEFAULT_USER=druid \ + DEFAULT_USER_UID=1000 \ + APP_ENV=prod \ + COMPOSER_HOME=/home/druid/.composer \ + COMPOSER_AUDIT_ABANDONED=report \ + COMPOSER_FUND=0 \ + PATH="${PATH}:/home/druid/.composer/vendor/bin:/app/vendor/bin" \ + DRUPAL_DB_NAME=drupal \ + DRUPAL_DB_USER=drupal \ + DRUPAL_DB_PASS=drupal \ + DRUPAL_DB_HOST=db \ + DRUPAL_DB_PORT=3306 + +WORKDIR ${APP_PATH} + +COPY --link --from=composer/composer:2.9-bin /composer /usr/local/bin/ +COPY --link --from=amazeeio/envplate:26.3.0 /usr/local/bin/ep /usr/local/bin/ +COPY --link --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + +RUN apk --no-cache add \ + bash curl doas fastfetch git make \ + mariadb-client mariadb-connector-c \ + msmtp nano nginx openssh-client patch rsync ssmtp tar tini unzip && \ + install-php-extensions \ + apcu bcmath exif gd igbinary imagick opcache pdo_mysql redis sockets uploadprogress xdebug zip && \ + addgroup -S ${DEFAULT_USER} -g ${DEFAULT_USER_UID} && \ + adduser -D -S -G ${DEFAULT_USER} -u ${DEFAULT_USER_UID} -s /bin/bash ${DEFAULT_USER} && \ + adduser ${DEFAULT_USER} www-data && \ + mkdir -p /home/${DEFAULT_USER}/.composer && \ + curl -sSf https://composer.github.io/releases.pub -o /home/${DEFAULT_USER}/.composer/keys.tags.pub && \ + curl -sSf https://composer.github.io/snapshots.pub -o /home/${DEFAULT_USER}/.composer/keys.dev.pub && \ + chown -R ${DEFAULT_USER}:${DEFAULT_USER} /home/${DEFAULT_USER} ${APP_PATH} && \ + chmod -R a+rwx /var/lib/nginx /var/log/nginx && \ + curl -sSL https://github.com/Smile-SA/gdpr-dump/releases/latest/download/gdpr-dump.phar -o /usr/local/bin/gdpr-dump && \ + chmod +x /usr/local/bin/gdpr-dump && \ + gdpr-dump --version + +COPY --chown=${DEFAULT_USER}:${DEFAULT_USER} rootfs/home/druid/ /home/druid +COPY rootfs/entrypoints/ /entrypoints/ +COPY rootfs/etc/ /etc/ +COPY rootfs/usr/local/ /usr/local/ + +# https://learn.microsoft.com/en-us/azure/mysql/flexible-server/how-to-connect-tls-ssl +RUN mkdir -p /opt/ssl +COPY --chmod=644 rootfs/opt/ssl/DigiCertGlobalRootCA.crt.pem /opt/ssl/ + +EXPOSE 8080/tcp + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["entrypoint"] +USER ${DEFAULT_USER} +SHELL ["/bin/bash", "-c"] diff --git a/app/build.mk b/app/build.mk new file mode 100644 index 0000000..68d4eff --- /dev/null +++ b/app/build.mk @@ -0,0 +1,28 @@ +BAKE_FLAGS := --pull --no-cache --push + +PHONY += --app-bake +--app-bake: + @PHP83_MINOR=$(call get_php_fpm_minor,8.3) PHP84_MINOR=$(call get_php_fpm_minor,8.4) PHP85_MINOR=$(call get_php_fpm_minor,8.5) \ + docker buildx bake -f app/docker-bake.hcl $(BAKE_FLAGS) + +PHONY += app-bake-all +app-bake-all: buildx-create --app-bake buildx-destroy ## Bake all APP images + +PHONY += app-bake-print +app-bake-print: BAKE_FLAGS := --print +app-bake-print: --app-bake ## Print bake plan for App images + +PHONY += app-bake-local +app-bake-local: BAKE_FLAGS := --pull --progress plain --no-cache --load --set *.platform=linux/$(CURRENT_ARCH) +app-bake-local: --app-bake run-app-tests ## Bake all App images locally + +PHONY += app-bake-test +app-bake-test: BAKE_FLAGS := --pull --progress plain --no-cache +app-bake-test: --app-bake run-app-tests ## CI test for App images + +PHONY += run-app-tests +run-app-tests: + @for v in $(PHP_VERSIONS); do \ + printf "\n\e[0;33mRun tests in ghcr.io/druidfi/app:php-$$v\e[0m\n\n"; \ + docker run --rm -t -v $(CURDIR)/tests/scripts:/app/scripts ghcr.io/druidfi/app:php-$$v /app/scripts/tests.sh || exit 1; \ + done diff --git a/app/docker-bake.hcl b/app/docker-bake.hcl new file mode 100644 index 0000000..3c20e51 --- /dev/null +++ b/app/docker-bake.hcl @@ -0,0 +1,92 @@ +variable "REPO_BASE" { + default = "druidfi/app" +} + +variable "PHP83_MINOR" {} +variable "PHP84_MINOR" {} +variable "PHP85_MINOR" {} + +group "default" { + targets = ["php-83", "php-84", "php-85"] +} + +target "common" { + platforms = ["linux/amd64", "linux/arm64"] + labels = { + "org.opencontainers.image.url" = "https://github.com/druidfi/docker-images" + "org.opencontainers.image.source" = "https://github.com/druidfi/docker-images" + "org.opencontainers.image.licenses" = "MIT" + "org.opencontainers.image.vendor" = "Druid Oy" + "org.opencontainers.image.created" = timestamp() + } +} + +# +# PHP +# + +target "php" { + context = "./app" + args = { + PHP_MAJOR_VERSION = 8 + } +} + +target "php-83" { + inherits = ["common", "php"] + args = { + PHP_VERSION = "8.3" + PHP_SHORT_VERSION = "83" + } + contexts = { + php-base = "docker-image://php:${PHP83_MINOR}-fpm-alpine" + } + labels = { + "org.opencontainers.image.title" = "Druid App image with PHP 8.3" + "org.opencontainers.image.description" = "Base PHP 8.3 image" + } + tags = [ + "ghcr.io/${REPO_BASE}:php-8.3", + "ghcr.io/${REPO_BASE}:php-${PHP83_MINOR}", + ] +} + +target "php-84" { + inherits = ["common", "php"] + args = { + PHP_VERSION = "8.4" + PHP_SHORT_VERSION = "84" + } + contexts = { + php-base = "docker-image://php:${PHP84_MINOR}-fpm-alpine" + } + labels = { + "org.opencontainers.image.title" = "Druid App image with PHP 8.4" + "org.opencontainers.image.description" = "Base PHP 8.4 image" + } + tags = [ + "ghcr.io/${REPO_BASE}:php-8", + "ghcr.io/${REPO_BASE}:php-8.4", + "ghcr.io/${REPO_BASE}:php-${PHP84_MINOR}", + "ghcr.io/${REPO_BASE}:latest", + ] +} + +target "php-85" { + inherits = ["common", "php"] + args = { + PHP_VERSION = "8.5" + PHP_SHORT_VERSION = "85" + } + contexts = { + php-base = "docker-image://php:${PHP85_MINOR}-fpm-alpine" + } + labels = { + "org.opencontainers.image.title" = "Druid App image with PHP 8.5" + "org.opencontainers.image.description" = "Base PHP 8.5 image" + } + tags = [ + "ghcr.io/${REPO_BASE}:php-8.5", + "ghcr.io/${REPO_BASE}:php-${PHP85_MINOR}", + ] +} diff --git a/app/rootfs/entrypoints/00-umask.sh b/app/rootfs/entrypoints/00-umask.sh new file mode 100644 index 0000000..5d7f088 --- /dev/null +++ b/app/rootfs/entrypoints/00-umask.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Make sure that new files generated by Docker have group write permission +umask 002 diff --git a/app/rootfs/entrypoints/05-fix-permissions.sh b/app/rootfs/entrypoints/05-fix-permissions.sh new file mode 100755 index 0000000..e84630a --- /dev/null +++ b/app/rootfs/entrypoints/05-fix-permissions.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +FILES_DIR=/app/public/sites/default/files + +if [ -d "${FILES_DIR}" ]; then + if find "${FILES_DIR}" -not -user www-data -print -quit 2>/dev/null | grep -q .; then + echo "Fix ownership of ${FILES_DIR}..." + doas chown -R www-data:www-data "${FILES_DIR}" + fi +fi diff --git a/app/rootfs/entrypoints/10-ssh-agent.sh b/app/rootfs/entrypoints/10-ssh-agent.sh new file mode 100644 index 0000000..f0057c7 --- /dev/null +++ b/app/rootfs/entrypoints/10-ssh-agent.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +# Set a safe default for SSH_AUTH_SOCK at runtime if not provided +: "${SSH_AUTH_SOCK:=/tmp/ssh-agent}" +mkdir -p "$(dirname "${SSH_AUTH_SOCK}")" + +SOCKET_NAMES="druid amazeeio" +SOCKET_FOUND=0 + +for i in ${SOCKET_NAMES} +do + : + SOCKET="/tmp/${i}_ssh-agent/socket" + + if [ -S "${SOCKET}" ]; then + echo "Found socket from ${SOCKET}" + echo "Symlink ${SSH_AUTH_SOCK} to ${SOCKET}..." + ln -sf "${SOCKET}" "$SSH_AUTH_SOCK" + SOCKET_FOUND=1 + fi +done + +if [ ${SOCKET_FOUND} = 0 ]; then + echo "No socket found" +fi diff --git a/app/rootfs/entrypoints/15-xdebug.sh b/app/rootfs/entrypoints/15-xdebug.sh new file mode 100644 index 0000000..79a51df --- /dev/null +++ b/app/rootfs/entrypoints/15-xdebug.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +XDEBUG_INI=/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + +# Detect Docker host: try DNS first, fall back to default gateway +if [ -z "$XDEBUG_CLIENT_HOST" ]; then + if getent hosts host.docker.internal &>/dev/null; then + XDEBUG_CLIENT_HOST="host.docker.internal" + else + XDEBUG_CLIENT_HOST=$(ip route show default 2>/dev/null | awk '/default/ {print $3; exit}') + XDEBUG_CLIENT_HOST="${XDEBUG_CLIENT_HOST:-host.docker.internal}" + fi +fi + +if [ "$XDEBUG_ENABLE" = "true" ]; then + echo "Start with Xdebug enabled (client: ${XDEBUG_CLIENT_HOST}). Remove XDEBUG_ENABLE=true ENV variable to disable it." + if [ -f "$XDEBUG_INI" ]; then + echo "Already enabled..." + else + doas mv "$XDEBUG_INI".disabled "$XDEBUG_INI" + doas touch /tmp/xdebug.log && doas chmod 666 /tmp/xdebug.log + fi + doas sed -i "s/^xdebug.client_host=.*/xdebug.client_host=${XDEBUG_CLIENT_HOST}/" "$XDEBUG_INI" +else + echo "Start with Xdebug disabled. Add XDEBUG_ENABLE=true ENV variable to enable it." + if [ -f "$XDEBUG_INI" ]; then + doas mv "$XDEBUG_INI" "$XDEBUG_INI".disabled + else + echo "Already disabled..." + fi +fi diff --git a/app/rootfs/entrypoints/19-php_ini.sh b/app/rootfs/entrypoints/19-php_ini.sh new file mode 100644 index 0000000..53fc503 --- /dev/null +++ b/app/rootfs/entrypoints/19-php_ini.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +TEMPLATE=/usr/local/etc/php/conf.d/docker-php-ext-zzz-dynamic.ini.ep +TARGET=/usr/local/etc/php/conf.d/docker-php-ext-zzz-dynamic.ini + +if [ -f "$TEMPLATE" ]; then + echo "Prepare PHP docker-php-ext-zzz-dynamic.ini conf..." + + doas ep -v "$TEMPLATE" + doas mv "$TEMPLATE" "$TARGET" +fi + +if [ "$APP_ENV" = "prod" ]; then + echo "Enable production PHP ini..." + doas cp -f /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini +fi diff --git a/app/rootfs/entrypoints/20-php-fpm.sh b/app/rootfs/entrypoints/20-php-fpm.sh new file mode 100644 index 0000000..898b475 --- /dev/null +++ b/app/rootfs/entrypoints/20-php-fpm.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +echo "Start up PHP-FPM..." + +doas php-fpm & + +# Usage: php [-n] [-e] [-h] [-i] [-m] [-v] [-t] [-p ] [-g ] [-c ] [-d foo[=bar]] [-y ] [-D] [-F [-O]] +# -c | Look for php.ini file in this directory +# -n No php.ini file will be used +# -d foo[=bar] Define INI entry foo with value 'bar' +# -e Generate extended information for debugger/profiler +# -h This help +# -i PHP information +# -m Show compiled in modules +# -v Version number +# -p, --prefix +# Specify alternative prefix path to FastCGI process manager (default: /usr). +# -g, --pid +# Specify the PID file location. +# -y, --fpm-config +# Specify alternative path to FastCGI process manager config file. +# -t, --test Test FPM configuration and exit +# -D, --daemonize force to run in background, and ignore daemonize option from config file +# -F, --nodaemonize +# force to stay in foreground, and ignore daemonize option from config file +# -O, --force-stderr +# force output to stderr in nodaemonize even if stderr is not a TTY +# -R, --allow-to-run-as-root +# Allow pool to run as root (disabled by default) diff --git a/app/rootfs/entrypoints/30-nginx.sh b/app/rootfs/entrypoints/30-nginx.sh new file mode 100644 index 0000000..7860bd5 --- /dev/null +++ b/app/rootfs/entrypoints/30-nginx.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "Prepare Nginx conf..." + +doas ep -v /etc/nginx/conf.d/default.conf + +echo "Start up Nginx..." + +nginx diff --git a/app/rootfs/entrypoints/40-ssmtp.sh b/app/rootfs/entrypoints/40-ssmtp.sh new file mode 100755 index 0000000..24f24a5 --- /dev/null +++ b/app/rootfs/entrypoints/40-ssmtp.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [ ${SSMTP_MAILHUB+x} ]; then + + echo "- Prepare SSMTP conf..." + + doas ep -v /etc/ssmtp/ssmtp.conf + +fi + +echo "- Prepare MSMTP conf in /etc/msmtprc..." + +doas ep -v /etc/msmtprc diff --git a/app/rootfs/etc/doas.d/doas.conf b/app/rootfs/etc/doas.d/doas.conf new file mode 100644 index 0000000..765a368 --- /dev/null +++ b/app/rootfs/etc/doas.d/doas.conf @@ -0,0 +1 @@ +permit nopass keepenv druid as root diff --git a/app/rootfs/etc/drush/drush.yml b/app/rootfs/etc/drush/drush.yml new file mode 100644 index 0000000..9b3bff8 --- /dev/null +++ b/app/rootfs/etc/drush/drush.yml @@ -0,0 +1,26 @@ +# +# See https://github.com/drush-ops/drush/blob/master/examples/example.drush.yml +# + +# This section is for setting command-specific options. +command: + sql: + options: + extra: "--skip-ssl" + dump: + options: + extra-dump: "--skip-ssl --no-tablespaces" + +sql: + # List of tables whose *data* is skipped by the 'sql-dump' and 'sql-sync' + # commands when the "--structure-tables-key=common" option is provided. + # You may add specific tables to the existing array or add a new element. + structure-tables: + common: + - 'cache' + - 'cache_*' + - 'history' + - 'search_*' + - 'sessions' + - 'watchdog' + - 'feeds_log' diff --git a/app/rootfs/etc/my.cnf.d/mariadb-client.cnf b/app/rootfs/etc/my.cnf.d/mariadb-client.cnf new file mode 100644 index 0000000..63fa3a1 --- /dev/null +++ b/app/rootfs/etc/my.cnf.d/mariadb-client.cnf @@ -0,0 +1,3 @@ +[client] +ssl-ca = /opt/ssl/DigiCertGlobalRootCA.crt.pem +disable-ssl-verify-server-cert diff --git a/app/rootfs/etc/nginx/conf.d/default.conf b/app/rootfs/etc/nginx/conf.d/default.conf new file mode 100644 index 0000000..8461f99 --- /dev/null +++ b/app/rootfs/etc/nginx/conf.d/default.conf @@ -0,0 +1,140 @@ +server { + listen [::]:8080 default_server; + listen 8080 default_server; + server_name _; + client_max_body_size 512M; + + root /app/${WEBROOT:-public}; + index index.php; + + # rewriting /index.php to / because after https://www.drupal.org/node/2599326 + # autocomplete URLs are forced to go to index.php + rewrite ^/index.php / last; + + # The 'default' location. + location / { + # This has to come before any *.txt path-based blocking + # Support for the securitytxt module + # http://drupal.org/project/securitytxt. + # RFC8615 standard path. + location ~* /\.well-known/security\.txt(\.sig)?$ { + access_log off; + try_files $uri @rewrite; + } + + location ~ ^/(status|ping)$ { + include fastcgi_params; + fastcgi_pass 127.0.0.1:9000; + fastcgi_param SCRIPT_FILENAME $fastcgi_script_name; + allow 127.0.0.1; + deny all; + } + + location ~ /(?:a|A)utodiscover/(?:a|A)utodiscover.xml { + deny all; + access_log off; + return 404; + } + + # Do not allow access to .txt and .md unless inside sites/*/files/ + location ~* ^(?!.+sites\/.+\/files\/).+\.(txt|md)$ { + deny all; + access_log off; + log_not_found off; + } + + # Replicate the Apache directive of Drupal standard + # .htaccess. Disable access to any code files. Return a 404 to curtail + # information disclosure. + location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|.*sql\.gz|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^\/(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|web\.config)$|composer\.(json|lock)$|^\/#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { + deny all; + access_log off; + log_not_found off; + return 404; + } + + ## Directives for installing drupal. + location ~* ^(/install.php|/core/install.php) { + try_files /dev/null @php; + } + + # Disallow access to any dot files, but send the request to Drupal + location ~* /\. { + try_files /dev/null @rewrite; + } + + # Direct Access to .php files is not allowed and is sent to Drupal instead + location ~* ^.+\.php$ { + try_files /dev/null @rewrite; + } + + # Try to find a file with given URL, if not pass to Drupal + try_files $uri @rewrite; + } + + # PHP Location. + # Warning: This allows to execute any PHP files, use with care! + location @php { + include /etc/nginx/fastcgi.conf; + fastcgi_pass 127.0.0.1:9000; + } + + location @rewrite { + include /etc/nginx/fastcgi.conf; + fastcgi_param SCRIPT_NAME /index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root/index.php; + fastcgi_pass 127.0.0.1:9000; + } + + # Trying to access private files directly returns a 404. + location /sites/default/files/private/ { + internal; + } + + # Passes image style and asset generation to PHP. + location ~ ^/sites/.*/files/(css|js|styles)/ { + try_files $uri @rewrite; + } + + # Disallow access to vendor directory. + location ^~ /core/vendor/ { + deny all; + access_log off; + log_not_found off; + } + + # Disallow access to vendor directory. + location ^~ /vendor/ { + deny all; + access_log off; + log_not_found off; + } + + # Support for the robotstxt module + # http://drupal.org/project/robotstxt. + location = /robots.txt { + access_log off; + try_files $uri @rewrite; + } + + # Add support for the humanstxt module + # http://drupal.org/project/humanstxt. + location = /humans.txt { + access_log off; + try_files $uri @rewrite; + } + + # Support for favicon. Return an 1x1 transparent GIF if it doesn't + # exist. + location = /favicon.ico { + expires 30d; + try_files /favicon.ico @empty; + } + + # Return an in memory 1x1 transparent GIF. + location @empty { + expires 30d; + empty_gif; + } + +} diff --git a/app/rootfs/etc/nginx/fastcgi.conf b/app/rootfs/etc/nginx/fastcgi.conf new file mode 100644 index 0000000..6ff38e5 --- /dev/null +++ b/app/rootfs/etc/nginx/fastcgi.conf @@ -0,0 +1,54 @@ +set $fastcgi_port "80"; +if ($http_x_forwarded_proto = 'https') { + set $fastcgi_https "on"; + set $fastcgi_port "443"; +} + +fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; +fastcgi_param HTTPS $fastcgi_https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; + +# Setting to Port 80 and 443 based on if we have an upstream https or not +fastcgi_param SERVER_PORT $fastcgi_port; + +# Setting to $host as $server_name is empty all the time +fastcgi_param SERVER_NAME $host; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; + +# Mitigate https://httpoxy.org/ vulnerabilities +fastcgi_param HTTP_PROXY ""; + +# Mitigate CVE-2018-14773: https://symfony.com/blog/cve-2018-14773-remove-support-for-legacy-and-risky-http-headers +fastcgi_param HTTP_X-ORIGINAL-URL ""; +fastcgi_param HTTP_X_ORIGINAL_URL ""; +fastcgi_param HTTP_X-REWRITE-URL ""; +fastcgi_param HTTP_X_REWRITE_URL ""; + +fastcgi_keep_conn on; +fastcgi_index index.php; +fastcgi_hide_header 'X-Generator'; + +fastcgi_buffers 256 32k; +fastcgi_buffer_size 32k; +fastcgi_read_timeout 3600s; +fastcgi_temp_path /tmp/fastcgi_temp; diff --git a/app/rootfs/etc/nginx/nginx.conf b/app/rootfs/etc/nginx/nginx.conf new file mode 100644 index 0000000..7a89950 --- /dev/null +++ b/app/rootfs/etc/nginx/nginx.conf @@ -0,0 +1,80 @@ +daemon off; +worker_processes auto; + +error_log /dev/stderr warn; +pid /tmp/nginx.pid; + + +events { + worker_connections 1024; + multi_accept on; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/stdout main; + + send_timeout 60s; + sendfile on; + client_body_timeout 60s; + client_header_timeout 60s; + client_max_body_size 100M; + client_body_buffer_size 16k; + client_header_buffer_size 4k; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + fastcgi_intercept_errors on; + large_client_header_buffers 8 16K; + keepalive_timeout 75s; + keepalive_requests 1000; + reset_timedout_connection off; + tcp_nodelay on; + tcp_nopush on; + server_tokens off; + underscores_in_headers off; + + gzip on; + gzip_buffers 16 8k; + gzip_comp_level 1; + gzip_http_version 1.1; + gzip_min_length 20; + gzip_vary on; + gzip_proxied any; + gzip_disable msie6; + gzip_types application/atom+xml + application/geo+json + application/javascript + application/json + application/ld+json + application/manifest+json + application/rdf+xml + application/rss+xml + application/vnd.ms-fontobject + application/wasm + application/x-web-app-manifest+json + application/xhtml+xml + application/xml + font/otf + image/bmp + image/svg+xml + text/cache-manifest + text/calendar + text/css + text/javascript + text/markdown + text/plain + text/vcard + text/vnd.rim.location.xloc + text/vtt + text/x-component + text/x-cross-domain-policy; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/app/rootfs/home/druid/.bashrc b/app/rootfs/home/druid/.bashrc new file mode 100644 index 0000000..3e32f29 --- /dev/null +++ b/app/rootfs/home/druid/.bashrc @@ -0,0 +1,20 @@ +#!/bin/sh + +# Make sure that new files generated by Docker have group write permission +source /entrypoints/00-umask.sh + +# Druid prompt with fire +if [ "$PS1" ]; then + NORMAL="\[\e[0m\]" + RED="\[\e[1;31m\]" + YELLOW="\[\e[1;33m\]" + PS1="🔥 ${YELLOW}[${APP_ENV:-Unknown}] ${RED}[${HOSTNAME}] ${YELLOW}\w${NORMAL} $ " +fi + +# Aliases +alias ll="ls -lah" +alias make="make -s" +alias sudo="doas" + +# Run fastfetch on login +fastfetch --config /home/druid/.config/fastfetch/config.jsonc diff --git a/app/rootfs/home/druid/.config/fastfetch/config.jsonc b/app/rootfs/home/druid/.config/fastfetch/config.jsonc new file mode 100644 index 0000000..4f4e0e7 --- /dev/null +++ b/app/rootfs/home/druid/.config/fastfetch/config.jsonc @@ -0,0 +1,40 @@ +{ + "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", + "logo": { + "source": "/home/druid/ascii", + "type": "file", + "color": { + "1": "red" + } + }, + "display": { + "separator": ": " + }, + "modules": [ + "os", + { + "type": "command", + "key": "PHP", + "text": "php -r 'echo phpversion();'" + }, + { + "type": "command", + "key": "Composer", + "text": "composer --version | awk '{print $3}'" + }, + "kernel", + "uptime", + "shell", + { + "type": "command", + "key": "User ID", + "text": "id -u" + }, + { + "type": "command", + "key": "Groups", + "text": "id -Gn" + }, + "localip" + ] +} diff --git a/app/rootfs/home/druid/.ssh/config b/app/rootfs/home/druid/.ssh/config new file mode 100644 index 0000000..9a8a056 --- /dev/null +++ b/app/rootfs/home/druid/.ssh/config @@ -0,0 +1,8 @@ +Host * + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null + ForwardAgent yes + LogLevel QUIET +Host github.com + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null diff --git a/app/rootfs/home/druid/ascii b/app/rootfs/home/druid/ascii new file mode 100644 index 0000000..04aae6d --- /dev/null +++ b/app/rootfs/home/druid/ascii @@ -0,0 +1,16 @@ + , + *** + ,****** + *********** + **************** + **********,********** + *********** ********* + *********** ********** + ********** ********* + ******* ********* + ********* * ******** + ********* ** * ******** + ******** * * ******* + ******* *. * ******* + ******,** ******** + ***** ***** diff --git a/app/rootfs/opt/ssl/DigiCertGlobalRootCA.crt.pem b/app/rootfs/opt/ssl/DigiCertGlobalRootCA.crt.pem new file mode 100644 index 0000000..fd4341d --- /dev/null +++ b/app/rootfs/opt/ssl/DigiCertGlobalRootCA.crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- diff --git a/app/rootfs/usr/local/bin/entrypoint b/app/rootfs/usr/local/bin/entrypoint new file mode 100755 index 0000000..e1d45a9 --- /dev/null +++ b/app/rootfs/usr/local/bin/entrypoint @@ -0,0 +1,21 @@ +#!/bin/bash + +# This script will be the default ENTRYPOINT for all children docker images. +# It just sources all files within /entrypoints/* in an alphabetical order and then runs `exec` on the given parameter. + +set -e + +if [ -d /entrypoints ]; then + for i in /entrypoints/*; do + if [ -r "$i" ]; then + echo "# Source $i" + . "$i" + else + echo "! $i not sourced" + fi + done + unset i +fi + +echo "# Exec CMD: $@" +exec "$@" diff --git a/app/rootfs/usr/local/bin/fix-permissions b/app/rootfs/usr/local/bin/fix-permissions new file mode 100755 index 0000000..8259c29 --- /dev/null +++ b/app/rootfs/usr/local/bin/fix-permissions @@ -0,0 +1,6 @@ +#!/bin/sh +# Fix permissions on the given directory to allow group read/write of +# regular files and execute of directories. +find -L "$1" -exec chgrp 0 {} \; +find -L "$1" -exec chmod g+rw {} \; +find -L "$1" -type d -exec chmod g+x {} + diff --git a/app/rootfs/usr/local/bin/unzip b/app/rootfs/usr/local/bin/unzip new file mode 100755 index 0000000..5dfc0cd --- /dev/null +++ b/app/rootfs/usr/local/bin/unzip @@ -0,0 +1,2 @@ +#!/bin/sh +exec /usr/bin/unzip -o "$@" diff --git a/app/rootfs/usr/local/etc/php-fpm.d/zzz-docker.conf b/app/rootfs/usr/local/etc/php-fpm.d/zzz-docker.conf new file mode 100644 index 0000000..0a26936 --- /dev/null +++ b/app/rootfs/usr/local/etc/php-fpm.d/zzz-docker.conf @@ -0,0 +1,28 @@ +[global] +; Error log level. Possible values: alert, error, warning, notice, debug. Default value: notice. +log_level = notice +process_control_timeout = 30 + +[www] +user = www-data +group = www-data +; listen = [::]:9000 + +pm = dynamic +pm.max_children = 8 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 500 +pm.process_idle_timeout = 60s + +; Allow environment variables to reach PHP via getenv() +clear_env = no + +; Redirect worker stdout/stderr to main error log +catch_workers_output = yes + +php_value[memory_limit] = 512M + +pm.status_path = /status +ping.path = /ping diff --git a/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini b/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini new file mode 100644 index 0000000..7e679dd --- /dev/null +++ b/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini @@ -0,0 +1,7 @@ +[xdebug] +zend_extension=xdebug.so + +xdebug.mode=debug +xdebug.client_host=host.docker.internal +xdebug.idekey=PHPSTORM +xdebug.log=/tmp/xdebug.log diff --git a/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-zzz-custom.ini b/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-zzz-custom.ini new file mode 100644 index 0000000..9fa3abb --- /dev/null +++ b/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-zzz-custom.ini @@ -0,0 +1,17 @@ +[PHP] +expose_php=Off +memory_limit=512M +realpath_cache_size=8M + +[Date] +date.timezone="Europe/Helsinki" + +[opcache] +opcache.memory_consumption=512 +opcache.interned_strings_buffer=64 +opcache.max_accelerated_files=30000 +opcache.enable_file_override=1 +opcache.validate_timestamps=1 + +[apcu] +apc.shm_size=64M diff --git a/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-zzz-dynamic.ini.ep b/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-zzz-dynamic.ini.ep new file mode 100644 index 0000000..416eebd --- /dev/null +++ b/app/rootfs/usr/local/etc/php/conf.d/docker-php-ext-zzz-dynamic.ini.ep @@ -0,0 +1,8 @@ +[PHP] +display_errors=${PHP_DISPLAY_ERRORS:-On} +max_execution_time=${PHP_MAX_EXECUTION_TIME:-180} +max_input_vars=${PHP_MAX_INPUT_VARS:-2000} +memory_limit=${PHP_MEMORY_LIMIT:-512M} +post_max_size=${PHP_POST_MAX_SIZE:-32M} +sendmail_path=${PHP_SENDMAIL_PATH:-/usr/sbin/sendmail -S host.docker.internal:1025 -t} +upload_max_filesize=${PHP_UPLOAD_MAX_FILESIZE:-32M} diff --git a/bin/helper b/bin/helper index 61f6d54..d5577fe 100755 --- a/bin/helper +++ b/bin/helper @@ -9,6 +9,9 @@ FALLBACK_VERSION_85="8.5.3" FALLBACK_FRANKENPHP_VERSION="1.11.3" FALLBACK_FRANKENPHP_PHP84="8.4.19" FALLBACK_FRANKENPHP_PHP85="8.5.3" +FALLBACK_PHP_FPM_83="8.3.30" +FALLBACK_PHP_FPM_84="8.4.19" +FALLBACK_PHP_FPM_85="8.5.4" if [[ $COMMAND == alpineversion ]] then @@ -114,4 +117,31 @@ then fi +if [[ $COMMAND == php-fpm-minor ]] +then + + BRANCH=${2:-8.4} + SKOPEO=$(which skopeo 2>/dev/null) + + if [[ -z "$SKOPEO" ]]; then + [[ "$BRANCH" == "8.3" ]] && echo "$FALLBACK_PHP_FPM_83" && exit 0 + [[ "$BRANCH" == "8.4" ]] && echo "$FALLBACK_PHP_FPM_84" && exit 0 + [[ "$BRANCH" == "8.5" ]] && echo "$FALLBACK_PHP_FPM_85" && exit 0 + exit 1 + fi + + BRANCH_PATTERN="${BRANCH//./\\.}" + PHP_VERSION=$($SKOPEO list-tags docker://php 2>/dev/null \ + | jq -r '.Tags[]' \ + | grep -E "^${BRANCH_PATTERN}\.[0-9]+-fpm-alpine$" \ + | sort -V | tail -1 | sed 's/-fpm-alpine$//') + + [[ -n "$PHP_VERSION" ]] && echo "$PHP_VERSION" && exit 0 + [[ "$BRANCH" == "8.3" ]] && echo "$FALLBACK_PHP_FPM_83" && exit 0 + [[ "$BRANCH" == "8.4" ]] && echo "$FALLBACK_PHP_FPM_84" && exit 0 + [[ "$BRANCH" == "8.5" ]] && echo "$FALLBACK_PHP_FPM_85" && exit 0 + exit 1 + +fi + echo "No valid arguments given" && exit 1 diff --git a/make/build_targets.mk b/make/build_targets.mk index 9469ceb..d27e434 100644 --- a/make/build_targets.mk +++ b/make/build_targets.mk @@ -1,5 +1,6 @@ BUILD_TARGETS := +include $(PROJECT_DIR)/app/build.mk include $(PROJECT_DIR)/frankenphp/build.mk include $(PROJECT_DIR)/php/build.mk include $(PROJECT_DIR)/nginx/build.mk diff --git a/make/test_targets.mk b/make/test_targets.mk index e757a81..e719c0a 100644 --- a/make/test_targets.mk +++ b/make/test_targets.mk @@ -14,6 +14,24 @@ test-wp-running: @(cd tests/wp-test/ && docker-compose down -v && docker-compose up -d --remove-orphans) $(call step,See the example site: https://test-wp.docker.so) +PHONY += test-app-running +test-app-running: ## Start app test environment (https://app.docker.so) + $(call step,Run app test container) + @(cd tests/app/ && docker compose down -v && docker compose up -d --remove-orphans) + $(call step,See the test site: https://app.docker.so) + +PHONY += test-app-down +test-app-down: ## Stop app test environment + @(cd tests/app/ && docker compose down -v) + +PHONY += shell-app +shell-app: ## Shell into the running app test container + @docker compose -p test-app -f tests/app/compose.yaml exec app bash + +PHONY += run-app-tests-compose +run-app-tests-compose: ## Run test script inside the running app test container + @docker compose -p test-app -f tests/app/compose.yaml exec app /app/scripts/tests.sh + PHONY += example-drupal example-drupal: $(call step,Raise up example Drupal setup with Nginx and PHP containers) diff --git a/php/Dockerfile b/php/Dockerfile index 7ad1eb8..4ab287f 100644 --- a/php/Dockerfile +++ b/php/Dockerfile @@ -24,19 +24,14 @@ ENV KIND=druid-docker-image \ DEFAULT_USER=druid \ DEFAULT_USER_UID=1000 -# See https://docs.microsoft.com/en-us/azure/mysql/howto-configure-ssl -ADD --chmod=0644 https://cacerts.digicert.com/BaltimoreCyberTrustRoot.crt.pem /opt/ssl/ -# See https://learn.microsoft.com/en-us/azure/postgresql/single-server/concepts-certificate-rotation#what-change-was-scheduled-to-be-performed-starting-december-2022-122022 -ADD --chmod=0644 https://cacerts.digicert.com/DigiCertGlobalRootG2.crt.pem /opt/ssl/ # https://learn.microsoft.com/en-us/azure/mysql/flexible-server/how-to-connect-tls-ssl -#ADD --chmod=0644 https://dl.cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem /opt/ssl/ COPY --chmod=644 rootfs/opt/ssl/DigiCertGlobalRootCA.crt.pem /opt/ssl/ RUN < /etc/sudoers.d/${DEFAULT_USER} @@ -208,14 +203,16 @@ FROM final-php-fpm AS drupal-php-83 RUN sudo -s < bash' -# separator=" =": 'WM = dwm' -separator=":" - - -# Color Blocks - - -# Color block range -# The range of colors to print. -# -# Default: '0', '15' -# Values: 'num' -# Flag: --block_range -# -# Example: -# -# Display colors 0-7 in the blocks. (8 colors) -# neofetch --block_range 0 7 -# -# Display colors 0-15 in the blocks. (16 colors) -# neofetch --block_range 0 15 -block_range=(0 15) - -# Toggle color blocks -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --color_blocks -color_blocks="on" - -# Color block width in spaces -# -# Default: '3' -# Values: 'num' -# Flag: --block_width -block_width=3 - -# Color block height in lines -# -# Default: '1' -# Values: 'num' -# Flag: --block_height -block_height=1 - -# Color Alignment -# -# Default: 'auto' -# Values: 'auto', 'num' -# Flag: --col_offset -# -# Number specifies how far from the left side of the terminal (in spaces) to -# begin printing the columns, in case you want to e.g. center them under your -# text. -# Example: -# col_offset="auto" - Default behavior of neofetch -# col_offset=7 - Leave 7 spaces then print the colors -col_offset="auto" - -# Progress Bars - - -# Bar characters -# -# Default: '-', '=' -# Values: 'string', 'string' -# Flag: --bar_char -# -# Example: -# neofetch --bar_char 'elapsed' 'total' -# neofetch --bar_char '-' '=' -bar_char_elapsed="-" -bar_char_total="=" - -# Toggle Bar border -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --bar_border -bar_border="on" - -# Progress bar length in spaces -# Number of chars long to make the progress bars. -# -# Default: '15' -# Values: 'num' -# Flag: --bar_length -bar_length=15 - -# Progress bar colors -# When set to distro, uses your distro's logo colors. -# -# Default: 'distro', 'distro' -# Values: 'distro', 'num' -# Flag: --bar_colors -# -# Example: -# neofetch --bar_colors 3 4 -# neofetch --bar_colors distro 5 -bar_color_elapsed="distro" -bar_color_total="distro" - - -# Info display -# Display a bar with the info. -# -# Default: 'off' -# Values: 'bar', 'infobar', 'barinfo', 'off' -# Flags: --cpu_display -# --memory_display -# --battery_display -# --disk_display -# -# Example: -# bar: '[---=======]' -# infobar: 'info [---=======]' -# barinfo: '[---=======] info' -# off: 'info' -cpu_display="off" -memory_display="off" -battery_display="off" -disk_display="off" - - -# Backend Settings - - -# Image backend. -# -# Default: 'ascii' -# Values: 'ascii', 'caca', 'chafa', 'jp2a', 'iterm2', 'off', -# 'pot', 'termpix', 'pixterm', 'tycat', 'w3m', 'kitty' -# Flag: --backend -image_backend="ascii" - -# Image Source -# -# Which image or ascii file to display. -# -# Default: 'auto' -# Values: 'auto', 'ascii', 'wallpaper', '/path/to/img', '/path/to/ascii', '/path/to/dir/' -# 'command output (neofetch --ascii "$(fortune | cowsay -W 30)")' -# Flag: --source -# -# NOTE: 'auto' will pick the best image source for whatever image backend is used. -# In ascii mode, distro ascii art will be used and in an image mode, your -# wallpaper will be used. -image_source="auto" - - -# Ascii Options - - -# Ascii distro -# Which distro's ascii art to display. -# -# Default: 'auto' -# Values: 'auto', 'distro_name' -# Flag: --ascii_distro -# NOTE: AIX, Alpine, Anarchy, Android, Antergos, antiX, "AOSC OS", -# "AOSC OS/Retro", Apricity, ArcoLinux, ArchBox, ARCHlabs, -# ArchStrike, XFerience, ArchMerge, Arch, Artix, Arya, Bedrock, -# Bitrig, BlackArch, BLAG, BlankOn, BlueLight, bonsai, BSD, -# BunsenLabs, Calculate, Carbs, CentOS, Chakra, ChaletOS, -# Chapeau, Chrom*, Cleanjaro, ClearOS, Clear_Linux, Clover, -# Condres, Container_Linux, CRUX, Cucumber, Debian, Deepin, -# DesaOS, Devuan, DracOS, DarkOs, DragonFly, Drauger, Elementary, -# EndeavourOS, Endless, EuroLinux, Exherbo, Fedora, Feren, FreeBSD, -# FreeMiNT, Frugalware, Funtoo, GalliumOS, Garuda, Gentoo, Pentoo, -# gNewSense, GNOME, GNU, GoboLinux, Grombyang, Guix, Haiku, Huayra, -# Hyperbola, janus, Kali, KaOS, KDE_neon, Kibojoe, Kogaion, -# Korora, KSLinux, Kubuntu, LEDE, LFS, Linux_Lite, -# LMDE, Lubuntu, Lunar, macos, Mageia, MagpieOS, Mandriva, -# Manjaro, Maui, Mer, Minix, LinuxMint, MX_Linux, Namib, -# Neptune, NetBSD, Netrunner, Nitrux, NixOS, Nurunner, -# NuTyX, OBRevenge, OpenBSD, openEuler, OpenIndiana, openmamba, -# OpenMandriva, OpenStage, OpenWrt, osmc, Oracle, OS Elbrus, PacBSD, -# Parabola, Pardus, Parrot, Parsix, TrueOS, PCLinuxOS, Peppermint, -# popos, Porteus, PostMarketOS, Proxmox, Puppy, PureOS, Qubes, Radix, -# Raspbian, Reborn_OS, Redstar, Redcore, Redhat, Refracted_Devuan, -# Regata, Rosa, sabotage, Sabayon, Sailfish, SalentOS, Scientific, -# Septor, SereneLinux, SharkLinux, Siduction, Slackware, SliTaz, -# SmartOS, Solus, Source_Mage, Sparky, Star, SteamOS, SunOS, -# openSUSE_Leap, openSUSE_Tumbleweed, openSUSE, SwagArch, Tails, -# Trisquel, Ubuntu-Budgie, Ubuntu-GNOME, Ubuntu-MATE, Ubuntu-Studio, -# Ubuntu, Venom, Void, Obarun, windows10, Windows7, Xubuntu, Zorin, -# and IRIX have ascii logos -# NOTE: Arch, Ubuntu, Redhat, and Dragonfly have 'old' logo variants. -# Use '{distro name}_old' to use the old logos. -# NOTE: Ubuntu has flavor variants. -# Change this to Lubuntu, Kubuntu, Xubuntu, Ubuntu-GNOME, -# Ubuntu-Studio, Ubuntu-Mate or Ubuntu-Budgie to use the flavors. -# NOTE: Arcolinux, Dragonfly, Fedora, Alpine, Arch, Ubuntu, -# CRUX, Debian, Gentoo, FreeBSD, Mac, NixOS, OpenBSD, android, -# Antrix, CentOS, Cleanjaro, ElementaryOS, GUIX, Hyperbola, -# Manjaro, MXLinux, NetBSD, Parabola, POP_OS, PureOS, -# Slackware, SunOS, LinuxLite, OpenSUSE, Raspbian, -# postmarketOS, and Void have a smaller logo variant. -# Use '{distro name}_small' to use the small variants. -ascii_distro="auto" - -# Ascii Colors -# -# Default: 'distro' -# Values: 'distro', 'num' 'num' 'num' 'num' 'num' 'num' -# Flag: --ascii_colors -# -# Example: -# ascii_colors=(distro) - Ascii is colored based on Distro colors. -# ascii_colors=(4 6 1 8 8 6) - Ascii is colored using these colors. -ascii_colors=(distro) - -# Bold ascii logo -# Whether or not to bold the ascii logo. -# -# Default: 'on' -# Values: 'on', 'off' -# Flag: --ascii_bold -ascii_bold="on" - - -# Image Options - - -# Image loop -# Setting this to on will make neofetch redraw the image constantly until -# Ctrl+C is pressed. This fixes display issues in some terminal emulators. -# -# Default: 'off' -# Values: 'on', 'off' -# Flag: --loop -image_loop="off" - -# Thumbnail directory -# -# Default: '~/.cache/thumbnails/neofetch' -# Values: 'dir' -thumbnail_dir="${XDG_CACHE_HOME:-${HOME}/.cache}/thumbnails/neofetch" - -# Crop mode -# -# Default: 'normal' -# Values: 'normal', 'fit', 'fill' -# Flag: --crop_mode -# -# See this wiki page to learn about the fit and fill options. -# https://github.com/dylanaraps/neofetch/wiki/What-is-Waifu-Crop%3F -crop_mode="normal" - -# Crop offset -# Note: Only affects 'normal' crop mode. -# -# Default: 'center' -# Values: 'northwest', 'north', 'northeast', 'west', 'center' -# 'east', 'southwest', 'south', 'southeast' -# Flag: --crop_offset -crop_offset="center" - -# Image size -# The image is half the terminal width by default. -# -# Default: 'auto' -# Values: 'auto', '00px', '00%', 'none' -# Flags: --image_size -# --size -image_size="auto" - -# Gap between image and text -# -# Default: '3' -# Values: 'num', '-num' -# Flag: --gap -gap=3 - -# Image offsets -# Only works with the w3m backend. -# -# Default: '0' -# Values: 'px' -# Flags: --xoffset -# --yoffset -yoffset=0 -xoffset=0 - -# Image background color -# Only works with the w3m backend. -# -# Default: '' -# Values: 'color', 'blue' -# Flag: --bg_color -background_color= - - -# Misc Options - -# Stdout mode -# Turn off all colors and disables image backend (ASCII/Image). -# Useful for piping into another command. -# Default: 'off' -# Values: 'on', 'off' -stdout="off" diff --git a/php/rootfs/home/druid/ascii b/php/rootfs/home/druid/ascii index bf8da19..04aae6d 100644 --- a/php/rootfs/home/druid/ascii +++ b/php/rootfs/home/druid/ascii @@ -1,4 +1,3 @@ -${c1} , *** ,****** diff --git a/tests/app/.env b/tests/app/.env new file mode 100644 index 0000000..a0008bd --- /dev/null +++ b/tests/app/.env @@ -0,0 +1,4 @@ +COMPOSE_PROJECT_NAME=test-app +APP_HOSTNAME=app.docker.so +APP_IMAGE=ghcr.io/druidfi/app:php-8.4 +PHP_VERSION=8.4 diff --git a/tests/app/compose.yaml b/tests/app/compose.yaml new file mode 100644 index 0000000..eb16633 --- /dev/null +++ b/tests/app/compose.yaml @@ -0,0 +1,35 @@ +services: + + app: + container_name: ${COMPOSE_PROJECT_NAME}-app + hostname: ${COMPOSE_PROJECT_NAME} + image: ${APP_IMAGE} + build: + context: ../../app + dockerfile: Dockerfile + additional_contexts: + php-base: docker-image://php:${PHP_VERSION}-fpm-alpine + volumes: + - ../../tests/scripts:/app/scripts:ro + environment: + APP_ENV: dev + labels: + - traefik.enable=true + - traefik.http.routers.${COMPOSE_PROJECT_NAME}-app.entrypoints=https + - traefik.http.routers.${COMPOSE_PROJECT_NAME}-app.rule=Host(`${APP_HOSTNAME}`) + - traefik.http.routers.${COMPOSE_PROJECT_NAME}-app.tls=true + - traefik.http.services.${COMPOSE_PROJECT_NAME}-app.loadbalancer.server.port=8080 + - traefik.docker.network=stonehenge-network + networks: + - default + - stonehenge-network + depends_on: + - db + + db: + container_name: ${COMPOSE_PROJECT_NAME}-db + image: druidfi/mariadb:11.4-drupal-lts + +networks: + stonehenge-network: + external: true diff --git a/tests/scripts/db.sh b/tests/scripts/db.sh index 9048b54..6d044d9 100755 --- a/tests/scripts/db.sh +++ b/tests/scripts/db.sh @@ -1,3 +1,41 @@ #!/usr/bin/env bash -mysql --version +mariadb --version + +title "Test that mariadb-dump reads ssl-ca from [client] config" + +result=$(mariadb-dump --print-defaults 2>&1) + +if ! echo "$result" | grep -q "ssl-ca"; then + error "mariadb-dump is not picking up ssl-ca from [client] config" +fi + +if ! echo "$result" | grep -q "DigiCertGlobalRootCA"; then + error "mariadb-dump ssl-ca does not point to DigiCertGlobalRootCA.crt.pem" +fi + +title "Test that DigiCertGlobalRootCA.crt.pem exists" + +if [ ! -f /opt/ssl/DigiCertGlobalRootCA.crt.pem ]; then + error "DigiCertGlobalRootCA.crt.pem not found at /opt/ssl/" +fi + +title "Test that /etc/drush/drush.yml exists" + +if [ ! -f /etc/drush/drush.yml ]; then + error "/etc/drush/drush.yml not found" +fi + +title "Test that /etc/drush/drush.yml contains skip-ssl and structure-tables" + +if ! grep -q "skip-ssl" /etc/drush/drush.yml; then + error "/etc/drush/drush.yml does not contain skip-ssl" +fi + +if ! grep -q "no-tablespaces" /etc/drush/drush.yml; then + error "/etc/drush/drush.yml does not contain no-tablespaces" +fi + +if ! grep -q "structure-tables" /etc/drush/drush.yml; then + error "/etc/drush/drush.yml does not contain structure-tables" +fi diff --git a/tests/scripts/perms.sh b/tests/scripts/perms.sh index 7b3fe2a..4e22c1a 100644 --- a/tests/scripts/perms.sh +++ b/tests/scripts/perms.sh @@ -52,18 +52,26 @@ if [[ "$result" != "$expected" ]]; then error "Error! Folder permissions should be '$expected' instead of '$result'" fi -title "Test that user can operate folders inside folder owned by www-data" +title "Test that druid can unlink www-data-owned files in a www-data:www-data 775 folder" -# Challenge: /app/public/sites/default/files is owned by www-data and with permissions drwxr-xr-x -# Druid user cannot do operations which e.g. delete folders inside it. -# Permissions should be changed from drwxr-xr-x to drwxrwxr-x +# PHP-FPM runs as www-data with umask 002, so files/dirs in sites/default/files are +# www-data:www-data with permissions 664/775. Druid (member of www-data group) must be +# able to unlink these files so that drush deploy can clear aggregated CSS/JS caches. folder="$APP_PATH/folder-owned-by-www-data/" mkdir "$folder" || error "Cannot create a folder" -$sudo_bin chown www-data:www-data "$folder" || error "Cannot change owner of folder: $result" -$sudo_bin chmod g+rwx "$folder" || error "Cannot change permissions of folder: $result" -result=$(stat -c '%U:%G %A %a' "$folder") -title "Permissions: $folder : $result" -mkdir "$folder/somefolder" || error "Cannot create a folder inside folder" -ls -lahR "$folder" -rm -rf "$folder" || error "Cannot remove folder with permissions: $result" +$sudo_bin chown www-data:www-data "$folder" || error "Cannot chown folder to www-data" +$sudo_bin chmod 775 "$folder" || error "Cannot chmod folder to 775" +$sudo_bin touch "$folder/test.css.gz" || error "Cannot create test file" +$sudo_bin chown www-data:www-data "$folder/test.css.gz" || error "Cannot chown file to www-data" +$sudo_bin chmod 664 "$folder/test.css.gz" || error "Cannot chmod file to 664" + +result=$(stat -c '%U:%G %a' "$folder") +title "Folder: $folder : $result" + +result=$(stat -c '%U:%G %a' "$folder/test.css.gz") +title "File: $folder/test.css.gz : $result" + +rm "$folder/test.css.gz" || error "Cannot unlink www-data-owned file in www-data:www-data 775 folder" +mkdir "$folder/somefolder" || error "Cannot create subfolder" +rm -rf "$folder" || error "Cannot remove folder" diff --git a/tests/scripts/php.sh b/tests/scripts/php.sh index 9223e31..1f2dcd4 100755 --- a/tests/scripts/php.sh +++ b/tests/scripts/php.sh @@ -22,15 +22,17 @@ if [[ "$result" != "$expected" ]]; then error "Error! iconv result should be '$expected' instead of '$result'" fi +title "Test GD extension" + +php -r "exit(extension_loaded('gd') ? 0 : 1);" || error "GD extension is not loaded" + title "Test Imagick extension" -php -i | grep imagick +php -r "exit(extension_loaded('imagick') ? 0 : 1);" || error "Imagick extension is not loaded" title "Test Redis extension" -if ! php -r "exit(class_exists('Redis') ? 0 : 1);"; then - error "Class Redis does not exist" -fi +php -r "exit(class_exists('Redis') ? 0 : 1);" || error "Class Redis does not exist" title "Test Composer require" php_version=$(php -d error_reporting=22527 -d display_errors=1 -r 'echo phpversion();') diff --git a/tests/scripts/tests.sh b/tests/scripts/tests.sh index 155a76d..d78707a 100755 --- a/tests/scripts/tests.sh +++ b/tests/scripts/tests.sh @@ -2,7 +2,7 @@ sleep 1 -neofetch --ascii_colors 1 --ascii /app/scripts/ascii +fastfetch --config /home/druid/.config/fastfetch/config.jsonc . "$(dirname "$0")/utils.sh" . "$(dirname "$0")/perms.sh"