Skip to content

Commit 5ac8d44

Browse files
authored
Merge pull request #4 from zelti/fix/php85-opcache
Fix/php85 opcache
2 parents cd54dc2 + 740ce7c commit 5ac8d44

16 files changed

Lines changed: 704 additions & 198 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
paths-ignore: ["**.md", "LICENSE", "logo.png"]
7+
pull_request:
8+
paths-ignore: ["**.md", "LICENSE", "logo.png"]
9+
workflow_dispatch:
10+
11+
jobs:
12+
lint:
13+
name: Shell and Dockerfile lint
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
18+
- name: shellcheck
19+
run: |
20+
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
21+
shellcheck --severity=warning \
22+
install.sh install_cert.sh setup-local-dns.sh aliases.bash \
23+
docker-library/php/config_files/docker-php-entrypoint \
24+
docker-library/php/config_files/xdebug \
25+
docker-library/httpd/config_files/docker-entrypoint.sh
26+
27+
- name: hadolint
28+
uses: hadolint/hadolint-action@v3.1.0
29+
with:
30+
dockerfile: docker-library/php/Dockerfile
31+
failure-threshold: error
32+
33+
install:
34+
name: Install and serve on a clean machine
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 40
37+
steps:
38+
- uses: actions/checkout@v5
39+
40+
- name: compose file parses
41+
run: |
42+
cp .env.example .env
43+
docker compose config >/dev/null
44+
# Optional services live behind profiles instead of being commented
45+
# out, so they stay parsed and cannot rot unnoticed the way the old
46+
# nginx block did.
47+
docker compose --profile search --profile tools config >/dev/null
48+
echo "default services:"
49+
docker compose config --services
50+
echo "with every profile:"
51+
docker compose --profile search --profile tools config --services
52+
53+
- name: profiles do not leak into the default set
54+
run: |
55+
for s in es8143dev kibana mkcert; do
56+
if docker compose config --services | grep -qx "$s"; then
57+
echo "$s starts by default and should not"; exit 1
58+
fi
59+
done
60+
echo "optional services stay optional"
61+
62+
# --skip-dns: the runner's resolver is left alone; curl --resolve is used
63+
# instead. Certificates are not skipped, so real TLS is exercised.
64+
- name: run the installer
65+
run: ./install.sh --yes --skip-dns
66+
67+
- name: .env was written
68+
run: |
69+
grep -q '^PROJECTS_DIR=' .env
70+
grep -q "^PUID=$(id -u)$" .env
71+
grep -q "^PGID=$(id -g)$" .env
72+
73+
- name: build images
74+
# Every PHP version is built. 8.5 broke silently once: opcache became
75+
# built-in and docker-php-ext-install started failing on it.
76+
run: docker compose build php83dev php84dev php85dev apachedev
77+
78+
- name: start the stack
79+
run: |
80+
docker compose up -d apachedev php83dev php84dev php85dev
81+
docker compose ps
82+
# "running" only means the process started. The PHP entrypoints still
83+
# have setup to finish, and Apache serves in parallel, so wait for an
84+
# actual answer instead.
85+
for i in $(seq 1 60); do
86+
if curl -fsS --max-time 5 --resolve welcome--sites.phpforge.dev:443:127.0.0.1 \
87+
https://welcome--sites.phpforge.dev/ >/dev/null 2>&1; then
88+
echo "ready after ${i}s"; exit 0
89+
fi
90+
sleep 1
91+
done
92+
echo "still not serving after 60s"
93+
docker compose logs --tail=30
94+
exit 1
95+
96+
- name: create a project with a hyphen in its name
97+
run: |
98+
DIR="$(grep '^PROJECTS_DIR=' .env | cut -d= -f2-)"
99+
mkdir -p "$DIR/projects/my-app/public"
100+
printf '<?php echo "my-app on PHP ".PHP_VERSION."\\n";' \
101+
> "$DIR/projects/my-app/public/index.php"
102+
ln -sfn ../projects/my-app/public "$DIR/sites/my-app"
103+
104+
- name: diagnostics
105+
if: failure()
106+
run: |
107+
echo "--- PROJECTS_DIR in .env ---"
108+
grep '^PROJECTS_DIR=' .env
109+
echo "--- mount as compose resolves it ---"
110+
docker compose config | grep -A3 'public_html' | head -12
111+
echo "--- on the host ---"
112+
DIR="$(grep '^PROJECTS_DIR=' .env | cut -d= -f2-)"
113+
ls -la "$DIR" "$DIR/sites" "$DIR/sites/welcome" 2>&1 | head -30
114+
echo "--- inside apachedev ---"
115+
docker compose exec -T apachedev ls -la /home/php-devforge/ /home/php-devforge/public_html/ 2>&1 | head -20
116+
echo "--- DEV_DOMAIN inside apachedev ---"
117+
docker compose exec -T apachedev printenv DEV_DOMAIN PHP_VERSION
118+
echo "--- what the response actually is ---"
119+
curl -sv --resolve welcome--sites.phpforge.dev:443:127.0.0.1 \
120+
https://welcome--sites.phpforge.dev/ 2>&1 | tail -25
121+
122+
- name: the welcome page is served over HTTPS
123+
run: |
124+
curl -fsS --resolve welcome--sites.phpforge.dev:443:127.0.0.1 \
125+
https://welcome--sites.phpforge.dev/ | tee /tmp/welcome.txt
126+
grep -q "PHP DevForge is running" /tmp/welcome.txt
127+
128+
# Hyphens in project names were broken for a long time: mi-app--sites
129+
# resolved to sites/app/mi.
130+
- name: a hyphenated project name resolves
131+
run: |
132+
curl -fsS --resolve my-app--sites.phpforge.dev:443:127.0.0.1 \
133+
https://my-app--sites.phpforge.dev/ | tee /tmp/app.txt
134+
grep -q "my-app on PHP 8.4" /tmp/app.txt
135+
136+
- name: the URL suffix selects the PHP version
137+
run: |
138+
for v in 83 84 85; do
139+
host="my-app--sites--p${v}.phpforge.dev"
140+
got="$(curl -fsS --resolve "$host:443:127.0.0.1" "https://$host/")"
141+
echo "$got"
142+
printf '%s' "$got" | grep -q "PHP ${v:0:1}.${v:1:1}" \
143+
|| { echo "expected PHP ${v:0:1}.${v:1:1}"; exit 1; }
144+
done
145+
146+
- name: opcache is present in every version
147+
run: |
148+
for s in php83dev php84dev php85dev; do
149+
got="$(docker compose exec -T "$s" php -r 'echo extension_loaded("Zend OPcache")?"yes":"no";')"
150+
test "$got" = "yes" || { echo "$s: opcache missing"; exit 1; }
151+
echo "$s ok"
152+
done
153+
154+
# This one leaked source for every request that did not go through
155+
# DirectoryIndex. Guard it so it cannot come back.
156+
- name: PHP source is never served as text
157+
run: |
158+
for host in my-app--sites my-app--sites--p83 my-app--sites--p99; do
159+
body="$(curl -sk --resolve $host.phpforge.dev:443:127.0.0.1 \
160+
https://$host.phpforge.dev/index.php || true)"
161+
if printf '%s' "$body" | grep -q '<?php'; then
162+
echo "SOURCE LEAK on $host"; exit 1
163+
fi
164+
done
165+
body="$(curl -s http://127.0.0.1/index.php -H 'Host: my-app--sites.phpforge.dev' || true)"
166+
printf '%s' "$body" | grep -q '<?php' && { echo "SOURCE LEAK over plain HTTP"; exit 1; }
167+
echo "no leaks"
168+
169+
# Files written by the container must belong to the host user, or
170+
# node_modules cannot be deleted without sudo.
171+
- name: files created in the container belong to the host user
172+
run: |
173+
DIR="$(grep '^PROJECTS_DIR=' .env | cut -d= -f2-)"
174+
printf '<?php file_put_contents(__DIR__."/written.txt","x");' \
175+
> "$DIR/projects/my-app/public/write.php"
176+
curl -fsS --resolve my-app--sites.phpforge.dev:443:127.0.0.1 \
177+
https://my-app--sites.phpforge.dev/write.php >/dev/null
178+
owner="$(stat -c %u "$DIR/projects/my-app/public/written.txt")"
179+
test "$owner" = "$(id -u)" || { echo "owned by $owner, expected $(id -u)"; exit 1; }
180+
rm -f "$DIR/projects/my-app/public/written.txt"
181+
182+
# Apache does not forward the Authorization header to FastCGI by itself;
183+
# php-fpm.conf adds it back. Removing that line breaks every Bearer token
184+
# and JWT silently, with nothing else looking wrong.
185+
- name: the Authorization header reaches PHP
186+
run: |
187+
DIR="$(grep '^PROJECTS_DIR=' .env | cut -d= -f2-)"
188+
printf '<?php echo $_SERVER["HTTP_AUTHORIZATION"] ?? "MISSING";' \
189+
> "$DIR/sites/welcome/auth.php"
190+
got="$(curl -fsS -H 'Authorization: Bearer ci-token' \
191+
--resolve welcome--sites.phpforge.dev:443:127.0.0.1 \
192+
https://welcome--sites.phpforge.dev/auth.php)"
193+
test "$got" = "Bearer ci-token" || { echo "got '$got'"; exit 1; }
194+
195+
- name: hidden .php files are not served
196+
run: |
197+
DIR="$(grep '^PROJECTS_DIR=' .env | cut -d= -f2-)"
198+
printf '<?php echo "should not be reachable";' > "$DIR/sites/welcome/.php"
199+
code="$(curl -sk -o /dev/null -w '%{http_code}' \
200+
--resolve welcome--sites.phpforge.dev:443:127.0.0.1 \
201+
https://welcome--sites.phpforge.dev/.php)"
202+
rm -f "$DIR/sites/welcome/.php"
203+
test "$code" = "403" || { echo "expected 403, got $code"; exit 1; }
204+
205+
# Everything above proves the stack works, using curl --resolve. From here
206+
# the system resolver is configured for real, so a failure below points at
207+
# setup-local-dns.sh and nothing else.
208+
- name: configure the system DNS
209+
run: |
210+
./setup-local-dns.sh
211+
./setup-local-dns.sh --status
212+
213+
- name: the domain resolves through the system resolver
214+
run: |
215+
got="$(getent hosts welcome--sites.phpforge.dev | awk '{print $1}')"
216+
test "$got" = "127.0.0.1" || { echo "got '$got', expected 127.0.0.1"; exit 1; }
217+
# A wildcard, so any subdomain must work without being listed anywhere.
218+
got="$(getent hosts anything--at--all.phpforge.dev | awk '{print $1}')"
219+
test "$got" = "127.0.0.1" || { echo "wildcard failed: '$got'"; exit 1; }
220+
221+
- name: the rest of DNS is untouched
222+
run: |
223+
for h in github.com debian.org; do
224+
got="$(getent hosts $h | awk '{print $1; exit}')"
225+
test -n "$got" || { echo "$h does not resolve"; exit 1; }
226+
echo "$h -> $got"
227+
done
228+
229+
# The old script wrote Domains=~., sending every query on the machine
230+
# through the container. Stopping the stack then killed all DNS.
231+
- name: internet still works with dnsmasq stopped
232+
run: |
233+
docker compose stop dnsmasq
234+
sudo resolvectl flush-caches || true
235+
for h in github.com debian.org; do
236+
got="$(getent hosts $h | awk '{print $1; exit}')"
237+
test -n "$got" || { echo "FAIL: $h stopped resolving"; exit 1; }
238+
done
239+
echo "internet survived"
240+
docker compose start dnsmasq
241+
sleep 3
242+
243+
# No --resolve here: this is the path a browser actually takes.
244+
- name: serve over the real DNS path
245+
run: |
246+
for i in $(seq 1 20); do
247+
curl -fsS --max-time 5 https://welcome--sites.phpforge.dev/ >/dev/null 2>&1 && break
248+
sleep 1
249+
done
250+
curl -fsS https://welcome--sites.phpforge.dev/ | grep -q "PHP DevForge is running"
251+
curl -fsS https://my-app--sites.phpforge.dev/ | grep -q "my-app on PHP 8.4"
252+
curl -fsS https://my-app--sites--p83.phpforge.dev/ | grep -q "PHP 8.3"
253+
254+
- name: --remove undoes it
255+
run: |
256+
./setup-local-dns.sh --remove
257+
test ! -f /etc/systemd/resolved.conf.d/50-php-devforge.conf
258+
getent hosts github.com >/dev/null || { echo "DNS broken after removal"; exit 1; }
259+
echo "clean"
260+
261+
- name: container logs on failure
262+
if: failure()
263+
run: docker compose logs --tail=50

