From d4e1103fc3d6b7c583073022bc052d0f983bc761 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sun, 5 Jul 2026 20:36:19 +0200 Subject: [PATCH] fix(web-app-joomla): serialize first-boot copy and install across replicas The stock image copies /usr/src/joomla into /var/www/html and runs the auto-installer on every boot. With more than one replica sharing the NFS volume they corrupt the tree and race the install. Add a custom entrypoint that takes an atomic mkdir lock on the shared volume so one leader runs the setup while the others block on a ready marker, wired through the same host-path build args as the odoo entrypoint. Verified locally: 2 concurrent replicas on a shared volume copy and install exactly once and both serve HTTP 200. Sessions are already stored in the database and caching is off, so the replicas stay stateless. Co-Authored-By: Claude Opus 4.8 (1M context) --- roles/web-app-joomla/files/Dockerfile | 18 ++++++++----- .../files/docker-entrypoint-joomla.sh | 27 +++++++++++++++++++ roles/web-app-joomla/tasks/01_config.yml | 10 +++++++ roles/web-app-joomla/templates/compose.yml.j2 | 2 ++ roles/web-app-joomla/vars/main.yml | 5 ++++ 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 roles/web-app-joomla/files/docker-entrypoint-joomla.sh diff --git a/roles/web-app-joomla/files/Dockerfile b/roles/web-app-joomla/files/Dockerfile index 47bec66059..5946350895 100644 --- a/roles/web-app-joomla/files/Dockerfile +++ b/roles/web-app-joomla/files/Dockerfile @@ -1,15 +1,21 @@ ARG JOOMLA_IMAGE ARG JOOMLA_VERSION +ARG JOOMLA_ENTRYPOINT_SCRIPT_HOST_REL +ARG JOOMLA_ENTRYPOINT_SCRIPT_DOCKER FROM ${JOOMLA_IMAGE}:${JOOMLA_VERSION} +ARG JOOMLA_ENTRYPOINT_SCRIPT_HOST_REL +ARG JOOMLA_ENTRYPOINT_SCRIPT_DOCKER -# Composer + tooling for the runtime build of the plg_system_keycloak -# OIDC plugin (requirement 013). The plugin's PHP dependencies -# (jumbojett/openid-connect-php) are vendored at deploy time inside -# the running container via `composer install`, so the host never -# needs PHP/Composer. `unzip` is required by Joomla's -# `extension:install --path=` CLI. +# composer + unzip are needed to vendor and package the OIDC plugin inside the running container RUN rm -rf /var/lib/apt/lists/* \ && apt-get update \ && apt-get install -y --no-install-recommends git unzip curl ca-certificates \ && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \ && rm -rf /var/lib/apt/lists/* + +COPY ${JOOMLA_ENTRYPOINT_SCRIPT_HOST_REL} ${JOOMLA_ENTRYPOINT_SCRIPT_DOCKER} +RUN chmod +x "${JOOMLA_ENTRYPOINT_SCRIPT_DOCKER}" \ + && ln -sf "${JOOMLA_ENTRYPOINT_SCRIPT_DOCKER}" /usr/local/bin/entrypoint-joomla + +ENTRYPOINT ["/usr/local/bin/entrypoint-joomla"] +CMD ["apache2-foreground"] diff --git a/roles/web-app-joomla/files/docker-entrypoint-joomla.sh b/roles/web-app-joomla/files/docker-entrypoint-joomla.sh new file mode 100644 index 0000000000..e3952439ed --- /dev/null +++ b/roles/web-app-joomla/files/docker-entrypoint-joomla.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Serialize the stock image's first-boot copy+auto-install across replicas that +# share the /var/www/html NFS volume (mkdir is atomic on NFS: one leader runs it, +# the rest block on the ready marker). The setup only runs when $1 is apache2*/ +# php-fpm, so the leader must drive it with `apache2 -v` (returns after setup) — +# a plain `true` skips the whole setup block and copies nothing. + +target="/var/www/html" +lock="${target}/.infinito-init.lock" +ready="${target}/.infinito-init.ready" + +if [ ! -e "${ready}" ] && [ ! -e "${target}/configuration.php" ]; then + if mkdir "${lock}" 2>/dev/null; then + /entrypoint.sh apache2 -v || true + if [ -e "${target}/configuration.php" ]; then + touch "${ready}" + fi + else + tries=0 + while [ ! -e "${ready}" ] && [ ! -e "${target}/configuration.php" ] && [ "${tries}" -lt 300 ]; do + sleep 2 + tries=$((tries + 1)) + done + fi +fi + +exec /entrypoint.sh "$@" diff --git a/roles/web-app-joomla/tasks/01_config.yml b/roles/web-app-joomla/tasks/01_config.yml index de9a289c9f..4a66b266c5 100644 --- a/roles/web-app-joomla/tasks/01_config.yml +++ b/roles/web-app-joomla/tasks/01_config.yml @@ -4,3 +4,13 @@ src: php-upload.ini.j2 dest: "{{ JOOMLA_UPLOAD_CONFIG }}" mode: "0644" + +- name: "📋 Copy Joomla entrypoint (first-boot copy serializer)" + copy: + src: "{{ JOOMLA_ENTRYPOINT_SCRIPT_FILE }}" + dest: "{{ JOOMLA_ENTRYPOINT_SCRIPT_HOST_ABS }}" + mode: "0755" + notify: + - compose build + - compose up + when: IS_STACK_HOST | bool diff --git a/roles/web-app-joomla/templates/compose.yml.j2 b/roles/web-app-joomla/templates/compose.yml.j2 index 5eb2557b8a..8fc7f30cf3 100644 --- a/roles/web-app-joomla/templates/compose.yml.j2 +++ b/roles/web-app-joomla/templates/compose.yml.j2 @@ -6,6 +6,8 @@ args: JOOMLA_IMAGE: "{{ JOOMLA_IMAGE }}" JOOMLA_VERSION: "{{ JOOMLA_VERSION }}" + JOOMLA_ENTRYPOINT_SCRIPT_HOST_REL: "{{ JOOMLA_ENTRYPOINT_SCRIPT_HOST_REL }}" + JOOMLA_ENTRYPOINT_SCRIPT_DOCKER: "{{ JOOMLA_ENTRYPOINT_SCRIPT_DOCKER }}" {{ lookup('container_image', application_id, 'joomla', custom=True) }} {{ lookup('compose_only', 'container_name', JOOMLA_CONTAINER) }} {% include 'roles/sys-svc-container/templates/base.yml.j2' %} diff --git a/roles/web-app-joomla/vars/main.yml b/roles/web-app-joomla/vars/main.yml index dd43f959fa..3f42158d91 100644 --- a/roles/web-app-joomla/vars/main.yml +++ b/roles/web-app-joomla/vars/main.yml @@ -14,6 +14,11 @@ JOOMLA_CONFIG_FILE: "/var/www/html/configuration.php" JOOMLA_INSTALLER_CLI_FILE: "/var/www/html/installation/joomla.php" JOOMLA_CLI_FILE: "/var/www/html/cli/joomla.php" +JOOMLA_ENTRYPOINT_SCRIPT_FILE: "docker-entrypoint-joomla.sh" +JOOMLA_ENTRYPOINT_SCRIPT_HOST_ABS: "{{ [ lookup('container', application_id, 'directories.volumes'), JOOMLA_ENTRYPOINT_SCRIPT_FILE ] | path_join }}" +JOOMLA_ENTRYPOINT_SCRIPT_HOST_REL: "volumes/{{ JOOMLA_ENTRYPOINT_SCRIPT_FILE }}" +JOOMLA_ENTRYPOINT_SCRIPT_DOCKER: "{{ [ '/usr/local/bin/', JOOMLA_ENTRYPOINT_SCRIPT_FILE ] | path_join }}" + JOOMLA_UPLOAD_CONFIG: "{{ [ lookup('container', application_id, 'directories.instance'), 'php-upload.ini' ] | path_join }}" JOOMLA_UPLOAD_SIZE: "{{ lookup('config', application_id, 'services.joomla.upload.size_mb') }}" JOOMLA_UPLOAD_MAX_FILESIZE: "{{ (JOOMLA_UPLOAD_SIZE | int) }}M"