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
12 changes: 9 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,24 @@ USERNAME=${USER}
USER_DEV=php-devforge
PASSWD_DEV=php-devforge01

##### DATABASES AND EXTRAS #####
##### DATABASES, PHP VERSIONS AND EXTRAS #####
# Comma separated. Nothing here starts unless listed. Manage with:
# forge db list forge db on pg18 forge mail on
# Available: pg16 pg17 pg18 mariadb11 mariadb12 mail search nginx
COMPOSE_PROFILES=
# forge php list forge php on 8.3
# Available: php83 php84 php85 pg16 pg17 pg18 mariadb11 mariadb12 mail search nginx
#
# The default version below must be listed here, or nothing answers a plain
# host name. `forge php off` refuses to remove it for that reason.
COMPOSE_PROFILES=php84

##### LOCAL DNS PORT #####
# Port on 127.0.0.1 where dnsmasq listens. Not 53: it is usually taken
# (systemd-resolved, Pi-hole). Change it if 5354 is busy on your machine.
DNS_PORT=5354

##### DEFAULT PHP VERSION #####
# Answers host names with no --pNN suffix. Change it with `forge use 8.5`,
# which also installs that version if you do not have it yet.
PHP_VERSION=84

##### HOST USER #####
Expand Down
Binary file modified .github/scripts/__pycache__/drive-install.cpython-314.pyc
Binary file not shown.
Binary file modified .github/scripts/__pycache__/run-step.cpython-314.pyc
Binary file not shown.
5 changes: 4 additions & 1 deletion .github/scripts/drive-install.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@

# What to answer, in order: (text that means the menu is up, what to pick).
# A single-choice menu takes one name; a multi-choice menu takes a list.
# 84 comes preselected, so naming it here toggles it off: the run installs 8.3
# and 8.5 only, which is the point of picking versions at all.
STEPS = [
("Default PHP version", "85"),
("PHP versions", ["84", "83", "85"]),
("Default version", "85"),
("Images", "pull"),
("space toggles", ["pg18", "mail"]),
]
Expand Down
102 changes: 98 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ jobs:
# reproduce the bug: --profiles bypassed validation entirely, so this
# passed even while every typed name was being rejected.
./install.sh --yes --skip-dns --skip-cert --skip-link --profiles=pg18,mail >/dev/null
grep -q '^COMPOSE_PROFILES=pg18,mail$' .env
# The default PHP version leads the line: versions are profiles too.
grep -q '^COMPOSE_PROFILES=php84,pg18,mail$' .env
rm -f .env .env.backup

# a name that does not must stop a scripted install, not write a dead profile
Expand All @@ -108,7 +109,9 @@ jobs:
./install.sh --skip-cert --skip-dns --skip-link --domain=phpforge.dev
grep -q '^PHP_VERSION=85$' .env # moved off the default 8.4
grep -q '^IMAGE_MODE=missing$' .env # kept 'pull'
grep -q '^COMPOSE_PROFILES=pg18,mail$' .env
# 8.4 was toggled off and 8.3/8.5 on, so the versions are a choice and
# the second question decided which of them answers a plain host name.
grep -q '^COMPOSE_PROFILES=php83,php85,pg18,mail$' .env
rm -f .env .env.backup
echo "the menus work end to end"

Expand Down Expand Up @@ -137,7 +140,26 @@ jobs:
# --skip-dns: the runner's resolver is left alone; curl --resolve is used
# instead. Certificates are not skipped, so real TLS is exercised.
- name: run the installer
run: ./install.sh --yes --skip-dns
run: ./install.sh --yes --skip-dns --php=84

# Every version used to be installed whether or not you wanted it: 1.9 GB
# each. What is not picked must not reach the default set at all.
- name: only the PHP versions you picked are installed
run: |
# Not an exact match on the line: --yes also accepts the mail catcher.
grep -qE '^COMPOSE_PROFILES=php84(,|$)' .env \
|| { grep '^COMPOSE_PROFILES=' .env; exit 1; }
if grep '^COMPOSE_PROFILES=' .env | grep -qE 'php(83|85)'; then
echo "a version nobody asked for was enabled"
grep '^COMPOSE_PROFILES=' .env; exit 1
fi
for s in php83dev php85dev; do
if docker compose config --services | grep -qx "$s"; then
echo "$s starts by default and should not"; exit 1
fi
done
docker compose config --services | grep -qx php84dev
echo "one version picked, one version installed"

