diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml
new file mode 100644
index 0000000..e1374ec
--- /dev/null
+++ b/.github/workflows/build-images.yml
@@ -0,0 +1,176 @@
+name: Build and push images
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - '*/*/Dockerfile'
+ - '*/*/variants.yaml'
+ - '*/*/rootfs/**'
+ - '*/*/.scripts/**'
+ - '*/*/otel/**'
+ - '*/latest'
+ workflow_dispatch:
+ inputs:
+ target:
+ description: 'Image/version to build (e.g. php-hyperf/8.3 or php-hyperf/latest).'
+ required: true
+
+permissions:
+ contents: read
+
+jobs:
+ detect:
+ runs-on: ubuntu-latest
+ outputs:
+ builds: ${{ steps.list.outputs.builds }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Get changed files
+ id: changed
+ if: github.event_name == 'push'
+ uses: tj-actions/changed-files@v44
+
+ - name: Build matrix
+ id: list
+ shell: bash
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ INPUT_TARGET: ${{ inputs.target }}
+ CHANGED_FILES: ${{ steps.changed.outputs.all_changed_files }}
+ run: |
+ set -euo pipefail
+
+ declare -a versions=()
+
+ collect_version() {
+ local v="$1"
+ [ -z "$v" ] && return
+ case "$v" in
+ [A-Za-z0-9_./-]*) ;;
+ *) echo "::warning::ignoring suspicious path '$v'"; return ;;
+ esac
+ local depth
+ depth=$(awk -F/ '{print NF}' <<< "$v")
+ if [ "$depth" != "2" ]; then return; fi
+ local resolved="$v"
+ if [ -L "$v" ]; then
+ local link_target
+ link_target=$(readlink "$v")
+ resolved="${v%/*}/${link_target}"
+ fi
+ if [ ! -f "${resolved}/Dockerfile" ]; then return; fi
+ for existing in "${versions[@]+"${versions[@]}"}"; do
+ if [ "$existing" = "$v" ]; then return; fi
+ done
+ versions+=("$v")
+ }
+
+ if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
+ collect_version "$INPUT_TARGET"
+ else
+ declare -a changed_dirs=()
+ for f in $CHANGED_FILES; do
+ dir=$(awk -F/ 'NF>=2 {print $1"/"$2}' <<< "$f")
+ [ -z "$dir" ] && continue
+ for existing in "${changed_dirs[@]+"${changed_dirs[@]}"}"; do
+ if [ "$existing" = "$dir" ]; then dir=""; break; fi
+ done
+ [ -n "$dir" ] && changed_dirs+=("$dir")
+ done
+
+ for v in "${changed_dirs[@]+"${changed_dirs[@]}"}"; do
+ collect_version "$v"
+ done
+
+ for symlink in */latest; do
+ [ -L "$symlink" ] || continue
+ target=$(readlink "$symlink")
+ parent="${symlink%/*}"
+ resolved="${parent}/${target}"
+ for changed in "${changed_dirs[@]+"${changed_dirs[@]}"}"; do
+ if [ "$changed" = "$resolved" ]; then
+ collect_version "$symlink"
+ break
+ fi
+ done
+ done
+ fi
+
+ declare -a builds=()
+ for v in "${versions[@]+"${versions[@]}"}"; do
+ image="${v%%/*}"
+ version="${v##*/}"
+ ctx="$v"
+ if [ -L "$ctx" ]; then
+ link_target=$(readlink "$ctx")
+ ctx="${ctx%/*}/${link_target}"
+ fi
+
+ manifest="${ctx}/variants.yaml"
+ if [ -f "$manifest" ]; then
+ if ! yq eval 'all_c(.target != null and .target != "" and has("suffix"))' "$manifest" | grep -qx true; then
+ echo "::error file=${manifest}::each entry must declare 'target' (non-empty) and 'suffix' (key required, value may be empty)"
+ exit 1
+ fi
+ entries=$(yq eval -o=json "$manifest")
+ else
+ entries='[{"target":"'"$image"'","suffix":""}]'
+ fi
+
+ while IFS= read -r line; do
+ [ -z "$line" ] && continue
+ builds+=("$line")
+ done < <(jq -c --arg img "$image" --arg ver "$version" --arg ctx "$ctx" '
+ .[] | {
+ image: $img,
+ tag: ($ver + (if (.suffix // "") == "" then "" else "-" + .suffix end)),
+ context: $ctx,
+ target: .target,
+ build_args: ((.args // {}) | to_entries | map(.key + "=" + (.value | tostring)) | join("\n"))
+ }
+ ' <<< "$entries")
+ done
+
+ if [ ${#builds[@]} -eq 0 ]; then
+ echo "builds=[]" >> "$GITHUB_OUTPUT"
+ else
+ joined=$(IFS=,; echo "${builds[*]}")
+ echo "builds=[${joined}]" >> "$GITHUB_OUTPUT"
+ fi
+
+ build:
+ needs: detect
+ if: needs.detect.outputs.builds != '[]'
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ build: ${{ fromJson(needs.detect.outputs.builds) }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: ./${{ matrix.build.context }}
+ target: ${{ matrix.build.target }}
+ build-args: ${{ matrix.build.build_args }}
+ platforms: linux/amd64
+ push: true
+ tags: devitools/${{ matrix.build.image }}:${{ matrix.build.tag }}
+ cache-from: type=gha,scope=${{ matrix.build.image }}-${{ matrix.build.tag }}
+ cache-to: type=gha,mode=max,scope=${{ matrix.build.image }}-${{ matrix.build.tag }}
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..da6b786
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,102 @@
+name: Validate variants.yaml
+
+on:
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Get changed variants.yaml files
+ id: changed
+ uses: tj-actions/changed-files@v44
+ with:
+ files: '*/*/variants.yaml'
+
+ - name: Validate each manifest
+ if: steps.changed.outputs.any_changed == 'true'
+ shell: bash
+ env:
+ CHANGED_MANIFESTS: ${{ steps.changed.outputs.all_changed_files }}
+ run: |
+ set -uo pipefail
+ errors=0
+
+ for manifest in $CHANGED_MANIFESTS; do
+ case "$manifest" in
+ [A-Za-z0-9_./-]*) ;;
+ *) echo "::warning::skipping suspicious path '$manifest'"; continue ;;
+ esac
+ echo "::group::${manifest}"
+ dir="$(dirname "$manifest")"
+ dockerfile="${dir}/Dockerfile"
+
+ if ! yq eval '.' "$manifest" > /dev/null 2>&1; then
+ echo "::error file=${manifest}::invalid YAML syntax"
+ errors=$((errors+1))
+ echo "::endgroup::"
+ continue
+ fi
+
+ kind=$(yq eval 'type' "$manifest")
+ if [ "$kind" != "!!seq" ]; then
+ echo "::error file=${manifest}::root must be a YAML list (got ${kind})"
+ errors=$((errors+1))
+ echo "::endgroup::"
+ continue
+ fi
+
+ entries=$(yq eval -o=json "$manifest")
+
+ bad=$(jq -c '[.[] | select((.target | type) != "string" or .target == "" or (has("suffix") | not))]' <<< "$entries")
+ if [ "$(jq 'length' <<< "$bad")" != "0" ]; then
+ echo "::error file=${manifest}::entries missing required 'target' (non-empty string) or 'suffix' key:"
+ jq -r '.[] | " - " + (. | tostring)' <<< "$bad"
+ errors=$((errors+1))
+ fi
+
+ bad_args=$(jq -c '[.[] | select(has("args") and (.args | type) != "object")]' <<< "$entries")
+ if [ "$(jq 'length' <<< "$bad_args")" != "0" ]; then
+ echo "::error file=${manifest}::'args' must be a mapping when present:"
+ jq -r '.[] | " - " + (. | tostring)' <<< "$bad_args"
+ errors=$((errors+1))
+ fi
+
+ if [ ! -f "$dockerfile" ]; then
+ echo "::error file=${manifest}::sibling Dockerfile not found at ${dockerfile}"
+ errors=$((errors+1))
+ else
+ for target in $(jq -r '.[].target' <<< "$entries" | sort -u); do
+ if ! grep -qE "^FROM[[:space:]]+.*[[:space:]]+AS[[:space:]]+${target}([[:space:]]|$)" "$dockerfile"; then
+ echo "::error file=${manifest}::target '${target}' is not declared as a stage in ${dockerfile} (expected: FROM AS ${target})"
+ errors=$((errors+1))
+ fi
+ done
+ fi
+
+ duplicates=$(jq -r '
+ [.[] | (if (.suffix // "") == "" then "" else .suffix end)] |
+ group_by(.) | map(select(length > 1)) | map(.[0]) | .[]
+ ' <<< "$entries" | sort -u)
+ if [ -n "$duplicates" ]; then
+ echo "::error file=${manifest}::duplicate suffix(es) detected (would produce colliding tags):"
+ echo "$duplicates" | sed 's/^/ - /'
+ errors=$((errors+1))
+ fi
+
+ echo "::endgroup::"
+ done
+
+ if [ $errors -gt 0 ]; then
+ echo "Found ${errors} error(s) across the changed manifests."
+ exit 1
+ fi
+ echo "All changed variants.yaml are valid."
diff --git a/php-hyperf/8.3/.scripts/setup-dev.sh b/php-hyperf/8.3/.scripts/setup-dev.sh
new file mode 100755
index 0000000..1e1beb3
--- /dev/null
+++ b/php-hyperf/8.3/.scripts/setup-dev.sh
@@ -0,0 +1,44 @@
+set -e
+
+SONAR_SCANNER_VERSION=6.2.1.4610
+
+if [ "$1" = "dev" ]; then
+ if ! command -v apk &> /dev/null; then
+ echo "Error: 'apk' not found. Make sure you are running this script in an Alpine Linux environment." >&2
+ exit 1
+ fi
+
+ if ! command -v composer &> /dev/null; then
+ echo "Error: 'composer' not found. Make sure it is installed and accessible in the system PATH." >&2
+ exit 1
+ fi
+
+ echo "[$1] Installing PHP extensions and dependencies"
+
+ apk add --no-cache \
+ libstdc++ \
+ ca-certificates \
+ libc6-compat \
+ openjdk17-jre \
+ php83-pecl-xdebug \
+ php83-pecl-pcov
+
+ {
+ echo "opcache.enable=0"
+ echo "opcache.interned_strings_buffer=72"
+ echo "xdebug.mode=develop,debug,coverage"
+ echo "xdebug.idekey=PHPSTORM"
+ } >> /etc/php83/conf.d/zzz_2_php.ini
+
+ mkdir -p /opt
+ curl -fSL https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux-x64.zip \
+ -o /opt/sonar-scanner.zip
+
+ unzip -qq /opt/sonar-scanner.zip -d /opt
+ mv /opt/sonar-scanner-${SONAR_SCANNER_VERSION}-linux-x64 /sonar-scanner
+ rm /opt/sonar-scanner.zip
+
+ ln -s /sonar-scanner/bin/sonar-scanner /bin/sonar-scanner
+
+ sed -i 's/use_embedded_jre=true/use_embedded_jre=false/g' /sonar-scanner/bin/sonar-scanner
+fi
diff --git a/php-hyperf/8.3/.scripts/setup.sh b/php-hyperf/8.3/.scripts/setup.sh
new file mode 100755
index 0000000..b99313a
--- /dev/null
+++ b/php-hyperf/8.3/.scripts/setup.sh
@@ -0,0 +1,18 @@
+set -e
+
+TIMEZONE=${1:-UTC}
+
+git config --global --add safe.directory /opt/www
+git config --global init.defaultBranch main
+
+# - config PHP
+{
+ echo "upload_max_filesize=128M"
+ echo "post_max_size=128M"
+ echo "memory_limit=1G"
+ echo "date.timezone=${TIMEZONE}"
+} | tee /etc/php83/conf.d/zzz_0_php.ini
+
+# - config timezone
+ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime
+echo "${TIMEZONE}" > /etc/timezone
diff --git a/php-hyperf/8.3/Dockerfile b/php-hyperf/8.3/Dockerfile
new file mode 100644
index 0000000..9626fdb
--- /dev/null
+++ b/php-hyperf/8.3/Dockerfile
@@ -0,0 +1,95 @@
+FROM alpine:3.20 AS hyperf
+
+ARG CONTEXT
+ARG TIMEZONE
+ARG APP_TARGET="liv"
+
+ENV TIMEZONE=${TIMEZONE:-"UTC"} \
+ STDOUT_LOG_LEVEL=alert,critical,emergency,error,warning \
+ SCAN_CACHEABLE=(true)
+
+COPY ${CONTEXT:-""}/rootfs /
+
+RUN apk update && \
+ apk upgrade --available && \
+ apk add --no-cache bash curl git unzip && \
+ mkdir -p /opt/www
+
+SHELL ["/bin/ash", "-o", "pipefail", "-c"]
+
+RUN apk add --no-cache \
+ php83-bcmath=8.3.15-r0 \
+ php83-ctype=8.3.15-r0 \
+ php83-curl=8.3.15-r0 \
+ php83-dom=8.3.15-r0 \
+ php83-fileinfo=8.3.15-r0 \
+ php83-fpm=8.3.15-r0 \
+ php83-gd=8.3.15-r0 \
+ php83-iconv=8.3.15-r0 \
+ php83-intl=8.3.15-r0 \
+ php83-mbstring=8.3.15-r0 \
+ php83-opcache=8.3.15-r0 \
+ php83-openssl=8.3.15-r0 \
+ php83-pcntl=8.3.15-r0 \
+ php83-pdo=8.3.15-r0 \
+ php83-pdo_mysql=8.3.15-r0 \
+ php83-pecl-decimal=1.5.0-r1 \
+ php83-pecl-ds=1.5.0-r0 \
+ php83-pecl-mcrypt=1.0.7-r0 \
+ php83-pecl-mongodb=1.19.1-r0 \
+ php83-pecl-rdkafka=6.0.5-r0 \
+ php83-pecl-redis=6.1.0-r0 \
+ php83-pecl-swoole=5.1.6-r0 \
+ php83-phar=8.3.15-r0 \
+ php83-posix=8.3.15-r0 \
+ php83-simplexml=8.3.15-r0 \
+ php83-sodium=8.3.15-r0 \
+ php83-tokenizer=8.3.15-r0 \
+ php83-xml=8.3.15-r0 \
+ php83-xmlreader=8.3.15-r0 \
+ php83-xmlwriter=8.3.15-r0 \
+ php83-zip=8.3.15-r0 \
+ && ln -sf /usr/bin/php83 /usr/bin/php \
+ && mv /etc/php/php.ini /etc/php83/conf.d/zzz_1_php.ini \
+ && mv /etc/php/php-fpm.conf /etc/php83/php-fpm.d/zphp.conf
+
+COPY --from=composer/composer:2.8.5-bin /composer /usr/local/bin/composer
+
+COPY .scripts /devitools/.scripts
+
+RUN set -ex \
+ && bash /devitools/.scripts/setup.sh "$TIMEZONE" \
+ && bash /devitools/.scripts/setup-dev.sh "$APP_TARGET" \
+ && rm -rf /var/cache/apk/* /tmp/* /usr/share/man
+
+WORKDIR /opt/www
+EXPOSE 9501
+ENTRYPOINT ["php", "/opt/www/bin/hyperf.php"]
+CMD ["start"]
+
+# --- Variant: with OTEL Collector + pgbouncer + supervisor ---
+FROM hyperf AS hyperf-otel
+
+ARG COLLECTOR=debug
+ARG OTEL_COLLECTOR_VERSION=0.121.0
+
+RUN apk add --no-cache --upgrade expat \
+ && apk add --no-cache supervisor wget pgbouncer \
+ && OTEL_BASE_URL="https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_COLLECTOR_VERSION}" \
+ && OTEL_ASSET="otelcol-contrib_${OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz" \
+ && cd /tmp \
+ && wget -q "${OTEL_BASE_URL}/${OTEL_ASSET}" -O "${OTEL_ASSET}" \
+ && wget -q "${OTEL_BASE_URL}/opentelemetry-collector-releases_otelcol-contrib_checksums.txt" -O checksums.txt \
+ && grep " ${OTEL_ASSET}$" checksums.txt | sha256sum -c - \
+ && tar -xzf "${OTEL_ASSET}" -C /usr/local/bin otelcol-contrib \
+ && rm "${OTEL_ASSET}" checksums.txt \
+ && chmod +x /usr/local/bin/otelcol-contrib \
+ && mkdir -p /etc/pgbouncer /var/log/pgbouncer /var/run/pgbouncer /etc/supervisor.d /var/run/supervisor
+
+COPY otel/collectors/${COLLECTOR}.yaml /etc/otel-collector-config.yaml
+COPY otel/supervisord.conf /etc/supervisord.conf
+COPY otel/entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+CMD []
diff --git a/php-hyperf/8.3/README-pt-BR.md b/php-hyperf/8.3/README-pt-BR.md
new file mode 100644
index 0000000..d37d40c
--- /dev/null
+++ b/php-hyperf/8.3/README-pt-BR.md
@@ -0,0 +1,79 @@
+# Devitools Hyperf Docker Image
+
+## Introdução
+Esta imagem Docker foi criada para fornecer um ambiente otimizado para aplicações **Hyperf** em **PHP 8.3**, suportando tanto ambientes de desenvolvimento quanto de produção.
+
+Ela inclui configurações específicas para **Xdebug**, **Sonar Scanner**, ajustes de performance no PHP e suporte a análise estática de código.
+
+---
+
+## 📦 Conteúdo da Imagem
+A imagem contém:
+- **PHP 8.3** com extensões essenciais
+- **Composer** para gerenciamento de dependências
+- **Xdebug** e **PCOV** para depuração e cobertura de código
+- **Sonar Scanner** para análise de qualidade de código
+- **Configuração de timezone** ajustável
+- **Suporte a análise de código via SonarQube**
+
+---
+
+## 🛠️ Configuração e Instalação
+A configuração da imagem é realizada através dos scripts:
+
+### `setup.sh`
+- Ajusta configurações do PHP, incluindo **limite de memória**, **upload_max_filesize**, e **timezone**.
+- Configura o timezone do sistema.
+
+### `setup-dev.sh`
+- Instala dependências para desenvolvimento, incluindo **Xdebug** e **Sonar Scanner**.
+- Configura `xdebug.ini` para integração com **PHPStorm**.
+- Desativa **JRE embutido** no Sonar Scanner para evitar conflitos.
+
+---
+
+## 📌 Como Construir a Imagem
+A imagem pode ser construída de duas formas: **para desenvolvimento** e **para produção**.
+
+### 🔹 **Imagem de Desenvolvimento**
+Inclui **Xdebug**, **PCOV** e **Sonar Scanner** para depuração e análise de código.
+
+```sh
+docker build --platform="linux/amd64" --build-arg APP_TARGET=dev -t devitools/php-hyperf:8.3-dev .
+```
+
+### 🔹 **Imagem de Produção**
+Removendo ferramentas de desenvolvimento para otimizar o ambiente de execução.
+
+```sh
+docker build --platform="linux/amd64" -t devitools/php-hyperf:8.3 .
+```
+
+---
+
+## 🚀 Diferenças entre as Versões
+
+| Versão | Recursos Incluídos |
+|-----------------------|------------------|
+| `devitools/php-hyperf:8.3-dev` | PHP 8.3 + Xdebug + PCOV + Sonar Scanner |
+| `devitools/php-hyperf:8.3` | PHP 8.3 otimizado para produção |
+
+---
+
+## 🛠 Uso
+Para rodar um container baseado na imagem:
+
+```sh
+docker run --rm -it devitools/php-hyperf:8.3-dev php -v
+```
+
+Para iniciar um projeto Hyperf com a imagem:
+
+```sh
+docker run --rm -it -v $(pwd):/opt/www devitools/php-hyperf:8.3-dev composer create-project hyperf/hyperf-skeleton .
+```
+
+---
+
+## 📌 Conclusão
+Esta imagem proporciona um ambiente completo para desenvolvimento e execução de aplicações Hyperf, garantindo produtividade e qualidade no código.
diff --git a/php-hyperf/8.3/README.md b/php-hyperf/8.3/README.md
new file mode 100644
index 0000000..15c9f1c
--- /dev/null
+++ b/php-hyperf/8.3/README.md
@@ -0,0 +1,158 @@
+# Devitools Hyperf
+
+Optimized image for running Hyperf applications with PostgreSQL and Swoole support.
+
+## Introduction
+
+This Docker image is designed to provide an optimized environment for **Hyperf** applications running **PHP 8.3**,
+supporting both development and production environments.
+
+It includes specific configurations for **Xdebug**, **Sonar Scanner**, PHP performance tuning, and static code analysis
+support.
+
+## 🛠️ How to Use
+
+### Run a Container Directly
+
+```sh
+docker run --rm --name hyperf -d -v "$(pwd):/opt/www" -p "8080:9501" --platform linux/amd64 devitools/php-hyperf:8.3-dev
+```
+
+### Example `Dockerfile` Ready for Production
+
+```dockerfile
+FROM devitools/php-hyperf:8.3
+
+COPY . /opt/www
+
+RUN composer install --prefer-dist --no-dev --optimize-autoloader
+```
+
+## 🚀 Example `docker-compose.yml` Ready for dev
+
+```yaml
+services:
+ app:
+ image: devitools/php-hyperf:8.3-dev
+ container_name: template_name-app
+ command: [ "server:watch" ]
+ volumes:
+ - ./:/opt/www
+ ports:
+ - "9501:9501"
+ environment:
+ - SCAN_CACHEABLE=false
+ - STDOUT_LOG_LEVEL=alert,critical,emergency,error,warning,notice,info
+ restart: on-failure
+```
+
+## 📌 Environment Variables
+
+- `SCAN_CACHEABLE`: Controls Hyperf’s scan cache.
+- `STDOUT_LOG_LEVEL`: Sets the log levels sent to `stdout`.
+
+## 🏗️ Just Run, no Build
+
+```sh
+docker-compose up -d
+```
+
+## 📂 Related Repositories
+
+- **Dockerfile Repository** (Source code for this image):
+ 🔗 [github.com/devitools/dockerfile](https://github.com/devitools/dockerfile)
+
+- **Example Project** (Hyperf with this image):
+ 🔗 [github.com/phpcomrapadura/hyperf-com-rapadura](https://github.com/phpcomrapadura/hyperf-com-rapadura)
+
+---
+
+## 📦 Image Contents
+
+The image includes:
+
+- **PHP 8.3** with essential extensions
+- **Composer** for dependency management
+- **Xdebug** and **PCOV** for debugging and code coverage
+- **Sonar Scanner** for code quality analysis
+- **Adjustable timezone configuration**
+- **Support for static code analysis via SonarQube**
+
+---
+
+## 🛠️ Configuration and Installation
+
+The image configuration is handled through the following scripts:
+
+### `setup.sh`
+
+- Adjusts PHP settings, including **memory limit**, **upload_max_filesize**, and **timezone**.
+- Configures the system timezone.
+
+### `setup-dev.sh`
+
+- Installs development dependencies, including **Xdebug** and **Sonar Scanner**.
+- Configures `xdebug.ini` for **PHPStorm** integration.
+- Disables **embedded JRE** in Sonar Scanner to avoid conflicts.
+
+---
+
+## 📌 How to Build the Image
+
+The image can be built in two ways: **for development** and **for production**.
+
+### 🔹 **Development Image**
+
+Includes **Xdebug**, **PCOV**, and **Sonar Scanner** for debugging and code analysis.
+
+```sh
+docker build --platform="linux/amd64" --build-arg APP_TARGET=dev -t devitools/php-hyperf:8.3-dev .
+```
+
+### 🔹 **Production Image**
+
+Removes development tools to optimize the runtime environment.
+
+```sh
+docker build --platform="linux/amd64" -t devitools/php-hyperf:8.3 .
+```
+
+## ✨ How to publish the new image
+
+After building the image, you can push it to Docker Hub:
+
+```sh
+docker push devitools/php-hyperf:8.3-dev
+```
+
+---
+
+## 🚀 Differences Between Versions
+
+| Version | Included Features |
+|----------------------------|-----------------------------------------|
+| `devitools/php-hyperf:8.3-dev` | PHP 8.3 + Xdebug + PCOV + Sonar Scanner |
+| `devitools/php-hyperf:8.3` | PHP 8.3 optimized for production |
+
+---
+
+## 🛠 Usage
+
+To run a container based on the image:
+
+```sh
+docker run --rm -it devitools/php-hyperf:8.3-dev php -v
+```
+
+To start a Hyperf project with the image:
+
+```sh
+docker run --rm -it -v $(pwd):/opt/www devitools/php-hyperf:8.3-dev composer create-project hyperf/hyperf-skeleton .
+```
+
+---
+
+## 📌 Conclusion
+
+This image provides a complete environment for developing and running Hyperf applications, ensuring productivity and
+code quality.
diff --git a/php-hyperf/8.3/otel/collectors/debug.yaml b/php-hyperf/8.3/otel/collectors/debug.yaml
new file mode 100644
index 0000000..4dc8718
--- /dev/null
+++ b/php-hyperf/8.3/otel/collectors/debug.yaml
@@ -0,0 +1,22 @@
+receivers:
+ zipkin:
+ endpoint: "0.0.0.0:9411"
+
+processors:
+ batch:
+ send_batch_size: 200
+ timeout: 5s
+ memory_limiter:
+ check_interval: 1s
+ limit_mib: 256
+
+exporters:
+ debug:
+ verbosity: normal
+
+service:
+ pipelines:
+ traces:
+ receivers: [zipkin]
+ processors: [memory_limiter, batch]
+ exporters: [debug]
diff --git a/php-hyperf/8.3/otel/collectors/google.yaml b/php-hyperf/8.3/otel/collectors/google.yaml
new file mode 100644
index 0000000..663fae1
--- /dev/null
+++ b/php-hyperf/8.3/otel/collectors/google.yaml
@@ -0,0 +1,22 @@
+receivers:
+ zipkin:
+ endpoint: "0.0.0.0:9411"
+
+processors:
+ batch:
+ send_batch_size: 200
+ timeout: 5s
+ memory_limiter:
+ check_interval: 1s
+ limit_mib: 256
+
+exporters:
+ googlecloud:
+ project: ${GOOGLE_CLOUD_PROJECT}
+
+service:
+ pipelines:
+ traces:
+ receivers: [zipkin]
+ processors: [memory_limiter, batch]
+ exporters: [googlecloud]
diff --git a/php-hyperf/8.3/otel/entrypoint.sh b/php-hyperf/8.3/otel/entrypoint.sh
new file mode 100755
index 0000000..57d6b3c
--- /dev/null
+++ b/php-hyperf/8.3/otel/entrypoint.sh
@@ -0,0 +1,112 @@
+#!/bin/sh
+set -e
+
+if [ "$PGBOUNCER_ENABLED" != "true" ]; then
+ exec supervisord -c /etc/supervisord.conf
+fi
+
+PGBOUNCER_DATABASES="${PGBOUNCER_DATABASES:-default}"
+
+DEFAULT_POOL_SIZE="${PGBOUNCER_DEFAULT_POOL_SIZE:-5}"
+MIN_POOL_SIZE="${PGBOUNCER_MIN_POOL_SIZE:-1}"
+RESERVE_POOL_SIZE="${PGBOUNCER_RESERVE_POOL_SIZE:-2}"
+MAX_CLIENT_CONN="${PGBOUNCER_MAX_CLIENT_CONN:-1000}"
+MAX_PREPARED_STATEMENTS="${PGBOUNCER_MAX_PREPARED_STATEMENTS:-100}"
+SERVER_IDLE_TIMEOUT="${PGBOUNCER_SERVER_IDLE_TIMEOUT:-300}"
+SERVER_LIFETIME="${PGBOUNCER_SERVER_LIFETIME:-3600}"
+
+validate_alias() {
+ case "$1" in
+ *[!A-Za-z0-9_]*|"")
+ echo "entrypoint: invalid PGBOUNCER_DATABASES alias '$1' (allowed: [A-Za-z0-9_])" >&2
+ exit 1
+ ;;
+ esac
+}
+
+resolve_prefix() {
+ if [ "$1" = "default" ]; then
+ echo "POSTGRES_DB"
+ else
+ echo "POSTGRES_DB_$(echo "$1" | tr '[:lower:]' '[:upper:]')"
+ fi
+}
+
+echo "[databases]" > /etc/pgbouncer/pgbouncer.ini
+
+OLD_IFS="$IFS"
+IFS=','
+for alias in $PGBOUNCER_DATABASES; do
+ IFS="$OLD_IFS"
+ alias=$(echo "$alias" | tr -d ' ')
+ if [ -n "$alias" ]; then
+ validate_alias "$alias"
+ prefix=$(resolve_prefix "$alias")
+ host=$(eval "printf '%s' \"\${${prefix}_HOST:-}\"")
+ port=$(eval "printf '%s' \"\${${prefix}_PORT:-5432}\"")
+ name=$(eval "printf '%s' \"\${${prefix}_NAME:-}\"")
+ user=$(eval "printf '%s' \"\${${prefix}_USERNAME:-}\"")
+ pass=$(eval "printf '%s' \"\${${prefix}_PASSWORD:-}\"")
+
+ [ -z "$host" ] && { echo "entrypoint: ${prefix}_HOST is required for alias '${alias}'" >&2; exit 1; }
+ [ -z "$name" ] && { echo "entrypoint: ${prefix}_NAME is required for alias '${alias}'" >&2; exit 1; }
+ [ -z "$user" ] && { echo "entrypoint: ${prefix}_USERNAME is required for alias '${alias}'" >&2; exit 1; }
+ [ -z "$pass" ] && { echo "entrypoint: ${prefix}_PASSWORD is required for alias '${alias}'" >&2; exit 1; }
+
+ echo "pgb_${alias} = host=${host} port=${port} dbname=${name} user=${user} password=${pass}" >> /etc/pgbouncer/pgbouncer.ini
+ fi
+ IFS=','
+done
+IFS="$OLD_IFS"
+
+cat >> /etc/pgbouncer/pgbouncer.ini < /etc/supervisor.d/pgbouncer.ini <