Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions .github/workflows/phpfpm.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build and Push Docker Images
name: Build, Test and Push Docker Images

on:
workflow_dispatch:
Expand All @@ -22,6 +22,7 @@ on:
- '.env'
- 'src/**'
- 'support/**'
- '.hadolint.yaml'
- '.github/workflows/phpfpm.yml'
- '!**/*.md'
pull_request:
Expand All @@ -32,6 +33,7 @@ on:
- '.env'
- 'src/**'
- 'support/**'
- '.hadolint.yaml'
- '.github/workflows/phpfpm.yml'
- '!**/*.md'

Expand All @@ -53,6 +55,29 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: gh api -X PUT repos/${{ github.repository }}/actions/workflows/phpfpm.yml/enable

# =============================================================================
# Lint — both Dockerfiles, single required context "hadolint"
# =============================================================================
hadolint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out the repository
uses: actions/checkout@v6

- name: Lint php Dockerfile (hadolint)
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: src/php/Dockerfile
config: .hadolint.yaml

- name: Lint nginx Dockerfile (hadolint)
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: src/nginx/Dockerfile
config: .hadolint.yaml

# =============================================================================
# PHP-FPM Images (Matrix Build)
# =============================================================================
Expand Down Expand Up @@ -87,7 +112,13 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Run smoke tests (amd64/arm64 on test images)
run: make test-all PHP_VERSION=${{ matrix.php_version }}

# Push-Guard: PRs build & smoke-test above but never publish; only
# merge/schedule/dispatch push the moving + immutable date tags.
- name: Build and push PHP-FPM ${{ matrix.php_version }}
if: github.event_name != 'pull_request'
env:
DOCKER_HUB: ${{ secrets.DOCKER_USER_NAME }}
run: |
Expand Down Expand Up @@ -124,8 +155,51 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Run smoke test (nginx config, amd64/arm64)
run: make nginx-test

# Push-Guard: PRs build & smoke-test above but never publish.
- name: Build and push Nginx
if: github.event_name != 'pull_request'
env:
DOCKER_HUB: ${{ secrets.DOCKER_USER_NAME }}
run: make nginx-push DOCKER_HUB=${DOCKER_HUB}

# =============================================================================
# Trivy — non-blocking CVE visibility on the freshly published images
# (base-image CVEs are not self-fixable, so this reports instead of failing).
# continue-on-error keeps the run green even if Trivy itself errors.
# =============================================================================
trivy-report:
if: github.event_name != 'pull_request'
needs: [build-phpfpm, build-nginx]
runs-on: ubuntu-latest
continue-on-error: true
permissions:
contents: read
strategy:
matrix:
image:
- headgent/phpfpm:8.2
- headgent/phpfpm:8.3
- headgent/phpfpm:8.4
- headgent/nginx:1.28
fail-fast: false
steps:
- name: Scan image for HIGH/CRITICAL CVEs
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: ${{ matrix.image }}
format: table
severity: HIGH,CRITICAL
exit-code: '0'
output: trivy-report.txt

- name: Publish report to job summary
run: |
make nginx-push DOCKER_HUB=${DOCKER_HUB}
{
echo "## Trivy CVE report — ${{ matrix.image }} (HIGH/CRITICAL)";
echo '```';
cat trivy-report.txt;
echo '```';
} >> "$GITHUB_STEP_SUMMARY"
12 changes: 12 additions & 0 deletions .hadolint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# hadolint configuration for the phpfpm base image (php-fpm + nginx).
# Every ignore below is a deliberate base-image design decision, not an oversight.
ignored:
# DL3018 — "Pin versions in apk add". The apk packages are Alpine system libraries
# that track the pinned ALPINE_VERSION / nginx-alpine build-arg. Pinning each library
# version individually would be brittle (versions change with every Alpine point
# release) and high-maintenance for a base image whose job is to stay current with
# security patches. Applies to both src/php/Dockerfile and src/nginx/Dockerfile.
- DL3018
# DL4006 — "Set pipefail before RUN with a pipe". The only pipe is `yes "" | pecl install`;
# the left side (`yes`) cannot meaningfully fail, so pipefail adds no safety here.
- DL4006
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export
include ./support/makefiles/docker.helper.mk
include ./support/makefiles/docker.build.local.mk
include ./support/makefiles/docker.build.push.mk
include ./support/makefiles/test.mk
include ./support/makefiles/ssh.mk