- name: .env was written
run: |
Expand All @@ -152,7 +174,7 @@ jobs:
test -L "$HOME/.local/bin/forge" || { echo "installer did not link forge"; exit 1; }
export PATH="$HOME/.local/bin:$PATH"
cd /tmp # it must work from anywhere, not just the checkout
forge help | grep -q "8.3 8.4 8.5" || { echo "versions not discovered"; exit 1; }
forge help | grep -q "out of 8.3, 8.4, 8.5" || { echo "versions not discovered"; exit 1; }
forge status | grep -q "PHP DevForge"
forge images status
# unknown versions must be refused, not passed to docker
Expand All @@ -164,6 +186,21 @@ jobs:
# built-in and docker-php-ext-install started failing on it.
run: docker compose build php83dev php84dev php85dev apachedev

# The rest of the run needs all three. Adding them through forge rather than
# by naming services proves the command people are told to use, and keeps
# .env and the containers agreeing -- Apache reads COMPOSE_PROFILES to know
# what exists.
- name: forge php on adds a version
run: |
export PATH="$HOME/.local/bin:$PATH"
forge php on 8.3
forge php on 8.5
for v in 83 84 85; do
grep -q "^COMPOSE_PROFILES=.*php$v" .env || { grep '^COMPOSE_PROFILES=' .env; exit 1; }
done
forge php list | grep -qE '^\s+8\.4\s+ON\s+default'
echo "versions added"

- name: start the stack
run: |
docker compose up -d apachedev php83dev php84dev php85dev
Expand Down Expand Up @@ -255,6 +292,63 @@ jobs:
|| { echo "expected PHP ${v:0:1}.${v:1:1}"; exit 1; }
done

# A version that is not installed used to proxy to a host that does not
# resolve: a bare 503, indistinguishable from a typo. The trap puts 8.3 back
# however this ends, because every step after this one uses it.
- name: an uninstalled version says so instead of failing blankly
run: |
export PATH="$HOME/.local/bin:$PATH"
trap 'forge php on 8.3 >/dev/null 2>&1 || true' EXIT

# the default cannot be removed: nothing would answer a plain host name
out="$(forge php off 8.4 2>&1)" && { echo "removing the default was allowed"; exit 1; }
printf '%s\n' "$out" | grep -q 'forge use' || { echo "$out"; exit 1; }

forge php off 8.3
docker compose ps --services | grep -qx php83dev \
&& { echo "php83dev still running"; exit 1; }

host=my-app--sites--p83.phpforge.dev
code="$(curl -sk -o /tmp/gone.html -w '%{http_code}' \
--resolve "$host:443:127.0.0.1" "https://$host/")"
test "$code" = 503 || { echo "expected 503, got $code"; exit 1; }
# "/" reaches PHP through DirectoryIndex, which is where an earlier
# version of this page was swallowed and served as an empty 200.
grep -q 'PHP 8.3 is not installed' /tmp/gone.html || { cat /tmp/gone.html; exit 1; }
grep -q 'forge php on 8.3' /tmp/gone.html || { cat /tmp/gone.html; exit 1; }
echo "it explains itself"

forge php on 8.3
# -k here only: TLS trust is what several other steps are for, and this
# way the step also runs on a machine that skipped ./install_cert.sh.
for i in $(seq 1 30); do
curl -fsSk --max-time 5 --resolve "$host:443:127.0.0.1" "https://$host/" \
| grep -q 'PHP 8.3' && { echo "8.3 serves again after ${i}s"; exit 0; }
sleep 1
done
echo "8.3 never came back"; exit 1

# Upgrading is a git pull, and a .env written before task 41 lists no php
# profile at all. Dropping to one version would silently break every --pNN
# host that person had.
- name: an older .env keeps the versions it had
run: |
export PATH="$HOME/.local/bin:$PATH"
cp .env /tmp/env.keep
trap 'cp /tmp/env.keep .env' EXIT
sed -i 's/^COMPOSE_PROFILES=.*/COMPOSE_PROFILES=/' .env

