Dockerfile 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. # --- Stage 1: compile Tailwind CSS + vendor JS deps ----------------------
  2. # Runs the Tailwind JIT over views/, src/, and our JS so only classes that
  3. # are actually referenced end up in the output. No runtime <style> injection,
  4. # which lets the CSP drop 'unsafe-inline' for style-src.
  5. #
  6. # The same stage also vendors Alpine.js (CSP build), htmx, and SortableJS
  7. # from npm into /build/vendor/ — copied into the runtime image alongside the
  8. # CSS so the strict CSP can keep `script-src 'self'`.
  9. FROM node:20-alpine AS css-builder
  10. WORKDIR /build
  11. COPY package.json package-lock.json* ./
  12. RUN npm ci --no-audit --no-fund
  13. # Only the files that contribute to class discovery / the entry stylesheet.
  14. COPY tailwind.config.js ./
  15. COPY assets/css/input.css ./assets/css/input.css
  16. COPY views/ ./views/
  17. COPY src/ ./src/
  18. COPY public/assets/js/ ./public/assets/js/
  19. RUN npx tailwindcss -i ./assets/css/input.css -o /build/app.css --minify
  20. # Pin the vendored JS bundles. Alpine CSP is the variant that doesn't need
  21. # `unsafe-eval`; standard Alpine would require relaxing the CSP.
  22. RUN mkdir -p /build/vendor \
  23. && cp node_modules/@alpinejs/csp/dist/cdn.min.js /build/vendor/alpine-csp.min.js \
  24. && cp node_modules/htmx.org/dist/htmx.min.js /build/vendor/htmx.min.js \
  25. && cp node_modules/sortablejs/Sortable.min.js /build/vendor/sortable.min.js
  26. # --- Stage 2: tailwind --watch (dev only) -------------------------------
  27. # Used by docker-compose.dev.yml. Source dirs are bind-mounted from the
  28. # host at /build/* and `app.css` is regenerated on save. Vendor JS bundles
  29. # are seeded into the host-mounted public/assets/js/vendor on first start
  30. # by bin/dev-css-watcher.sh — they don't change between iterations so a
  31. # single seed at startup is enough.
  32. FROM node:20-alpine AS css-watcher
  33. WORKDIR /build
  34. COPY package.json package-lock.json* ./
  35. RUN npm ci --no-audit --no-fund
  36. COPY bin/dev-css-watcher.sh /usr/local/bin/dev-css-watcher.sh
  37. RUN chmod +x /usr/local/bin/dev-css-watcher.sh
  38. CMD ["/usr/local/bin/dev-css-watcher.sh"]
  39. # --- Stage 3: the actual PHP runtime ------------------------------------
  40. FROM php:8.3-apache AS runtime
  41. RUN apt-get update && apt-get install -y --no-install-recommends \
  42. libsqlite3-dev libzip-dev libpng-dev libjpeg-dev libfreetype6-dev unzip git \
  43. && docker-php-ext-configure gd --with-freetype --with-jpeg \
  44. && docker-php-ext-install pdo pdo_sqlite zip gd \
  45. && a2enmod rewrite \
  46. && rm -rf /var/lib/apt/lists/*
  47. COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
  48. WORKDIR /var/www/html
  49. COPY composer.json composer.lock* ./
  50. RUN composer install --no-dev --no-interaction --prefer-dist --no-progress
  51. COPY . .
  52. # Place the compiled CSS + vendored JS where Apache can serve them.
  53. COPY --from=css-builder /build/app.css /var/www/html/public/assets/css/app.css
  54. COPY --from=css-builder /build/vendor/ /var/www/html/public/assets/js/vendor/
  55. # Twig cache lives under data/ alongside the SQLite file. www-data must be
  56. # able to write there at request time so first-render template compilation
  57. # succeeds.
  58. RUN mkdir -p /var/www/data /var/www/data/sessions /var/www/html/data/twig-cache \
  59. && chown -R www-data:www-data /var/www/data /var/www/html/data \
  60. && printf '%s\n' \
  61. '<VirtualHost *:80>' \
  62. ' DocumentRoot /var/www/html/public' \
  63. ' <Directory /var/www/html/public>' \
  64. ' Options -Indexes +FollowSymLinks' \
  65. ' AllowOverride All' \
  66. ' Require all granted' \
  67. ' FallbackResource /index.php' \
  68. ' </Directory>' \
  69. ' ErrorLog ${APACHE_LOG_DIR}/error.log' \
  70. ' CustomLog ${APACHE_LOG_DIR}/access.log combined' \
  71. '</VirtualHost>' \
  72. > /etc/apache2/sites-available/000-default.conf
  73. # R01-N22: run migrations at container start, before Apache binds the port.
  74. # The request path no longer auto-migrates — it only checks and refuses to
  75. # serve when something is pending — so a missed entrypoint produces a loud
  76. # 503, not silent stale-schema serving.
  77. RUN install -m 0755 /var/www/html/bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
  78. EXPOSE 80
  79. ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
  80. CMD ["apache2-foreground"]
  81. # --- Stage 4: phpunit + dev composer deps (test runner) -----------------
  82. # Built on top of `runtime` so the test environment matches prod exactly,
  83. # then layers in the composer dev dependencies (phpunit, mocks). Used by
  84. # `make test` / `make check` via `docker compose run --rm tests`.
  85. # Inherits the entrypoint, which runs migrations against an in-container
  86. # SQLite before phpunit starts — tests get a fresh, fully-migrated schema.
  87. FROM runtime AS tests
  88. RUN composer install --no-interaction --prefer-dist --no-progress
  89. CMD ["composer", "test"]