# ---------------------------------------------------------------------------
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ Modern PHP applications demand more than basic runtime environments. **Jardis PH
|-----|---------------|--------|
| `1.28`, `latest` | Nginx 1.28 | Current |

### Moving vs. Immutable Tags

Each publish produces two kinds of tags:

- **Moving tags** — `8.4`, `latest`, `1.28` — always point to the newest rebuild of that line. They pick up Alpine and base-image security patches automatically, so the content behind a moving tag changes over time.
- **Immutable date tags** — `8.4-YYYYMMDD`, `1.28-YYYYMMDD` (e.g. `8.4-20260614`) — are added on every publish and are never moved to a different build. They let you pin a reproducible image and roll back to a known-good state.

**Recommendation:** Pin an immutable date tag in production deployments for reproducible builds and deterministic rollback; use the moving tags where you want patches applied automatically (e.g. local development).

**Published under the `headgent/` Docker Hub namespace:**
- https://hub.docker.com/r/headgent/phpfpm
- https://hub.docker.com/r/headgent/nginx
Expand Down Expand Up @@ -313,9 +322,17 @@ make build-and-push-all
| `make phpfpm-build-all` | Build PHP-FPM (all versions) |
| `make nginx-build` | Build Nginx image |
| `make build-all` | Build all images locally |
| `make phpfpm-push` | Push PHP-FPM (current version) |
| `make phpfpm-push` | Push PHP-FPM (current version, + immutable date tag) |
| `make build-and-push-all` | Build and push all images |
| `make build-cache-delete` | Clear buildx cache |
| `make test-all` | Run php-fpm smoke tests (FPM boot, extensions, OPcache/JIT; amd64+arm64) |
| `make nginx-test` | Validate the rendered nginx config (`nginx -t`; amd64+arm64) |
| `make test-fpm-start` | Smoke test: php-fpm boots and answers `/ping` |
| `make test-extensions` | Smoke test: all expected PHP extensions loaded |
| `make test-opcache` | Smoke test: OPcache active and JIT enabled |

> Smoke tests build per-arch test images and run them on `amd64` + `arm64`. For a fast
> native-only run, override the platform list, e.g. `make test-all TEST_PLATFORMS=arm64`.

---

Expand Down
2 changes: 1 addition & 1 deletion src/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ RUN apk add --no-cache \
xdebug-${XDEBUG_VERSION} \
pcov-${PCOV_VERSION} \
&& docker-php-ext-enable apcu redis amqp rdkafka xdebug pcov \
&& docker-php-ext-enable opcache || true
&& docker-php-ext-enable opcache

# --------------------
# Runtime stage
Expand Down
14 changes: 12 additions & 2 deletions src/php/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ EOF

echo "info: Runtime configuration generated"

# Privilege drop and execute
[ "$(id -u)" = "0" ] && exec su-exec "$APP_USER" "$@"
# Privilege drop and execute.
# The container starts as root only for one-time init: align APP_USER's UID/GID
# with a bind-mounted /app (dev) and hand the container's stdout/stderr to
# APP_USER. The latter matters because php-fpm reopens the global error_log
# (/proc/self/fd/2, initially root-owned) AFTER the drop — without chowning the
# stdio pipes, FPM init fails with "failed to open error_log: Permission denied".
# With them chowned, the php-fpm master AND its workers run as APP_USER; nothing
# stays root. (Brief root-for-init is the standard Docker pattern, like gosu.)
if [ "$(id -u)" = "0" ]; then
chown "$APP_USER:$APP_USER" /proc/self/fd/1 /proc/self/fd/2 || true
exec su-exec "$APP_USER" "$@"
fi
exec "$@"
10 changes: 6 additions & 4 deletions support/makefiles/docker.build.push.mk
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ phpfpm-push: buildx-builder-create .check-docker-login ## Build and push PHP-FPM
--build-arg FPM_PM_MAX_SPARE_SERVERS=$(FPM_PM_MAX_SPARE_SERVERS) \
--build-arg FPM_PM_MAX_REQUESTS=$(FPM_PM_MAX_REQUESTS) \
-t $(PHPFPM_IMAGE):$(PHP_VERSION) \
-t $(PHPFPM_IMAGE):$(PHP_VERSION)-$(IMAGE_DATE) \
$(if $(filter $(PHP_VERSION),$(PHP_LATEST)),-t $(PHPFPM_IMAGE):latest) \
-f ./src/php/Dockerfile \
./src/php \
$(BUILD_EXTRA_FLAGS)
@echo "✅ Pushed $(PHPFPM_IMAGE):$(PHP_VERSION)"
@echo "✅ Pushed $(PHPFPM_IMAGE):$(PHP_VERSION) (+ :$(PHP_VERSION)-$(IMAGE_DATE))"
.PHONY: phpfpm-push

