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:
+40
-17
@@ -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
|
||||
|
||||
+17
-7
@@ -1,13 +1,23 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/html/public
|
||||
# Alpine's apache2 package ships mod_rewrite but doesn't load it, and defaults
|
||||
# to a document root (and log files) that don't match this project -- this
|
||||
# file (auto-included via httpd.conf's "IncludeOptional conf.d/*.conf")
|
||||
# overrides just those bits, on top of what php83-apache2's own
|
||||
# conf.d/php83-module.conf already wired up (LoadModule for mod_php, the
|
||||
# .php handler).
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
<Directory /var/www/html/public>
|
||||
ServerName localhost
|
||||
DocumentRoot "/var/www/html/public"
|
||||
|
||||
<Directory "/var/www/html/public">
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
DirectoryIndex index.html index.php
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
# Alpine's own defaults log to a real file under ServerRoot (logs/*.log) --
|
||||
# stdout/stderr instead, so `docker logs` shows them, the way the official
|
||||
# Debian php:apache image's symlinked log paths used to.
|
||||
ErrorLog /dev/stderr
|
||||
CustomLog /dev/stdout combined
|
||||
|
||||
@@ -5,10 +5,11 @@ set -e
|
||||
# development (see docker-compose.yml); the plain image falls back to ./storage.
|
||||
STORAGE_DIR="${STORAGE_PATH:-storage}"
|
||||
mkdir -p "$STORAGE_DIR"
|
||||
chown -R www-data:www-data "$STORAGE_DIR"
|
||||
chown -R apache:apache "$STORAGE_DIR"
|
||||
|
||||
# Apply pending migrations as www-data so the SQLite file it creates stays
|
||||
# writable by Apache.
|
||||
su -s /bin/sh -c 'php bin/migrate.php' www-data
|
||||
# Apply pending migrations as apache so the SQLite file it creates stays
|
||||
# writable by the web server. su-exec, not su -- BusyBox's su (this is an
|
||||
# Alpine image) doesn't take the same -c/user argument order as the GNU one.
|
||||
su-exec apache php bin/migrate.php
|
||||
|
||||
exec docker-php-entrypoint "$@"
|
||||
exec "$@"
|
||||
|
||||
@@ -21,8 +21,11 @@ src/Repository/ Database access (User, EmailVerification, Passkey,
|
||||
src/Support/Validator.php Request-body validation helper
|
||||
migrations/*.sql Schema, applied by bin/migrate.php
|
||||
Dockerfile Multi-stage: Node frontend build + PHP 8.3/Apache runtime
|
||||
(Alpine -- apk's own php83/apache2 packages, not a
|
||||
from-source compile; ~90MB image)
|
||||
docker-compose.yml Local stack: app (SPA + API) + Mailpit
|
||||
docker/ Apache vhost + container entrypoint
|
||||
docker/ Apache vhost (mod_rewrite, document root) + entrypoint
|
||||
(runs pending migrations, then hands off to Apache)
|
||||
web/ Vue 3 + TypeScript + Vite PWA frontend (dev on the host)
|
||||
```
|
||||
|
||||
|
||||
@@ -27,3 +27,4 @@ again in stage 12); see [docs/api.md](api.md) and
|
||||
| 17 | Card detail view (`/cards/:id`) + its own configuration view — a card's text is no longer inline-editable; every list links to its own page instead | ✅ done |
|
||||
| 18 | Project view split into real routes — Explore (`/projects/:id`) and Kanban (`/projects/:id/kanban`) are separate pages under a shared layout, not client-side tab state | ✅ done |
|
||||
| 19 | Backend/frontend refactoring pass — deduplicated controller/repository boilerplate and CSS, dropped the never-surfaced project `description` field, squashed the SQL migration history into one clean initial schema | ✅ done |
|
||||
| 20 | Docker image rebuilt on Alpine (apk's own php83/apache2 packages instead of compiling PHP from source on Debian) — ~735MB down to ~90MB, same architecture otherwise; `.dockerignore` trimmed to match | ✅ done |
|
||||
|
||||
+3
-1
@@ -13,7 +13,9 @@ docker compose up -d
|
||||
```
|
||||
|
||||
This runs a multi-stage build — a Node stage compiles the Vue frontend, then a
|
||||
PHP 8.3 + Apache stage bakes in the PHP source and the built SPA — applies
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user