‎.github/workflows/publish.yml‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Publish images
2+
3+
# Only from main, so a branch cannot overwrite what people are pulling.
4+
on:
5+
push:
6+
branches: [main]
7+
paths:
8+
- "docker-library/**"
9+
- ".github/workflows/publish.yml"
10+
workflow_dispatch:
11+
12+
env:
13+
REGISTRY: ghcr.io
14+
15+
jobs:
16+
publish:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
packages: write
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
include:
25+
- name: php
26+
context: ./docker-library/php
27+
args: PHP_TAG=8.3
28+
tag: php:8.3-dev
29+
- name: php
30+
context: ./docker-library/php
31+
args: PHP_TAG=8.4
32+
tag: php:8.4-dev
33+
- name: php
34+
context: ./docker-library/php
35+
args: PHP_TAG=8.5
36+
tag: php:8.5-dev
37+
- name: apache
38+
context: ./docker-library/httpd
39+
args: ""
40+
tag: apache:dev
41+
- name: mkcert
42+
context: ./docker-library/mkcert
43+
args: ""
44+
tag: mkcert:dev
45+
steps:
46+
- uses: actions/checkout@v5
47+
48+
- uses: docker/setup-qemu-action@v3
49+
- uses: docker/setup-buildx-action@v3
50+
51+
# GITHUB_TOKEN is enough; no secrets to manage.
52+
- uses: docker/login-action@v3
53+
with:
54+
registry: ${{ env.REGISTRY }}
55+
username: ${{ github.actor }}
56+
password: ${{ secrets.GITHUB_TOKEN }}
57+
58+
- name: build and push
59+
uses: docker/build-push-action@v6
60+
with:
61+
context: ${{ matrix.context }}
62+
# arm64 covers Apple Silicon, which would otherwise build locally.
63+
platforms: linux/amd64,linux/arm64
64+
push: true
65+
tags: ${{ env.REGISTRY }}/${{ github.repository }}/${{ matrix.tag }}
66+
build-args: ${{ matrix.args }}
67+
cache-from: type=gha,scope=${{ matrix.tag }}
68+
cache-to: type=gha,mode=max,scope=${{ matrix.tag }}

