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>
The Dockerfile is now multi-stage: a Node stage runs `npm run build`, and the
PHP/Apache stage copies the result into public/. Apache + public/.htaccess route
/api* to the Slim front controller, serve real files, and fall back to
index.html for client-side routes.
docker-compose.yml loses the `web` service, its volume, and the source
bind-mount -- the image is the artifact now (rebuild to pick up changes).
Frontend dev moves to `npm run dev` on the host; APP_URL defaults to :8080 since
the one container serves both halves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docker-compose.yml now mounts the working directory at /var/www/html so
PHP changes take effect without an image rebuild. To avoid a mount nested
inside that bind mount, the storage directory moves out to /var/www/storage
(still a named volume). Config gains a STORAGE_PATH env var driving both the
SQLite database and the JWT signing-key location; the entrypoint chowns that
directory. The Dockerfile is unchanged and still builds a self-contained
image (STORAGE_PATH defaults back to ./storage when unset).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PHP 8.3 + Apache image serving public/ on port 8080, with pdo_sqlite and
mbstring built in. The container entrypoint applies migrations as www-data
before starting Apache; the SQLite database and generated JWT signing key
persist in a named "storage" volume. `docker compose up -d` is now the
primary way to run the project without a local PHP toolchain.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>