phpfpm-push-all: buildx-builder-create .check-docker-login ## Build and push PHP-FPM images for all PHP versions (multi-arch)
Expand Down Expand Up @@ -103,11 +104,11 @@ phpfpm-push-all: buildx-builder-create .check-docker-login ## Build and push PHP
--build-arg FPM_PM_MIN_SPARE_SERVERS=$(FPM_PM_MIN_SPARE_SERVERS) \
--build-arg FPM_PM_MAX_SPARE_SERVERS=$(FPM_PM_MAX_SPARE_SERVERS) \
--build-arg FPM_PM_MAX_REQUESTS=$(FPM_PM_MAX_REQUESTS) \
-t $(PHPFPM_IMAGE):$$v $$LATEST_TAG \
-t $(PHPFPM_IMAGE):$$v -t $(PHPFPM_IMAGE):$$v-$(IMAGE_DATE) $$LATEST_TAG \
-f ./src/php/Dockerfile \
./src/php \
$(BUILD_EXTRA_FLAGS); \
echo "✅ Pushed $(PHPFPM_IMAGE):$$v"; \
echo "✅ Pushed $(PHPFPM_IMAGE):$$v (+ :$$v-$(IMAGE_DATE))"; \
done
.PHONY: phpfpm-push-all

Expand All @@ -124,11 +125,12 @@ nginx-push: buildx-builder-create .check-docker-login ## Build and push Nginx im
--build-arg PUID=$(PUID) \
--build-arg PGID=$(PGID) \
-t $(NGINX_IMAGE):$(WEBSERVER_VERSION) \
-t $(NGINX_IMAGE):$(WEBSERVER_VERSION)-$(IMAGE_DATE) \
-t $(NGINX_IMAGE):latest \
-f ./src/nginx/Dockerfile \
./src/nginx \
$(BUILD_EXTRA_FLAGS)
@echo "✅ Pushed $(NGINX_IMAGE):$(WEBSERVER_VERSION)"
@echo "✅ Pushed $(NGINX_IMAGE):$(WEBSERVER_VERSION) (+ :$(WEBSERVER_VERSION)-$(IMAGE_DATE))"
.PHONY: nginx-push

# ---------------------------------------------------------------------------
Expand Down
8 changes: 8 additions & 0 deletions support/makefiles/docker.helper.mk
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ PHP_LATEST ?= 8.4
PHPFPM_IMAGE = $(DOCKER_HUB)/phpfpm
NGINX_IMAGE = $(DOCKER_HUB)/nginx

# ---------------------------------------------------------------------------
# Immutable date tag (UTC), added alongside the moving :<ver>/:latest tags in
# the push targets only. Lets consumers pin a reproducible image for rollback
# (e.g. headgent/phpfpm:8.4-20260614, headgent/nginx:1.28-20260614) while the
# moving tags stay current for patch hygiene. Override for reproducible re-tags.
# ---------------------------------------------------------------------------
IMAGE_DATE ?= $(shell date -u +%Y%m%d)

# ---------------------------------------------------------------------------
# Build Flags (can be overridden via command line)
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading