Rebuild the Docker image on Alpine: ~735MB -> ~89MB

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>
This commit is contained in:
2026-09-05 02:15:39 +01:00
co-authored by Claude Sonnet 5
parent 82e8ee6c36
commit f1309b4c10
6 changed files with 75 additions and 35 deletions
+40 -17
View File
@@ -13,24 +13,51 @@ RUN npm ci
COPY web/ ./
RUN npm run build # vue-tsc type-check, then `vite build` -> /web/dist
# --- Stage 2: PHP + Apache runtime ---------------------------------------------
FROM php:8.3-apache
# --- Stage 2: PHP + Apache runtime, on Alpine -------------------------------
FROM alpine:3.24
# --- PHP extensions and CLI tools ----------------------------------------------
RUN apt-get update && apt-get install -y --no-install-recommends \
libsqlite3-dev \
libonig-dev \
unzip \
# apk's php83 packages are prebuilt (no compiling PHP from source, unlike the
# official Debian php:8.3-apache image), and split finely enough to pull in
# only what this app actually uses. php83-apache2 is Alpine's mod_php SAPI
# module -- the same role as php:8.3-apache's --with-apxs2 build. This alone
# takes the final image from ~735MB to ~90MB.
RUN apk add --no-cache \
apache2 \
curl \
&& docker-php-ext-install pdo_sqlite mbstring \
&& rm -rf /var/lib/apt/lists/*
php83 \
php83-apache2 \
php83-ctype \
php83-curl \
php83-fileinfo \
php83-iconv \
php83-mbstring \
php83-opcache \
php83-openssl \
php83-pdo \
php83-pdo_sqlite \
php83-phar \
php83-session \
php83-tokenizer \
su-exec \
# apk doesn't symlink an unversioned `php` -- composer's own wrapper
# script (copied in below) and bin/migrate.php's shebang both expect one.
&& ln -s /usr/bin/php83 /usr/local/bin/php
# --- Composer ----------------------------------------------------------------
# Borrowed from the official composer image rather than apk's own `composer`
# package, which drags in an entire second PHP interpreter (php85) as a
# dependency just to run the phar -- multi-stage COPY takes only the binary.
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# --- Apache: document root -> public/, allow .htaccess rewrites -------------
RUN a2enmod rewrite
COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf
COPY docker/apache.conf /etc/apache2/conf.d/zz-app.conf
# --- Entrypoint: migrate, then hand off to Apache ---------------------------
# Copied early, alongside the other rarely-changing setup above -- after
# COPY . . (below) every later layer re-runs on nearly every build, so this
# would otherwise redo work for a file that essentially never changes.
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
WORKDIR /var/www/html
@@ -42,17 +69,13 @@ RUN composer install --no-dev --no-interaction --no-progress --prefer-dist --no-
COPY . .
RUN composer dump-autoload --optimize --no-dev \
&& mkdir -p storage \
&& chown -R www-data:www-data storage
&& chown -R apache:apache storage
# --- Built frontend: served from the web root next to the API front controller
COPY --from=frontend /web/dist/ ./public/
# --- Entrypoint: migrate, then hand off to Apache ------------------------
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["apache2-foreground"]
CMD ["httpd", "-D", "FOREGROUND"]
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD curl -fsS http://localhost/api/health || exit 1