# syntax=docker/dockerfile:1 # --- Stage 1: build the Vue frontend --------------------------------------- FROM node:24-alpine AS frontend WORKDIR /web # Dependencies in their own layer, cached unless the manifests change. npm ci # resolves this stage's (musl) platform binaries for rollup/esbuild. The cache # mount keeps the npm download cache warm across builds, so a lockfile bump # only re-fetches what actually changed. COPY web/package.json web/package-lock.json ./ RUN --mount=type=cache,target=/root/.npm npm ci COPY web/ ./ RUN npm run build # vue-tsc type-check, then `vite build` -> /web/dist # --- Stage 2: PHP + Apache runtime, on Alpine ------------------------------- FROM alpine:3.24 # 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 \ 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 ------------- 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 --chmod=0755 docker/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /var/www/html # --- PHP dependencies (own layer, cached unless composer.* changes) -------- # The cache mount keeps Composer's package cache warm across builds, so a # composer.lock bump only re-fetches the packages that changed. COPY composer.json composer.lock ./ RUN --mount=type=cache,target=/tmp/composer-cache \ COMPOSER_CACHE_DIR=/tmp/composer-cache \ composer install --no-dev --no-interaction --no-progress --prefer-dist --no-autoloader # --- Application source ---------------------------------------------------- COPY . . RUN composer dump-autoload --optimize --no-dev \ && mkdir -p 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 ["/usr/local/bin/entrypoint.sh"] CMD ["httpd", "-D", "FOREGROUND"] HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ CMD curl -fsS http://localhost/api/health || exit 1