diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 38e90fe..1c71c67 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -23,13 +23,22 @@ concurrency: jobs: build: - runs-on: ubuntu-24.04 # ubuntu-latest + name: Build ${{ matrix.target.platform }} ${{ matrix.variant }} ${{ matrix.php_version }} + runs-on: ${{ matrix.target.runner }} timeout-minutes: 10 strategy: + fail-fast: false matrix: - php_version: ['8.4', '8.3', '8.1', '8.0', '7.4'] + php_version: ['8.5' , '8.4', '8.3', '8.1', '8.0', '7.4'] variant: ['apache', 'apache-chrome'] + target: + - platform: linux/amd64 + runner: ubuntu-24.04 # ubuntu-latest + artifact_suffix: linux-amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm # ubuntu-latest + artifact_suffix: linux-arm64 exclude: - php_version: '7.4' variant: 'apache-chrome' @@ -38,17 +47,13 @@ jobs: - name: Checkout Code uses: actions/checkout@v6 - - name: Github Short SHA - run: | - echo "GITHUB_SHA_SHORT=$(echo $GITHUB_SHA | cut -c 1-7)" >> $GITHUB_ENV - - name: Set variables run: | if [ "${{ matrix.variant }}" == "apache" ]; then - echo "DOCKER_TAG=${{ matrix.php_version }}" >> $GITHUB_ENV + echo "DOCKER_TAG=${{ github.event.number }}_${{ matrix.php_version }}" >> $GITHUB_ENV echo "DOCKER_FILE=Dockerfile" >> $GITHUB_ENV elif [ "${{ matrix.variant }}" == "apache-chrome" ]; then - echo "DOCKER_TAG=${{ matrix.php_version }}-headless" >> $GITHUB_ENV + echo "DOCKER_TAG=${{ github.event.number }}_${{ matrix.php_version }}-headless" >> $GITHUB_ENV echo "DOCKER_FILE=Dockerfile-headless" >> $GITHUB_ENV else echo "Invalid variant: ${{ matrix.variant }}" @@ -58,21 +63,94 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Login to Docker Registry - run: echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u ${{ secrets.DOCKER_HUB_USER }} --password-stdin - - name: Login to GitHub Container Registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - - name: Build and push + - name: Build and push by digest + id: build uses: docker/build-push-action@v7 with: context: . - file: ./${{ matrix.php_version }}/${{ env.DOCKER_FILE }} - tags: - ghcr.io/${{ github.repository }}:${{ env.DOCKER_TAG }}, - ghcr.io/${{ github.repository }}:${{ env.GITHUB_SHA_SHORT }} - push: true - platforms: linux/amd64,linux/arm64 + file: ./aio/${{ env.DOCKER_FILE }} + build-args: | + PHP_VERSION=${{ matrix.php_version }} + platforms: ${{ matrix.target.platform }} + outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true + provenance: mode=min cache-from: type=gha cache-to: type=gha,mode=max + + - name: Export digest + run: | + mkdir -p "/tmp/digests/${{ env.DOCKER_TAG }}" + DIGEST="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${{ env.DOCKER_TAG }}/${DIGEST#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v7 + with: + name: digests-${{ env.DOCKER_TAG }}-${{ matrix.target.artifact_suffix }} + path: /tmp/digests + if-no-files-found: error + retention-days: 1 + + manifest: + name: Create multi-arch manifests + runs-on: ubuntu-24.04-arm + timeout-minutes: 10 + needs: build + + steps: + - name: Github Short SHA + run: | + echo "GITHUB_SHA_SHORT=$(echo "$GITHUB_SHA" | cut -c 1-7)" >> "$GITHUB_ENV" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to Docker Registry + run: echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u ${{ secrets.DOCKER_HUB_USER }} --password-stdin + + - name: Login to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Download digests + uses: actions/download-artifact@v8 + with: + pattern: digests-* + path: /tmp/digests + merge-multiple: true + + - name: Create multi-arch manifests + run: | + set -euo pipefail + + for digest_dir in /tmp/digests/*; do + if [ ! -d "$digest_dir" ]; then + continue + fi + + docker_tag="$(basename "$digest_dir")" + digest_count="$(find "$digest_dir" -maxdepth 1 -type f | wc -l | tr -d ' ')" + + if [ "$digest_count" -lt 2 ]; then + echo "Expected at least 2 digests for ${docker_tag}, found ${digest_count}" + find "$digest_dir" -maxdepth 1 -type f -print + exit 1 + fi + + refs="" + for digest_file in "$digest_dir"/*; do + digest="$(basename "$digest_file")" + refs="${refs} ghcr.io/${{ github.repository }}@sha256:${digest}" + done + + echo "Creating manifest for ghcr.io/${{ github.repository }}:${docker_tag}" + + docker buildx imagetools create \ + -t "ghcr.io/${{ github.repository }}:${docker_tag}" \ + -t "ghcr.io/${{ github.repository }}:${GITHUB_SHA_SHORT}-${docker_tag}" \ + ${refs} + + docker buildx imagetools inspect "ghcr.io/${{ github.repository }}:${docker_tag}" + done diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..922d0ec --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,78 @@ +name: Test Build Images on PR + +permissions: + contents: read + +on: + workflow_dispatch: {} + pull_request: + types: [opened, synchronize] + paths-ignore: + - "README.md" + - "docker-compose.yml" + - "examples/**" + - ".run/**" + - ".circleci/**" + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + build: + name: Build ${{ matrix.target.platform }} ${{ matrix.variant }} ${{ matrix.php_version }} + runs-on: ${{ matrix.target.runner }} + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + php_version: ['8.5' , '8.4', '8.3', '8.1', '8.0', '7.4'] + variant: ['apache', 'apache-chrome'] + target: + - platform: linux/amd64 + runner: ubuntu-24.04 # ubuntu-latest + artifact_suffix: linux-amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm # ubuntu-latest + artifact_suffix: linux-arm64 + exclude: + - php_version: '7.4' + variant: 'apache-chrome' + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Set variables + run: | + if [ "${{ matrix.variant }}" == "apache" ]; then + echo "DOCKER_TAG=pr_${{ github.event.number }}_${{ matrix.php_version }}" >> $GITHUB_ENV + echo "DOCKER_FILE=Dockerfile" >> $GITHUB_ENV + elif [ "${{ matrix.variant }}" == "apache-chrome" ]; then + echo "DOCKER_TAG=pr_${{ github.event.number }}_${{ matrix.php_version }}-headless" >> $GITHUB_ENV + echo "DOCKER_FILE=Dockerfile-headless" >> $GITHUB_ENV + else + echo "Invalid variant: ${{ matrix.variant }}" + exit 1 + fi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v7 + with: + context: . + file: ./aio/${{ env.DOCKER_FILE }} + build-args: | + PHP_VERSION=${{ matrix.php_version }} + platforms: ${{ matrix.target.platform }} + push: false + provenance: mode=min + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index 3c66c34..7956683 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,20 @@ ## Why -The images formed from this repo is to help minimize the extra steps needed to get up and running with a PHP Image with apache to serve traffic. All docker images are built in a CI pipeline and run every 2 days to ensure all dependencies are up-to-date with the source they are installed from. +The images formed from this repo are to help minimize the extra steps needed to get up and running with a PHP Image with apache to serve traffic. All docker images are built in a CI pipeline and run every 2 days to ensure all dependencies are up to date with the source they are installed from. -After using this docker image in other pipelines I was able to cut build times down by 4-5 minutes per build. +After using this docker image in other pipelines, I was able to cut build times down by 4-5 minutes per build. + +_Images built also support ARM/ARM64 architectures_ ## Changes -This image is the PHP 7.4 (and 8.0, 8.1) packaged with the following changes: -- PHP (version 7.4 or 8.0) +This image is the PHP+Apache Image packaged with the following changes: +- PHP (version 7.4 or 8.0–8.5) - Apache -- OPCache (and configured) +- OPCache (configured) - OpenSSL - ZIP (with libzip-dev and php extension) -- git -- iputils - Redis Extension - GD and imagick Extensions - PDO @@ -25,15 +25,16 @@ This image is the PHP 7.4 (and 8.0, 8.1) packaged with the following changes: - Apache Signatures Off - Apache Allow htaccess overrides - Apache Logs Updated to include the remote ips +- Apache Server Status (configured on :8282/healthz and :8282/server-status) --- # Images -## [Github Images](https://github.com/nhalstead/php-apache-core/pkgs/container/php-apache-core) +## [GitHub Images](https://github.com/nhalstead/php-apache-core/pkgs/container/php-apache-core) > ghcr.io/nhalstead/php-apache-core -> #### Automatically built on Wednesday +> #### Automatically built on Wednesday by GitHub Actions | PHP Version | Tag: Apache | Tag: Apache + Chrome | |:------------|:------------|:---------------------| @@ -46,7 +47,7 @@ This image is the PHP 7.4 (and 8.0, 8.1) packaged with the following changes: ## [Docker Hub Images](https://hub.docker.com/r/nhalstead00/php-apache-core) > nhalstead00/php-apache-core -> #### Automatically built every 2 days +> #### Automatically built every 2 days by CircleCI (will be replaced with GitHub Actions in the future) | PHP Version | Tag: Apache | Tag: Apache + Chrome | |:------------|:------------|:---------------------| @@ -54,5 +55,3 @@ This image is the PHP 7.4 (and 8.0, 8.1) packaged with the following changes: | 8.1 | 8.1-latest | 8.1-headless-latest | | 8.0 | 8.0-latest | 8.0-headless-latest | | 7.4 | 7.4-latest | | - - diff --git a/aio/Dockerfile b/aio/Dockerfile new file mode 100644 index 0000000..012cf79 --- /dev/null +++ b/aio/Dockerfile @@ -0,0 +1,51 @@ +# Normal Operations for setup of Apache and ENV Vars +# Allow building different PHP versions from build args (CI uses PHP_VERSION) +ARG PHP_VERSION=8.3 +ARG PHP_TAG=${PHP_VERSION}-apache +FROM php:${PHP_TAG} AS base + +# Configure OPCache +ENV PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 +ENV PHP_OPCACHE_MAX_ACCELERATED_FILES=10000 +ENV PHP_OPCACHE_MEMORY_CONSUMPTION=192 +ENV PHP_OPCACHE_MAX_WASTED_PERCENTAGE=10 + +COPY common/opcache.ini /usr/local/etc/php/conf.d/opcache.ini + +RUN apt-get update -y; \ + apt-get install -y openssl zip unzip curl libpng-dev libzip-dev libmagickwand-dev --no-install-recommends; \ + docker-php-ext-install -j"$(nproc)" gd zip pdo mysqli pdo_mysql opcache; \ + pecl install redis imagick; \ + docker-php-ext-enable -j"$(nproc)" redis imagick; \ + apt autoremove -y; \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \ + rm -rf /var/lib/apt/lists/*; + +FROM base + +# Configure Apache: combine multiple small RUNs into one layer to reduce image size +# Copy scripts and config files first so they can be used in the same layer +COPY common/remoteip-env /usr/sbin +COPY common/remoteip-clear /usr/sbin +COPY common/opcache-preload /usr/sbin +COPY common/opcache-status /usr/sbin +COPY common/apache-status /usr/sbin +COPY common/status.conf /etc/apache2/sites-enabled/000-status.conf +COPY common/healthz.html /var/www/html/healthz.html + +RUN set -eux; \ + a2enmod rewrite remoteip status; \ + touch /etc/apache2/conf-available/remoteip.conf; \ + ln -s /etc/apache2/conf-available/remoteip.conf /etc/apache2/conf-enabled/remoteip.conf || true; \ + chmod +x /usr/sbin/remoteip-* /usr/sbin/opcache-* /usr/sbin/apache-status || true; \ + sed -ri -e 's!AllowOverride None!AllowOverride All!g' /etc/apache2/apache2.conf; \ + sed -ri -e 's!Listen 80!Listen 8181!g' /etc/apache2/ports.conf; \ + sed -ri -e 's!VirtualHost \*:80!VirtualHost \*:8181!g' /etc/apache2/sites-enabled/000-default.conf; \ + echo "ServerSignature off" >> /etc/apache2/apache2.conf; \ + echo "ServerTokens Prod" >> /etc/apache2/apache2.conf; \ + printf '\n\n Listen 8282\n\n' >> /etc/apache2/ports.conf; \ + sed -ri -e 's!#LoadModule remoteip_module!LoadModule remoteip_module!g' /etc/apache2/apache2.conf; \ + sed -ri -e 's/LogFormat .* combined/LogFormat "%a %{c}a %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" %I %O" combined/g' /etc/apache2/apache2.conf + +EXPOSE 8181 +EXPOSE 8282 \ No newline at end of file diff --git a/aio/Dockerfile-headless b/aio/Dockerfile-headless new file mode 100644 index 0000000..17092ba --- /dev/null +++ b/aio/Dockerfile-headless @@ -0,0 +1,66 @@ +# Normal Operations for setup of Apache and ENV Vars +# Allow building different PHP versions from build args (CI uses PHP_VERSION) +ARG PHP_VERSION=8.3 +ARG PHP_TAG=${PHP_VERSION}-apache +FROM php:${PHP_TAG} AS base + +# Configure OPCache +ENV PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 +ENV PHP_OPCACHE_MAX_ACCELERATED_FILES=10000 +ENV PHP_OPCACHE_MEMORY_CONSUMPTION=192 +ENV PHP_OPCACHE_MAX_WASTED_PERCENTAGE=10 + +COPY common/opcache.ini /usr/local/etc/php/conf.d/opcache.ini + +RUN apt-get update -y; \ + apt-get install -y openssl zip unzip curl libpng-dev libzip-dev libmagickwand-dev --no-install-recommends; \ + docker-php-ext-install -j"$(nproc)" gd zip pdo mysqli pdo_mysql opcache; \ + pecl install redis imagick; \ + docker-php-ext-enable -j"$(nproc)" redis imagick; \ + apt autoremove -y; \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \ + rm -rf /var/lib/apt/lists/*; + +FROM base AS browser + +# As needed for Chrome to Run +# https://github.com/beganovich/snappdf#downloading-local-chromium +# https://github.com/beganovich/snappdf?tab=readme-ov-file#headless-chrome-doesnt-launch-on-unix +RUN apt-get update -y; \ + apt-get install -y ca-certificates fonts-liberation libnss3 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release xdg-utils --no-install-recommends; \ + rm -rf /var/lib/apt/lists/*; + +FROM browser + +# Configure Apache and combine many small RUN steps into a single layer to +# reduce image size and improve caching. +# This groups enabling modules, adding configs, making files executable and adjusting Apache settings. +COPY common/remoteip-env /usr/sbin +COPY common/remoteip-clear /usr/sbin +COPY common/opcache-preload /usr/sbin +COPY common/opcache-status /usr/sbin +COPY common/apache-status /usr/sbin +COPY common/status.conf /etc/apache2/sites-enabled/000-status.conf +COPY common/healthz.html /var/www/html/healthz.html + +RUN set -eux; \ + a2enmod rewrite remoteip status; \ + touch /etc/apache2/conf-available/remoteip.conf; \ + ln -s /etc/apache2/conf-available/remoteip.conf /etc/apache2/conf-enabled/remoteip.conf || true; \ + chmod +x /usr/sbin/remoteip-* /usr/sbin/opcache-* /usr/sbin/apache-status || true; \ + sed -ri -e 's!AllowOverride None!AllowOverride All!g' /etc/apache2/apache2.conf; \ + sed -ri -e 's!Listen 80!Listen 8181!g' /etc/apache2/ports.conf; \ + sed -ri -e 's!VirtualHost \*:80!VirtualHost \*:8181!g' /etc/apache2/sites-enabled/000-default.conf; \ + echo "ServerSignature off" >> /etc/apache2/apache2.conf; \ + echo "ServerTokens Prod" >> /etc/apache2/apache2.conf; \ + printf '\n\n Listen 8282\n\n' >> /etc/apache2/ports.conf; \ + sed -ri -e 's!#LoadModule remoteip_module!LoadModule remoteip_module!g' /etc/apache2/apache2.conf; \ + sed -ri -e 's/LogFormat .* combined/LogFormat "%a %{c}a %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" %I %O" combined/g' /etc/apache2/apache2.conf + +EXPOSE 8181 +EXPOSE 8282 + + + + + diff --git a/common/apache-status b/common/apache-status new file mode 100644 index 0000000..35bce69 --- /dev/null +++ b/common/apache-status @@ -0,0 +1,12 @@ +#!/bin/bash + +URL="127.0.0.1:8282/server-status?auto" + +if command -v curl >/dev/null 2>&1; then + curl -fsS "$URL" +elif command -v wget >/dev/null 2>&1; then + wget -q -O - "$URL" +else + echo "Error: neither curl nor wget is installed" >&2 + exit 1 +fi diff --git a/common/healthz.html b/common/healthz.html new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/common/healthz.html @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/common/status.conf b/common/status.conf new file mode 100644 index 0000000..57f50da --- /dev/null +++ b/common/status.conf @@ -0,0 +1,18 @@ + + + SetHandler server-status + Require all granted + # Order deny,allow + # Deny from all + # Allow from 127.0.0.1 # Allow access only from localhost + + + DocumentRoot /var/www/html + + Alias /healthz /var/www/html/healthz.html + + ForceType text/plain + + + CustomLog ${APACHE_LOG_DIR}/access.log combined + \ No newline at end of file