diff --git a/.env b/.env index fd05b43d6f..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,127.0.0.2 +TRUSTED_PROXIES=REMOTE_ADDR #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..9828c03935 --- /dev/null +++ b/.github/workflows/build-image-beta.yml @@ -0,0 +1,267 @@ +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=sha,prefix=sha-,suffix=-web + 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 + version: v0.74.0 + + 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 }}", + "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 }}" + } 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..779bf51c31 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 update && 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 \ @@ -79,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; \ { \ @@ -99,6 +103,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/ @@ -112,7 +118,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 +135,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 ./ @@ -140,6 +148,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 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/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 8af09dc1e2..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; @@ -29,6 +52,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 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 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