Stage 2 was php:8.3-apache (Debian), which compiles PHP from source with --with-apxs2 for mod_php -- that base image alone is 719MB of our 735MB, before any app code. Replaced with alpine:3.24 + apk's own prebuilt php83/php83-apache2/apache2 packages: same architecture (one process, mod_php, .htaccess-driven rewriting), no fpm/nginx rewrite needed. - docker/apache.conf: rewritten for Alpine's apache2 (mod_rewrite ships but isn't loaded by default; a different default document root/log paths). Logs redirected to stdout/stderr so `docker logs` still shows them -- Alpine's own defaults write to a real file under ServerRoot, unlike the official Debian image's symlinked paths. - docker/entrypoint.sh: su-exec instead of su -- BusyBox's su doesn't take the same -c/user argument order as the GNU one the previous entrypoint relied on. Also moved earlier in the Dockerfile (with the other rarely-changing setup, before COPY . .) so it no longer re-runs on every build for a file that essentially never changes. - Composer's binary is still borrowed from the official composer:2 image via multi-stage COPY, not apk's own `composer` package, which turned out to pull in an entire second PHP interpreter (php85) as a dependency just to run itself. - ext-iconv needed adding explicitly (symfony/polyfill-mbstring depends on it; the official Debian image bundles it by default, apk doesn't). Verified against the real compose stack, not just that it builds: apk install; composer install; migrations on startup; PHPUnit 88/88 (runs on the host, but confirms nothing else broke); and by hand, all through the actual container -- health check, SPA fallback for unknown routes, static assets served directly, the API's 401 guard, and a full magic-link -> verify -> JWT -> authenticated project create/list round trip via the real Mailpit catcher. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
106 lines
4.9 KiB
Markdown
106 lines
4.9 KiB
Markdown
# Setup & configuration
|
|
|
|
For the quick version — just running the app — see the root
|
|
[README](../README.md#getting-started). This covers the rest: running
|
|
without Docker, every configuration option, and the test suite.
|
|
|
|
## Run with Docker
|
|
|
|
The only requirement is Docker with the Compose plugin.
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
This runs a multi-stage build — a Node stage compiles the Vue frontend, then a
|
|
PHP 8.3 + Apache stage (Alpine-based; apk's own prebuilt packages rather than
|
|
compiling PHP from source, which is most of why the image is ~90MB rather
|
|
than several times that) bakes in the PHP source and the built SPA — applies
|
|
migrations, and serves the whole app at <http://localhost:8080>: the SPA at `/`
|
|
(assets and all) and the REST API under `/api` (e.g.
|
|
`curl http://localhost:8080/api/health`). Unknown paths fall back to the SPA
|
|
shell for client-side routing.
|
|
|
|
- A **[Mailpit](https://mailpit.axllent.org/)** container (the maintained MailHog
|
|
successor — one ~15 MB Go binary, messages kept in memory) also starts. The API
|
|
sends all email to it; read it at <http://localhost:8025>. Set
|
|
`MAIL_TRANSPORT=mail` or `=smtp` (with `MAIL_SMTP_*`) to send for real.
|
|
- The image is the artifact: PHP source and the compiled frontend are copied in
|
|
at build time, not bind-mounted. Rebuild to pick up any code change:
|
|
`docker compose up -d --build`. For iterating on the frontend, run the Vite
|
|
dev server on the host instead (see [web/README.md](../web/README.md)).
|
|
- The SQLite database and the generated JWT signing key live in the `storage`
|
|
named volume, mounted at `/var/www/storage`, so they survive
|
|
`docker compose restart` / `down` + `up`.
|
|
- `docker compose down -v` removes the volume and gives you a clean database.
|
|
- Override settings via the environment or a `.env` file in the repo root
|
|
(Compose substitutes `APP_DEBUG`, `JWT_SECRET`, `JWT_TTL` — see
|
|
[docker-compose.yml](../docker-compose.yml)).
|
|
|
|
## Run without Docker
|
|
|
|
Requires PHP 8.1+ with the `pdo_sqlite` and `mbstring` extensions, plus
|
|
[Composer](https://getcomposer.org/). On Fedora:
|
|
|
|
```bash
|
|
sudo dnf install php-cli php-pdo php-mbstring composer
|
|
```
|
|
|
|
### Setup
|
|
|
|
```bash
|
|
composer install
|
|
cp .env.example .env # optional; sane defaults are used without it
|
|
composer migrate # creates storage/database.sqlite and its tables
|
|
```
|
|
|
|
### Running
|
|
|
|
```bash
|
|
composer serve # http://localhost:8080 (php -S localhost:8080 -t public)
|
|
```
|
|
|
|
Any web server can serve the API as long as the document root is `public/` and
|
|
unknown paths fall through to `public/index.php`. `public/.htaccess` also serves
|
|
a built frontend from `public/` (copy `web/dist/` there) and only falls back to
|
|
`index.php` for `/api` and when no `index.html` is present.
|
|
|
|
The frontend itself still needs its own toolchain — see
|
|
[web/README.md](../web/README.md).
|
|
|
|
## Configuration
|
|
|
|
All settings are optional environment variables (read from `.env` or the real
|
|
environment). See [.env.example](../.env.example).
|
|
|
|
| Variable | Default | Purpose |
|
|
|----------|---------|---------|
|
|
| `APP_DEBUG` | `false` | Include exception details in error responses |
|
|
| `DATABASE_PATH` | `storage/database.sqlite` | SQLite file location |
|
|
| `JWT_SECRET` | auto-generated into `storage/secret.key` | Token signing key |
|
|
| `JWT_TTL` | `86400` | Token lifetime in seconds |
|
|
| `APP_ALLOW_REGISTRATION` | `true` | When `false`, a magic link is only ever sent to an existing address — an unknown one is silently ignored, so no new accounts get created |
|
|
| `MAGIC_LINK_RESEND_SECONDS` | `60` | Minimum gap before a magic link can be resent to the same address (sign-in or email-change). Docker Compose overrides this to `0`, so links resend immediately in development |
|
|
| `APP_URL` | `http://localhost:8080` | Base URL used to build magic links (`http://localhost:5173` for a host `npm run dev`) |
|
|
| `WEBAUTHN_RP_ID` | `APP_URL`'s host | Passkey relying party ID (domain). Must be `localhost` or a real domain over HTTPS — a LAN IP won't work |
|
|
| `WEBAUTHN_RP_NAME` | `Projects` | Passkey relying party display name, shown in the browser/OS prompt |
|
|
| `MAIL_TRANSPORT` | `mail` | `mail` (PHP `mail()`), `smtp`, or `log` (append to a file) |
|
|
| `MAIL_FROM` / `MAIL_FROM_NAME` | `no-reply@todo.test` / `Projects` | Envelope sender |
|
|
| `MAIL_LOG_PATH` | `storage/mail.log` | Where `log` transport writes |
|
|
| `MAIL_SMTP_HOST` / `_PORT` / `_USERNAME` / `_PASSWORD` / `_ENCRYPTION` | — / `587` / — / — / `tls` | Used only when `MAIL_TRANSPORT=smtp` |
|
|
|
|
Standalone, SMTP is opt-in and the API otherwise falls back to PHP's `mail()`.
|
|
Under Docker Compose the default is `MAIL_TRANSPORT=smtp` pointed at the bundled
|
|
Mailpit container (`mailpit:1025`, no auth/TLS); open <http://localhost:8025> to
|
|
read what was "sent".
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
composer install # installs phpunit (require-dev)
|
|
vendor/bin/phpunit
|
|
```
|
|
|
|
Each test run applies every file in `migrations/*.sql` to a fresh SQLite
|
|
database, so it always exercises the current schema from scratch.
|