Skip to content

Commit cd54dc2

Browse files
authored
Merge pull request #3 from zelti/feat/installer
Feat/installer
2 parents b313999 + 4c2ee29 commit cd54dc2

10 files changed

Lines changed: 442 additions & 97 deletions

File tree

.env renamed to .env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
# Copy to .env, or let ./install.sh generate it for you.
2+
# .env is gitignored: it holds your machine's own values.
3+
14
COMPOSE_PROJECT_NAME=php-devforge
25
COMPOSE_FILE=docker-compose.yml:docker-compose.override.yml
36

47
#### domain ####
58
DEV_DOMAIN=phpforge.dev
69

10+
##### WHERE YOUR PROJECTS LIVE (on the host) #####
11+
# Mounted at /home/php-devforge/public_html inside the containers, which is where
12+
# Apache and PHP look. Only this side changes; the container path is fixed.
13+
# Symlinks must point INSIDE this folder, or the container cannot follow them.
14+
PROJECTS_DIR=${HOME}/php-devforge
15+
716
##### USER #####
817
USERNAME=${USER}
918

@@ -19,6 +28,12 @@ DNS_PORT=5354
1928
##### DEFAULT PHP VERSION #####
2029
PHP_VERSION=84
2130

31+
##### HOST USER #####
32+
# The PHP containers adopt this uid/gid, so files they create are yours and
33+
# node_modules can be deleted without sudo. Check yours with: id -u ; id -g
34+
PUID=1000
35+
PGID=1000
36+
2237
##### NODE VERSION (built into the PHP images) #####
2338
# Changing this needs a rebuild: docker compose build php83dev php84dev
2439
NODE_VERSION=24

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
/certificates/*
44
!/certificates/.gitkeep
55
/.caroot
6+
7+
/.env

‎README.md‎

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,53 +32,41 @@ The environment automatically generates URLs based on your project folder struct
3232

3333
1. **Clone the repository:**
3434
```bash
35-
git clone git@github.com:zelti/php-devforge.git $HOME/php-devforge-config
36-
cd $HOME/php-devforge-config
35+
git clone git@github.com:zelti/php-devforge.git
36+
cd php-devforge
3737
```
38-
It’s recommended to clone the project inside your home directory.
39-
This ensures the provided bash alias will work correctly.
38+
Clone it wherever you like: the scripts and aliases resolve their own location.
4039

41-
2. **Configure environment variables:**
42-
Edit the `.env` file to customize:
43-
- `DEV_DOMAIN`: Your development domain (default: phpforge.dev)
44-
- `PHP_VERSION`: Default PHP version (83 for 8.3, 84 for 8.4)
45-
- `USERNAME`: Your system username
40+
2. **Run the installer:**
41+
```bash
42+
./install.sh
43+
```
44+
It asks for your domain, where your projects should live and the default PHP
45+
version, finds a free DNS port, detects your user id, writes `.env`, creates the
46+
projects folder, and offers to generate the certificates and configure local DNS.
4647

47-
3. **Install SSL certificates:**
48+
Safe to re-run: your current `.env` supplies the defaults. For unattended use:
4849
```bash
49-
./install_cert.sh
50+
./install.sh --yes --domain=mydomain.dev --projects-dir=~/code
51+
./install.sh --help # every option
5052
```
51-
This script:
52-
- Runs mkcert **inside a Docker container** — nothing is installed on your machine
53-
- Generates wildcard SSL certificates for `*.phpforge.dev` into `certificates/`
54-
- Saves the Certificate Authority (CA) in `.caroot/`, and reuses it on every later run
55-
- Installs that CA in your system trust store, and in Firefox/Chrome when `certutil` is available
5653

57-
It will ask for your password once: adding the CA to the system trust store is the
58-
only step that cannot happen inside a container, because a container has its own
59-
trust store. You will not be asked again on later runs.
54+
Nothing needs `sudo` except the DNS step, and only if you accept it.
6055

61-
4. **Setup local DNS:**
56+
3. **Load the shortcuts (optional):**
6257
```bash
63-
./setup-local-dns.sh
58+
source aliases.bash
6459
```
65-
This configures your system to resolve `*.phpforge.dev` domains to `127.0.0.1`.
66-
67-
5. **Source the aliases (optional but recommended):**
68-
```bash
69-
source aliases.bash
70-
```
71-
Or add the following to your `~/.bashrc` for permanent availability:
72-
```bash
73-
source $HOME/php-devforge-config/aliases.bash
74-
```
60+
Or add that line to your `~/.bashrc`, using the full path to this folder.
7561

76-
6. **Start the containers:**
62+
4. **Start the containers:**
7763
```bash
78-
docker-compose up -d
64+
docker compose up -d
7965
```
8066
(If you sourced the aliases, you can use `forge:start` instead)
8167

68+
Check it works: `https://welcome--sites.phpforge.dev`
69+
8270
## 💻 Usage
8371

8472
### ▶️ Starting and Stopping
@@ -118,11 +106,25 @@ forge:logs:php84
118106
### 🌐 Accessing Your Projects
119107

120108
1. **Create project structure:**
121-
Place your PHP projects in `/home/php-devforge/public_html/`
122-
it will create this folder on your local you will need to change the folder user and group manually for first time
109+
Your projects live in the folder you chose during install (`PROJECTS_DIR` in
110+
`.env`, `~/php-devforge` by default). The installer creates it with:
111+
123112
```
124-
sudo chown yourUser:www-data -R /home/php-devforge
125-
```
113+
~/php-devforge/
114+
├── projects/ your actual code
115+
└── sites/ one symlink per project, for shorter URLs
116+
└── welcome/ a test page
117+
```
118+
119+
Inside the containers this is always `/home/php-devforge/public_html`, which is
120+
where Apache and PHP look. Only the host side is configurable.
121+
122+
**Symlinks must point inside `PROJECTS_DIR`.** The containers only see that
123+
folder, so a link to anywhere else resolves to nothing and returns 404.
124+
125+
No `chown` is needed. The PHP containers adopt your user id at startup
126+
(`PUID`/`PGID` in `.env`), so files they create belong to you and `node_modules`
127+
can be deleted without `sudo`.
126128
127129
2. **URL Structure:**
128130
Projects are accessible via automatically generated URLs:
@@ -198,7 +200,7 @@ Plain PHP app → `plain-php--site.phpforge.dev`
198200
- **DNS resolution not working**: Ensure you ran `./setup-local-dns.sh` and restarted your browser or system. You may need to flush DNS cache.
199201
- **SSL certificate not trusted**: Make sure you ran `./install_cert.sh` and restart your browser afterwards. Check the CA is present with `openssl verify -CAfile .caroot/rootCA.pem certificates/php-devforge.pem`. Firefox keeps its own trust store on Linux, so install `nss` (Arch) or `libnss3-tools` (Debian/Ubuntu) and re-run the script if Firefox still complains.
200202
- **Containers not starting**: Check that Docker and Docker Compose are installed and running. Ensure ports 80 and 443 are not in use by other services.
201-
- **Permission issues with public_html**: Run `sudo chown yourUser:www-data -R /home/php-devforge` to set correct ownership.
203+
- **Permission issues with public_html**: Check that `PUID`/`PGID` in `.env` match your own ids (`id -u`, `id -g`), then recreate the containers with `docker compose up -d --force-recreate`. The containers adopt those ids on startup, so files they write are owned by you.
202204
- **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.
203205
- **Aliases not working**: Ensure you sourced `aliases.bash` or added it to your `~/.bashrc`.
204206

‎TASKS.md‎

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,50 @@ approach.
216216

217217
## 💬 Needs discussion before implementing
218218

219-
### 7. Interactive installer 💬 DISCUSS FIRST
219+
### 7. Interactive installer — DONE
220+
221+
- [x] **Implemented as `install.sh`**
222+
223+
Asks for domain, projects folder and default PHP version; finds a free DNS port;
224+
detects `PUID`/`PGID` with `id -u`/`id -g`; writes `.env`; creates the projects
225+
folder; and offers to run the certificate and DNS steps. Re-running is safe: the
226+
current `.env` supplies the defaults, and the old one is kept as `.env.backup`.
227+
Flags for unattended use: `--domain=`, `--projects-dir=`, `--php=`, `--dns-port=`,
228+
`--yes`, `--skip-cert`, `--skip-dns`. With no terminal it refuses to run rather
229+
than silently accepting defaults.
230+
231+
**Config split:** `.env` is now generated and gitignored; `.env.example` is the
232+
tracked template. This also fixes `forge:use:php83` dirtying the git tree on every
233+
PHP version switch, and stops personal values (domain, uid, paths) being committed.
234+
Anyone with an existing clone will see `.env` drop out of version control; their
235+
local file is untouched.
236+
237+
**Projects folder:** `PROJECTS_DIR` on the host, `~/php-devforge` by default,
238+
mounted at the fixed `/home/php-devforge/public_html` inside the containers — so
239+
Apache, the Lua resolver and PHP need no changes. Only 2 lines in compose. The
240+
installer creates `projects/` for code and `sites/` for symlinks, plus a
241+
`sites/welcome` test page. No `sudo` at any point, thanks to task 8.
242+
243+
**Bug found while testing — hyphens in project names were broken.** The Lua split
244+
the host with `[^%-%-]+`, which is "not a hyphen", not "split on `--`". So:
245+
246+
| Host | Resolved to | |
247+
|---|---|---|
248+
| `mi-app--sites` | `sites/app/mi` | wrong |
249+
| `laravel-app--sites` | `sites/app/laravel` | wrong |
250+
| `miapp--sites` | `sites/miapp` | fine |
251+
252+
**All three examples in the README** (`laravel-app`, `symfony-app`, `plain-php`)
253+
were broken, and had been all along. Fixed by replacing `--` with a sentinel that
254+
cannot occur in a hostname before splitting. Verified: `mi-app--sites`,
255+
`laravel-app--sites`, `a--b--c` and `mi-app--sites--p83` all resolve correctly.
220256

221-
- [ ] **Agree on the approach** (questions below), then implement
257+
**README updated:** installer flow, the new folder layout, the warning that
258+
symlinks must point inside `PROJECTS_DIR`, and the clone path is no longer forced
259+
(aliases resolve their own location since task 3).
260+
261+
<details>
262+
<summary>Original open questions (kept for the record)</summary>
222263

223264
Goal: replace the manual README steps with `./install.sh`, which asks for the
224265
settings and does the setup.
@@ -267,12 +308,66 @@ container and does not need to change:
267308
```
268309
269310
The same idea applies to ports 80 and 443.
311+
312+
</details>
270313
- `aliases.bash` hardcodes `$HOME/php-devforge-config`. Should the installer generate
271314
the aliases file with the real path, or should the aliases derive it themselves?
272315
273-
### 8. Container user / file ownership 💬 DISCUSS FIRST
274-
275-
- [ ] **Choose an option** (A / B / C below), then implement
316+
### 8. Container user / file ownership — DONE
317+
318+
- [x] **Implemented: the container adopts the host uid at startup (PUID/PGID)**
319+
320+
Measured first, rather than reasoned about. The host user is uid 1000, the
321+
container ran as uid 33 (`www-data`), and:
322+
- a normal user-owned folder was **read-only** to the container
323+
- `chgrp 33` on your own folder fails without root — which is *why* the README
324+
demanded `sudo chown`, not laziness
325+
- `--user 1000:1000` at runtime **killed the container**: uid 1000 had no passwd
326+
entry, so `$HOME` was empty and the entrypoint failed writing `/.bashrc`
327+
- `pnpm install` in a plain user folder produced nothing at all
328+
329+
Publishing prebuilt images was wanted (task 16), so baking the host uid at build
330+
time was ruled out. Chosen instead: keep the image generic and adjust the uid when
331+
the container starts.
332+
333+
**What changed**
334+
- Dockerfile: the dev user gets **its own group** and defaults to uid/gid 1000
335+
(the usual first Linux user, so most hosts need no adjustment at all). The
336+
`sed` rewriting `www-data`'s home in `/etc/passwd` was dropped as vestigial.
337+
- `USER php-devforge` removed. The container starts as root, which is how the
338+
official `php:fpm` image works: a root master dropping workers to an
339+
unprivileged user. That `USER` line was what forced the old `exec sudo` hack.
340+
- `zz-docker.conf`: workers run as `php-devforge`.
341+
- The entrypoint adjusts uid/gid, fixes ownership, and no longer needs `sudo`.
342+
343+
**Two traps found while testing, both from the user asking the right questions**
344+
- `usermod -u` does **not** chown files outside the home dir, and nvm lives in
345+
`/usr/local/nvm`. Without an explicit chown, `nvm install` and global npm/pnpm
346+
packages break — the container would start fine and fail later, confusingly.
347+
- The ownership fix must run on **every start**, not only when the uid changes. A
348+
named volume keeps the ownership it was created with: the existing
349+
`dataphp-devforge` volume was still `www-data 700`, so workers could not
350+
traverse `/home/php-devforge` and every page returned `File not found`.
351+
`$HOMEDIR` also needs mode 755, because Apache is a different uid and still has
352+
to traverse it.
353+
- The recursive chown deliberately **excludes `public_html`** — that is the
354+
mounted host folder, and a `chown -R` there would rewrite the ownership of the
355+
user's real projects.
356+
357+
**Verified**: containers start; workers run as uid 1000 while the master stays
358+
root; a file written by PHP over the web is owned by `yostinv` and deletable
359+
without sudo; `pnpm install` leaves `node_modules` owned by you; `nvm install 22`
360+
works (v22.23.2); `sudo` still works inside (the sudoers rule is by name, not
361+
uid); all three host forms serve over HTTPS; it works from a wiped volume; and
362+
with `PUID=1501` the user, `NVM_DIR` and created files all land on 1501.
363+
364+
README updated: the `sudo chown yourUser:www-data` step is gone.
365+
366+
**Unblocks task 7** — the installer can now create the projects folder with no
367+
sudo at all.
368+
369+
<details>
370+
<summary>Options considered (kept for the record)</summary>
276371
277372
The problem this needs to solve: today the README tells users to run
278373
`sudo chown yourUser:www-data -R /home/php-devforge`. That is confusing for people
@@ -303,6 +398,11 @@ Options (**none chosen yet**):
303398
- macOS handling is different (Docker Desktop maps ownership automatically) — should
304399
the two platforms behave differently, or pick one approach that works for both?
305400
401+
</details>
402+
403+
**Still open:** none of this was tested on macOS. Docker Desktop maps ownership on its
404+
own there, so PUID/PGID should be harmless, but it is unverified — see task 15 (CI).
405+
306406
---
307407
308408
## 🟡 Quality and maintenance

‎docker-compose.yml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ x-logging: &json-logging
99
x-phpconfig_default: &phpconfig_default
1010
volumes:
1111
- dataphp-devforge:/home/php-devforge
12-
- /home/php-devforge/public_html:/home/php-devforge/public_html
12+
- ${PROJECTS_DIR:-/home/php-devforge/public_html}:/home/php-devforge/public_html
1313
- ./.caroot:/caroot:ro
1414
environment:
1515
- PHP_XDEBUG=${XDEBUG_ENABLE}
1616
- XDEBUG_IDKEY=${XDEBUG_IDKEY}
17+
# Files created in the container end up owned by this uid/gid on the host.
18+
- PUID=${PUID:-1000}
19+
- PGID=${PGID:-1000}
1720
logging: *json-logging
1821

1922
services:
@@ -56,7 +59,7 @@ services:
5659
volumes:
5760
- dataphp-devforge:/home/php-devforge:ro
5861
- ./certificates:/etc/apache2/ssl/:ro
59-
- /home/php-devforge/public_html:/home/php-devforge/public_html
62+
- ${PROJECTS_DIR:-/home/php-devforge/public_html}:/home/php-devforge/public_html
6063
container_name: apachedev
6164
hostname: ${USERNAME}-apachedev
6265
environment:

‎docker-library/httpd/config_files/resolve_docroot.lua‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ function silly_mapper(r)
3333
local path_parts = {}
3434

3535
if string.find(final_path, "%-%-") then
36-
-- Usar separador --
37-
for part in string.gmatch(final_path, "[^%-%-]+") do
36+
-- Split on "--" only. "[^%-%-]+" would split on every single hyphen,
37+
-- so a project named mi-app became sites/app/mi instead of sites/mi-app.
38+
-- \1 cannot appear in a hostname, so it is a safe temporary separator.
39+
local tmp = (final_path:gsub("%-%-", "\1"))
40+
for part in string.gmatch(tmp, "[^\1]+") do
3841
table.insert(path_parts, part)
3942
end
4043
else

‎docker-library/php/Dockerfile‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ RUN pecl install imagick redis apcu xdebug \
6060
# Instalar Composer
6161
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
6262

63-
# Crear usuario php-devforge con UID/GID 33
64-
RUN /usr/sbin/useradd -g 33 -o -u 33 -c "php dev" -m -d /home/php-devforge -s /bin/bash php-devforge \
65-
&& sed -i -e "s/\\/var\\/www/\\/home\\/php-devforge/g" /etc/passwd
63+
# Dev user with its own group. 1000/1000 is the usual first user on Linux, so
64+
# most hosts need no adjustment; the entrypoint fixes it via PUID/PGID otherwise.
65+
RUN groupadd -g 1000 php-devforge \
66+
&& useradd -u 1000 -g php-devforge -c "php dev" -m -d /home/php-devforge -s /bin/bash php-devforge
6667

6768
RUN echo 'php-devforge ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/php-devforge \
6869
&& chmod 440 /etc/sudoers.d/php-devforge
@@ -102,5 +103,4 @@ RUN chmod +x /usr/local/bin/xdebug \
102103

103104
COPY config_files/docker-php-ext-xdebug.ini "/usr/local/etc/php/mods-available/docker-php-ext-xdebug.ini"
104105

105-
USER php-devforge
106106
WORKDIR /home/php-devforge

0 commit comments

Comments
 (0)