out="$(forge status)"
printf '%s\n' "$out" | grep -q 'PHP versions are now picked' \
|| { echo "$out"; echo "no notice printed"; exit 1; }
grep -q '^COMPOSE_PROFILES=php83,php84,php85$' .env \
|| { grep '^COMPOSE_PROFILES=' .env; exit 1; }

# and it says it once: the write is the migration
forge status | grep -q 'PHP versions are now picked' \
&& { echo "the notice repeats"; exit 1; }
echo "older .env kept its versions"

# Neither is installed by us any more: the base images already provide them.
# If that ever stops being true, this catches it instead of a user finding out.
- name: opcache and mbstring are present in every version
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ This is the core mechanism and it spans three files.

1. **DNS** — `dnsmasq` resolves `*.${DEV_DOMAIN}` (default `phpforge.dev`) to `127.0.0.1`. `setup-local-dns.sh` inserts `127.0.0.1` as the system's first nameserver (NetworkManager → systemd-resolved → raw `resolv.conf`, in that order of preference).
2. **Docroot resolution** — `docker-library/httpd/config_files/resolve_docroot.lua` runs as `LuaHookTranslateName`. It strips the `--pNN` suffix and the base domain from the Host header, splits the remainder on `--` (or on `.`), **reverses the segments**, and joins with `/` under `/home/php-devforge/public_html`. So `public--site--laravel.phpforge.dev` → `/home/php-devforge/public_html/laravel/site/public`.
3. **PHP version routing** — `devlocal.conf` / `devlocal_https.conf` set `PHP_VERSION` from the env (via `PassEnv`), then override it per-request with `SetEnvIf Host ".*--p83.*"`. A `<FilesMatch \.php$>` block picks `proxy:fcgi://php83dev:9000` or `php84dev:9000` accordingly. Both PHP containers are always running; the hostname decides which one handles the request.
3. **PHP version routing** — `resolve_docroot.lua` (`set_php_handler`, a `LuaHookFixups`) picks `proxy:fcgi://phpNNdev:9000` from the `--pNN` suffix, falling back to `PHP_VERSION`. Each version is a compose profile (`php83`, `php84`, `php85`) listed in `COMPOSE_PROFILES`, so only the installed ones run; the same variable reaches Apache and nginx as `ENABLED_PROFILES`, and the Lua answers a request for a version that is not installed with a page instead of a bare 503. `COMPOSE_PROFILES` always contains the default version — `forge php off` refuses to remove it, and a `.env` with no `php*` entry is migrated by `forge`.

`docker-library/httpd/config_files/php-fpm.conf` is a legacy/unused fallback — the active handler selection lives in the two vhost files. Both vhosts are duplicates apart from the `SSLEngine` block; **changes to routing must be applied to both**.

