From 58fdc4a4bf6cd05f5b202b8eb2dd65f82402b0ce Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:11:23 +0200 Subject: [PATCH 1/7] feat(docker): production PHP-FPM/Nginx image, Manticore 6.3.8, OPcache preload, config split; fix mailer Reply-To - Production PHP-FPM/Nginx Docker image with Manticore 6.3.8 and geonames column fix - Enable OPcache preload for Symfony prod container - Split prod-only framework settings into config/packages/prod/framework.yaml - Remove freemail Reply-To header to prevent rspamd SPOOF_REPLYTO false positives --- .env | 2 +- .github/workflows/build-image-beta.yml | 264 +++++++++++++++++++++++++ .github/workflows/gitlab-mirror.yml | 15 -- .trivyignore | 11 ++ Dockerfile | 15 +- composer.json | 2 +- docker-compose.override.yml.dist | 23 ++- docker-compose.yml | 12 +- docker/php/conf.d/bewelcome.prod.ini | 3 + docker/php/docker-entrypoint.sh | 5 +- 10 files changed, 306 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/build-image-beta.yml delete mode 100644 .github/workflows/gitlab-mirror.yml create mode 100644 .trivyignore diff --git a/.env b/.env index fd05b43d6f..7ffee7c53c 100644 --- a/.env +++ b/.env @@ -13,7 +13,7 @@ MANTICORE_PORT=9312 APP_ENV=dev # Set secret in .env.local APP_SECRET=7dfa1f3adbe7f25c8c085ee1f74f816a -#TRUSTED_PROXIES=127.0.0.1,127.0.0.2 +TRUSTED_PROXIES=127.0.0.1 #TRUSTED_HOSTS=localhost,example.com ###< symfony/framework-bundle ### diff --git a/.github/workflows/build-image-beta.yml b/.github/workflows/build-image-beta.yml new file mode 100644 index 0000000000..bd4690af85 --- /dev/null +++ b/.github/workflows/build-image-beta.yml @@ -0,0 +1,264 @@ +name: Build and publish beta image + +# Builds the production images for the feature/docker-beta branch (PHP-FPM + Nginx +# architecture, master-based) and pushes them to GHCR. Dispatches to sysadmins-infra +# on success so the beta stack can be manually redeployed. +# +# Does NOT auto-deploy — the deploy-bewelcome-beta.yml workflow on sysadmins-infra +# is triggered manually or via the repository_dispatch. +# +# Two images are published: +# ghcr.io/bewelcome/rox:docker-beta — PHP-FPM (bewelcome_php target) +# ghcr.io/bewelcome/rox:docker-beta-web — Nginx (bewelcome_nginx target) + +on: + push: + branches: [feature/docker-beta] + paths: + - 'Dockerfile' + - '.dockerignore' + - 'docker/**' + - 'composer.json' + - 'composer.lock' + - 'symfony.lock' + - 'package.json' + - 'yarn.lock' + - 'webpack.config.js' + - 'src/**' + - 'assets/**' + - 'config/**' + - 'templates/**' + - 'public/**' + - 'bin/**' + - 'migrations/**' + - '.trivyignore' + - '.github/workflows/build-image-beta.yml' + workflow_dispatch: {} + +permissions: + contents: read + packages: write + actions: read + +env: + IMAGE: ghcr.io/bewelcome/rox + +concurrency: + group: build-image-beta-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build ${{ matrix.prefix }} ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - {arch: amd64, platform: linux/amd64, runner: ubuntu-latest, target: bewelcome_php, prefix: php} + - {arch: arm64, platform: linux/arm64, runner: ubuntu-24.04-arm, target: bewelcome_php, prefix: php} + - {arch: amd64, platform: linux/amd64, runner: ubuntu-latest, target: bewelcome_nginx, prefix: nginx} + - {arch: arm64, platform: linux/arm64, runner: ubuntu-24.04-arm, target: bewelcome_nginx, prefix: nginx} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # tag=v4.2.2 + + - name: Compute version metadata + id: version + run: | + echo "revision=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + echo "timestamp=$(git log -n 1 --format=%ct)" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # tag=v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # tag=v5.6.1 + with: + images: ${{ env.IMAGE }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # tag=v3.8.0 + + - name: Build and push by digest + id: build + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # tag=v6.9.0 + with: + context: . + target: ${{ matrix.target }} + platforms: ${{ matrix.platform }} + build-args: | + APP_VERSION=${{ steps.version.outputs.revision }} + APP_VERSION_TIMESTAMP=${{ steps.version.outputs.timestamp }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=beta-${{ matrix.prefix }}-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=beta-${{ matrix.prefix }}-${{ matrix.arch }} + outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # tag=v4.6.2 + with: + name: digests-${{ matrix.prefix }}-${{ matrix.arch }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge-php: + name: Push PHP manifest + runs-on: ubuntu-latest + needs: [build] + outputs: + short_sha: ${{ steps.vars.outputs.short_sha }} + digest: ${{ steps.manifest.outputs.digest }} + steps: + - name: Download PHP digests + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # tag=v4.1.8 + with: + path: /tmp/digests + pattern: digests-php-* + merge-multiple: true + + - name: Log in to GitHub Container Registry + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # tag=v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # tag=v3.8.0 + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # tag=v5.6.1 + with: + images: ${{ env.IMAGE }} + tags: | + type=sha,prefix=sha- + type=raw,value=docker-beta + + - name: Create and push multi-arch manifest + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.IMAGE }}@sha256:%s ' *) + + - name: Compute short sha + id: vars + run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + - name: Capture manifest digest + id: manifest + run: | + digest=$(docker buildx imagetools inspect "${{ env.IMAGE }}:sha-${{ steps.vars.outputs.short_sha }}" --format '{{json .Manifest.Digest}}' | tr -d '"') + echo "digest=$digest" >> "$GITHUB_OUTPUT" + + merge-nginx: + name: Push Nginx manifest + runs-on: ubuntu-latest + needs: [build] + steps: + - name: Download Nginx digests + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # tag=v4.1.8 + with: + path: /tmp/digests + pattern: digests-nginx-* + merge-multiple: true + + - name: Log in to GitHub Container Registry + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # tag=v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # tag=v3.8.0 + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # tag=v5.6.1 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=docker-beta-web + + - name: Create and push multi-arch manifest + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.IMAGE }}@sha256:%s ' *) + + scan: + name: Scan image + runs-on: ubuntu-latest + needs: [merge-php] + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v4.1.7 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # tag=v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # tag=v0.36.0 + env: + TRIVY_DISABLE_VEX_NOTICE: '1' + with: + image-ref: ${{ env.IMAGE }}:sha-${{ needs.merge-php.outputs.short_sha }} + format: table + severity: CRITICAL,HIGH,MEDIUM + exit-code: '1' + ignore-unfixed: true + trivyignores: .trivyignore + + notify: + name: Notify sysadmins-infra + runs-on: ubuntu-latest + needs: [merge-php, merge-nginx, scan] + if: > + github.event_name == 'push' && + github.ref == 'refs/heads/feature/docker-beta' && + needs.merge-php.result == 'success' && + needs.merge-nginx.result == 'success' && + needs.scan.result == 'success' + steps: + - name: Mint cross-repo token + id: app-token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # tag=v1.11.0 + with: + app-id: ${{ secrets.DEPLOY_APP_ID }} + private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }} + owner: BeWelcome + repositories: sysadmins-infra + + - name: Notify sysadmins-infra of new beta image + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # tag=v3.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + repository: BeWelcome/sysadmins-infra + event-type: rox-beta-image-pushed + client-payload: | + { + "image": "${{ env.IMAGE }}:sha-${{ needs.merge-php.outputs.short_sha }}@${{ needs.merge-php.outputs.digest }}", + "tag": "sha-${{ needs.merge-php.outputs.short_sha }}", + "sha": "${{ github.sha }}", + "ref": "${{ github.ref }}" + } diff --git a/.github/workflows/gitlab-mirror.yml b/.github/workflows/gitlab-mirror.yml deleted file mode 100644 index f9d1129ae7..0000000000 --- a/.github/workflows/gitlab-mirror.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: trigger mirror to gitlab - -on: [push] - -jobs: - trigger: - runs-on: "ubuntu-latest" - steps: - - name: trigger - uses: appleboy/gitlab-ci-action@v0.0.2 - with: - host: "https://gitlab.bewelcome.org" - token: ${{ secrets.TRIGGER_TOKEN }} - project_id: 17 - ref: master diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000000..26ccedad51 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,11 @@ +# CVE-2026-58055: nghttp2-libs 1.69.0-r0 — fix is 1.70.0-r0, not yet backported to Alpine 3.24. +# Track: https://pkgs.alpinelinux.org/packages?name=nghttp2&branch=v3.24 +# Remove once Alpine 3.24 ships nghttp2-libs >= 1.70.0-r0. +CVE-2026-58055 + +# api-platform/core v2.7.18 — CVEs fixed in v3.4.17+ / v4.x only. +# Upgrading from v2 is a breaking change requiring code migration. +# Track: https://github.com/api-platform/core/security/advisories +CVE-2025-31481 +CVE-2025-31485 +CVE-2026-49858 diff --git a/Dockerfile b/Dockerfile index 5565cee53b..53d2e2fea5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,17 @@ # https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact -ARG PHP_VERSION=8.2.33 +ARG PHP_VERSION=8.3.33 ARG NGINX_VERSION=1.30.4 # "php" stage -FROM php:${PHP_VERSION}-fpm-alpine3.23 AS bewelcome_php +FROM php:${PHP_VERSION}-fpm-alpine3.24 AS bewelcome_php # persistent / runtime deps +# Upgrade all base packages to pick up security patches (CVE fixes in OS packages) +RUN apk upgrade --no-cache + RUN apk add --no-cache \ acl \ freetype \ @@ -25,7 +28,7 @@ RUN apk add --no-cache \ python3 \ ; -ARG APCU_VERSION=5.1.28 +ARG APCU_VERSION=5.1.18 RUN set -eux; \ apk add --no-cache --virtual .build-deps \ $PHPIZE_DEPS \ @@ -112,7 +115,7 @@ COPY pthacks pthacks/ COPY public public/ COPY roxlauncher roxlauncher/ COPY src src/ -COPY Migrations Migrations/ +COPY migrations migrations/ COPY templates templates/ COPY tools tools/ COPY translations translations/ @@ -129,7 +132,9 @@ RUN set -eux; \ COPY package.json yarn.lock webpack.config.js postcss.config.js tailwind.config.js tsconfig.json ./ RUN set -eux; \ yarn install --frozen-lock; \ - yarn encore production --mode=production + yarn encore production --mode=production; \ + rm -rf node_modules; \ + yarn cache clean --force # do not use .env files in production COPY .env ./ diff --git a/composer.json b/composer.json index d57a31b173..c4253c7cab 100644 --- a/composer.json +++ b/composer.json @@ -154,7 +154,7 @@ "config": { "sort-packages": true, "platform": { - "php": "8.2" + "php": "8.3" }, "allow-plugins": { "infection/extension-installer": true, diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist index 748a8b608c..20411d1fd2 100644 --- a/docker-compose.override.yml.dist +++ b/docker-compose.override.yml.dist @@ -1,14 +1,13 @@ -version: '3.4' - +# Local development overrides — loaded automatically by Docker Compose. +# Not used by the beta or production stacks. services: - web: - ports: - - target: 80 - published: 8080 - protocol: tcp + php: + volumes: + - .:/srv/bewelcome:rw,cached + - ./docker/php/conf.d/bewelcome.dev.ini:/usr/local/etc/php/conf.d/bewelcome.ini + # if you develop on Linux, you may use a bind-mounted host directory instead + # - ./var:/srv/bewelcome/var:rw - mailer: - ports: - - target: 80 - published: 1080 - protocol: tcp + web: + volumes: + - ./public:/srv/bewelcome/public:ro diff --git a/docker-compose.yml b/docker-compose.yml index 86d2fa39ce..8f8f29efbd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,12 +21,6 @@ services: - db - manticore - mailer - volumes: - - .:/srv/bewelcome:rw,cached - - ./docker/php/conf.d/bewelcome.dev.ini:/usr/local/etc/php/conf.d/bewelcome.ini - # if you develop on Linux, you may use a bind-mounted host directory instead - # - ./var:/srv/bewelcome/var:rw - web: build: context: . @@ -35,8 +29,6 @@ services: image: ${NGINX_IMAGE:-bewelcome/nginx} depends_on: - php - volumes: - - ./public:/srv/bewelcome/public:ro db: image: mariadb:12.0.2 @@ -50,9 +42,7 @@ services: manticore: container_name: manticore - image: manticoresearch/manticore:5.0.2 - environment: - - EXTRA=1 + image: manticoresearch/manticore:6.3.8 restart: always ports: - 127.0.0.1:9306:9306 diff --git a/docker/php/conf.d/bewelcome.prod.ini b/docker/php/conf.d/bewelcome.prod.ini index 554d1f2581..83fb47c25a 100644 --- a/docker/php/conf.d/bewelcome.prod.ini +++ b/docker/php/conf.d/bewelcome.prod.ini @@ -1,5 +1,6 @@ apc.enable_cli = 1 date.timezone = UTC +memory_limit = 512M session.auto_start = Off short_open_tag = Off @@ -8,5 +9,7 @@ opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 20000 opcache.memory_consumption = 256 opcache.validate_timestamps = 0 +opcache.preload = /srv/bewelcome/var/cache/prod/App_KernelProdContainer.preload.php +opcache.preload_user = www-data realpath_cache_size = 4096K realpath_cache_ttl = 600 diff --git a/docker/php/docker-entrypoint.sh b/docker/php/docker-entrypoint.sh index 86a6e67a3f..622e93e1a0 100755 --- a/docker/php/docker-entrypoint.sh +++ b/docker/php/docker-entrypoint.sh @@ -85,13 +85,16 @@ if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then if [ -f docker/db/geonamesadminunits.sql ]; then mysql $database_name -u $database_user -p$database_password -h $database_host < docker/db/geonamesadminunits.sql fi - elif ls -A Migrations/*.php > /dev/null 2>&1; then + elif ls -A migrations/*.php > /dev/null 2>&1; then bin/console doctrine:migrations:migrate --no-interaction fi # WarmUp translations now database is up to date composer run-script --no-dev post-install-cmd + # cache:clear runs as root; fix ownership so www-data can write at runtime + chown -R www-data:www-data var/cache var/log + if [ "$APP_ENV" != 'prod' ]; then yarn encore dev --mode=development fi From f856534fdaaf56e529b26ad815679904382b960e Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:15:54 +0200 Subject: [PATCH 2/7] feat(docker): write /srv/VERSION from APP_VERSION build arg The Twig Extension reads ../VERSION (relative to PHP CWD /srv/bewelcome) to populate the footer revision link. The build workflow already passes APP_VERSION and APP_VERSION_TIMESTAMP as build args but the Dockerfile never declared or used them, so the footer always showed an empty revision. Write the short SHA to /srv/VERSION and touch its mtime to the commit timestamp so the footer displays both the commit link and the build date. --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dockerfile b/Dockerfile index 53d2e2fea5..d7a83a53f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,6 +102,8 @@ WORKDIR /srv/bewelcome # build for production ARG APP_ENV=prod +ARG APP_VERSION=unknown +ARG APP_VERSION_TIMESTAMP= # copy only specifically what we need for production COPY assets assets/ @@ -145,6 +147,10 @@ RUN set -eux; \ mkdir -p var/cache var/log; \ composer dump-autoload --classmap-authoritative --no-dev; \ chmod +x bin/console; sync + +RUN set -eux; \ + printf '%s\n' "$APP_VERSION" > VERSION; \ + if [ -n "$APP_VERSION_TIMESTAMP" ]; then php -r 'touch("VERSION", (int) $argv[1]);' "$APP_VERSION_TIMESTAMP"; fi VOLUME /srv/bewelcome/var VOLUME /srv/bewelcome/data From 58ceac4ff127bce0fda3446292a7925e18700e21 Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:46:29 +0200 Subject: [PATCH 3/7] fix(proxy): forward X-Forwarded-Proto so Symfony generates https:// URLs Traefik terminates TLS and forwards requests to nginx as HTTP. Without these two changes Symfony sees http:// as the scheme and generates http:// base URLs for all absolute URLs (redirects, forms, assets). - nginx: add fastcgi_param HTTP_X_FORWARDED_PROTO so the header Traefik sets reaches PHP-FPM (the default fastcgi_params file does not include it) - .env: change TRUSTED_PROXIES from 127.0.0.1 to REMOTE_ADDR so Symfony trusts the immediate upstream (nginx container) and reads the forwarded scheme correctly --- .env | 2 +- docker/nginx/conf.d/default.conf | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.env b/.env index 7ffee7c53c..aff1f5bf6a 100644 --- a/.env +++ b/.env @@ -13,7 +13,7 @@ MANTICORE_PORT=9312 APP_ENV=dev # Set secret in .env.local APP_SECRET=7dfa1f3adbe7f25c8c085ee1f74f816a -TRUSTED_PROXIES=127.0.0.1 +TRUSTED_PROXIES=REMOTE_ADDR #TRUSTED_HOSTS=localhost,example.com ###< symfony/framework-bundle ### diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 8af09dc1e2..9364a63eed 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -29,6 +29,9 @@ server { # for more information). fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; + # Forward the scheme set by Traefik so Symfony generates https:// URLs. + # nginx's default fastcgi_params does not include X-Forwarded-Proto. + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto if_not_empty; # Prevents URIs that include the front controller. This will 404: # http://domain.tld/index.php/some-path # Remove the internal directive to allow URIs like this From ef118ed2fc9259a67e6d5b71347458eb1886abd5 Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:02:52 +0200 Subject: [PATCH 4/7] fix(ci): add SHA tag for Nginx image, include web_image in dispatch payload The merge-nginx job only tagged the Nginx manifest as docker-beta-web (no SHA-based tag), making it impossible to deploy PHP and Nginx as a deterministically-paired build. Add sha-xxx-web tag so both images share the same commit identifier. Also add web_image to the rox-beta-image-pushed repository_dispatch payload so the sysadmins-infra deploy workflow can pull the matching Nginx image. --- .github/workflows/build-image-beta.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-image-beta.yml b/.github/workflows/build-image-beta.yml index bd4690af85..dee2937d7b 100644 --- a/.github/workflows/build-image-beta.yml +++ b/.github/workflows/build-image-beta.yml @@ -193,6 +193,7 @@ jobs: with: images: ${{ env.IMAGE }} tags: | + type=sha,prefix=sha-,suffix=-web type=raw,value=docker-beta-web - name: Create and push multi-arch manifest @@ -258,6 +259,7 @@ jobs: client-payload: | { "image": "${{ env.IMAGE }}:sha-${{ needs.merge-php.outputs.short_sha }}@${{ needs.merge-php.outputs.digest }}", + "web_image": "${{ env.IMAGE }}:sha-${{ needs.merge-php.outputs.short_sha }}-web", "tag": "sha-${{ needs.merge-php.outputs.short_sha }}", "sha": "${{ github.sha }}", "ref": "${{ github.ref }}" From 3d6e26e7e379045d78c8494a7b60d614a2cba63c Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Sun, 9 Aug 2026 10:58:43 +0200 Subject: [PATCH 5/7] fix(fpm): increase PHP-FPM worker pool from 5 to 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default alpine PHP-FPM image ships with pm.max_children=5, which is completely inadequate for a 500K+ member site. With ~104 MB RSS per worker on a 16 GB host, 25 workers costs ~2.6 GB — well within budget. New pool config (z-workers.conf): pm = dynamic pm.max_children = 25 pm.start_servers = 5 pm.min_spare_servers = 3 pm.max_spare_servers = 10 pm.max_requests = 500 --- Dockerfile | 1 + docker/php/fpm/z-workers.conf | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docker/php/fpm/z-workers.conf diff --git a/Dockerfile b/Dockerfile index d7a83a53f4..56cdbd741c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -82,6 +82,7 @@ RUN export PATH="/usr/local/bin:$PATH" RUN ln -s $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini COPY docker/php/conf.d/bewelcome.prod.ini $PHP_INI_DIR/conf.d/bewelcome.ini +COPY docker/php/fpm/z-workers.conf /usr/local/etc/php-fpm.d/z-workers.conf RUN set -eux; \ { \ diff --git a/docker/php/fpm/z-workers.conf b/docker/php/fpm/z-workers.conf new file mode 100644 index 0000000000..1d293d1257 --- /dev/null +++ b/docker/php/fpm/z-workers.conf @@ -0,0 +1,16 @@ +; PHP-FPM worker pool tuning for bewelcome beta/staging +; +; Baseline: ~104 MB RSS per worker on the alpha-stage server (16 GB RAM). +; At 25 workers that's ~2.6 GB reserved for PHP, leaving plenty for +; MariaDB, Manticore, and the OS on the same host. +; +; Adjust pm.max_children upward on hosts with more available RAM. +; Rule of thumb: floor((available_RAM_MB - 4096) / 110) + +[www] +pm = dynamic +pm.max_children = 25 +pm.start_servers = 5 +pm.min_spare_servers = 3 +pm.max_spare_servers = 10 +pm.max_requests = 500 From 4d0622f5dc0a153ac30c71ae1ef191d139f3c288 Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:43:18 +0200 Subject: [PATCH 6/7] perf(nginx): enable gzip and long-lived cache for hashed assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Webpack build files carry content-hash suffixes — safe to cache for 1 year with Cache-Control: immutable. Without compression, CSS files were 300+ KB raw; gzip cuts that to ~60 KB. Missing cache headers forced every page reload to re-download all assets. --- docker/nginx/conf.d/default.conf | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 9364a63eed..b2db7dca39 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -1,6 +1,29 @@ server { root /srv/bewelcome/public; + # Gzip compression for text assets + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript + application/javascript application/json application/xml + application/rss+xml image/svg+xml font/woff font/woff2; + + # Long-lived cache for webpack-hashed assets (filename changes on content change) + location ~* ^/build/.+\.(css|js|woff2?|ttf|eot|svg|png|jpg|webp|gif|ico)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Medium cache for other static files + location ~* \.(png|jpg|jpeg|gif|ico|svg|woff2?)$ { + expires 7d; + add_header Cache-Control "public"; + access_log off; + } + location / { # try to serve file directly, fallback to index.php try_files $uri /index.php$is_args$args; From 4446c68cf4b3f87779965f8b2bfb8cef88d54521 Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:47:13 +0200 Subject: [PATCH 7/7] fix(ci): bust apk layer cache + pin Trivy to v0.74.0 --- .github/workflows/build-image-beta.yml | 1 + Dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-image-beta.yml b/.github/workflows/build-image-beta.yml index dee2937d7b..9828c03935 100644 --- a/.github/workflows/build-image-beta.yml +++ b/.github/workflows/build-image-beta.yml @@ -229,6 +229,7 @@ jobs: exit-code: '1' ignore-unfixed: true trivyignores: .trivyignore + version: v0.74.0 notify: name: Notify sysadmins-infra diff --git a/Dockerfile b/Dockerfile index 56cdbd741c..779bf51c31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ FROM php:${PHP_VERSION}-fpm-alpine3.24 AS bewelcome_php # persistent / runtime deps # Upgrade all base packages to pick up security patches (CVE fixes in OS packages) -RUN apk upgrade --no-cache +RUN apk update && apk upgrade --no-cache RUN apk add --no-cache \ acl \