‎.hadolint.yaml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Rules deliberately not applied. Everything else must pass.
2+
ignored:
3+
# DL3008 - pin apt versions.
4+
# Pinning Debian package versions makes the build fail as soon as the archive
5+
# moves on, which for a dev image costs more than it protects.
6+
- DL3008
7+
# SC2016 - expressions do not expand in single quotes.
8+
# Intentional: the nvm snippet is written literally into /etc/profile.d so the
9+
# variable expands when a shell sources it, not while building.
10+
- SC2016
11+
# SC1091 - not following a sourced file.
12+
# /etc/profile.d/nvm.sh is created earlier in the same build; the linter cannot
13+
# see it.
14+
- SC1091

‎README.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,24 @@ forge:logs:php84
144144
- Use Xdebug for debugging (configure your IDE to listen on port **9003**, the Xdebug 3 default)
145145
- Access PHP containers via `forge:exec:php84` (or `forge:exec:php83` for PHP 8.3)
146146
147+
## 🧩 Optional Services
148+
149+
Some services are not started by default. They carry a compose `profile`, so you
150+
ask for them when you want them:
151+
152+
```bash
153+
docker compose --profile search up -d # Elasticsearch + Kibana
154+
```
155+
156+
| Profile | Services | Notes |
157+
|---|---|---|
158+
| *(none)* | apachedev, php83dev, php84dev, dnsmasq, postgres16dev | started by `docker compose up -d` |
159+
| `search` | es8143dev, kibana | Kibana on `127.0.0.1:5601` |
160+
| `tools` | mkcert | used by `install_cert.sh`; not a long-running service |
161+
162+
They are real service definitions rather than commented-out YAML, so
163+
`docker compose config` and CI keep checking them and they cannot quietly break.
164+
147165
## 🛠️ Supported PHP Extensions and Tools
148166

149167
### 📦 PHP Extensions
@@ -204,7 +222,7 @@ Plain PHP app → `plain-php--site.phpforge.dev`
204222
- **PHP version not switching**: Use the aliases `forge:use:php83` or `forge:use:php84` to change the default version, or append `--p83`/`--p84` to the URL.
205223
- **Aliases not working**: Ensure you sourced `aliases.bash` or added it to your `~/.bashrc`.
206224

207-
For more help, check the logs with `docker-compose logs` or create an issue on GitHub.
225+
For more help, check the logs with `docker compose logs` or create an issue on GitHub.
208226

209227
## 🤝 Contributing
210228

0 commit comments

Comments
 (0)