Expand Down
43 changes: 35 additions & 8 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ https://mi-app--p83.phpforge.dev # esta petición en PHP 8.3
https://mi-app--p85.phpforge.dev # esta petición en PHP 8.5
```

El mismo código en tres versiones, sin reiniciar y sin cambiar nada. Útil para
comprobar una actualización antes de comprometerte con ella.
El mismo código en las versiones que instalaste, sin reiniciar y sin cambiar nada.
Útil para comprobar una actualización antes de comprometerte con ella.

Todo se sirve por **HTTPS real**, con un certificado en el que tu equipo confía.

Expand All @@ -83,7 +83,7 @@ pensado para tener muchos proyectos sobre un mismo entorno, sin configurar ningu

## 📖 Qué incluye

- **Apache + PHP-FPM 8.3, 8.4 y 8.5**, los tres activos, elegidos por petición
- **Apache + PHP-FPM 8.3, 8.4 y 8.5** — instalas las que quieras, elegidas por petición
- **HTTPS automático** con una CA local de confianza
- **DNS local con comodín** que solo afecta a tu dominio de desarrollo
- **Edición en vivo** — los archivos están montados, no hay que sincronizar nada
Expand Down Expand Up @@ -124,14 +124,16 @@ Y abre **https://welcome.phpforge.dev**.
```bash
./install.sh
```
Pregunta por tu dominio, dónde vivirán tus proyectos y la versión de PHP por
defecto; busca un puerto DNS libre, detecta tu usuario, escribe `.env`, crea la
carpeta de proyectos, y ofrece generar los certificados y configurar el DNS.
Pregunta por tu dominio, dónde vivirán tus proyectos, qué versiones de PHP
instalar y cuál de ellas es la de por defecto; busca un puerto DNS libre, detecta
tu usuario, escribe `.env`, crea la carpeta de proyectos, y ofrece generar los
certificados y configurar el DNS.

Se puede volver a ejecutar sin miedo: tu `.env` actual da los valores por defecto.
Para uso desatendido:
```bash
./install.sh --yes --domain=midominio.dev --projects-dir=~/code
./install.sh --yes --php=84,83 # dos versiones, 8.4 la de por defecto
./install.sh --yes --profiles=pg18,mail # bases de datos y correo, desatendido
./install.sh --help # todas las opciones
```
Expand Down Expand Up @@ -161,7 +163,10 @@ forge restart
forge status # qué corre y cómo está configurado

forge link ~/code/app/public # publicar un proyecto en app.<dominio>
forge use 8.5 # cambiar la versión por defecto

forge php list # versiones de PHP, y cuáles están instaladas
forge php on|off 8.3 # instalar una, o liberar los ~2 GB que ocupa
forge use 8.5 # cambiar la versión por defecto (la instala si falta)
forge shell 8.4 # entrar a un contenedor
forge logs 8.4 # seguir sus logs

Expand Down Expand Up @@ -242,6 +247,25 @@ ln -s ../projects/mi-app/public ~/php-devforge/sites/mi-app
El enlace debe apuntar a la carpeta que contiene el `index.php` (el `public/` del
framework). El `.htaccess` funciona: `AllowOverride All` está activado.

### 🐘 Versiones de PHP

Se eligen al instalar, igual que las bases de datos, porque cada una es una imagen
aparte de unos 2 GB:

```bash
forge php list # 8.3 off / 8.4 ON default / 8.5 off
forge php on 8.3 # descargarla y arrancarla
forge php off 8.3 # pararla; la imagen sigue en disco hasta que la borres
```

Una de ellas es la **de por defecto**: responde a todos los nombres de host sin
sufijo `--pNN`. `forge use 8.5` la cambia, instalando esa versión antes si no la
tienes, y `forge php off` se niega a quitar la de por defecto — si no, ningún
nombre de host normal tendría quién lo atienda.

Pedir una versión que no instalaste devuelve una página que lo explica y dice el
comando para añadirla, en vez de un 503 pelado.

### 🔄 Día a día

- Edita el código en tu editor — los archivos están montados, los cambios son inmediatos
Expand Down Expand Up @@ -407,7 +431,8 @@ las imágenes que arranca ese perfil.

| Perfil | Servicios | Notas |
|---|---|---|
| *(ninguno)* | apachedev, php83dev, php84dev, php85dev, dnsmasq | los levanta `forge start` |
| *(ninguno)* | apachedev, dnsmasq | los levanta `forge start` |
| `php83` `php84` `php85` | php83dev, php84dev, php85dev | se eligen al instalar, o `forge php on 8.3` |
| `pg16` `pg17` `pg18` | postgres | ver la sección Bases de datos y correo |
| `mariadb11` `mariadb12` | mariadb | igual |
| `mail` | mailpit | interfaz en `https://mail.<dominio>` |
Expand Down Expand Up @@ -471,6 +496,8 @@ dentro de un contenedor; tu editor debe escuchar en el puerto **9003**.
adoptan esos ids al arrancar, así que lo que escriben es tuyo.
- **No cambia la versión de PHP**: revisa `PHP_VERSION` en `.env`, o añade
`--p83`/`--p84`/`--p85` al nombre del host. `forge status` muestra la de por defecto.
- **"PHP 8.3 is not installed"**: esa versión no se eligió al instalar.
`forge php on 8.3` la añade; `forge php list` muestra las que tienes.
- **`npm` muestra un aviso sobre pnpm**: es a propósito. npm sigue funcionando; aquí se
prefiere pnpm.
- **Un `.ini` de `custom/php.d/` parece ignorado**: recrea el contenedor con
Expand Down
Loading