瀏覽代碼

Build: split Dockerfile into 4 targets + dev compose overlay + Makefile

The single Dockerfile now exposes:
  * css-builder  — one-shot Tailwind for prod (existing behaviour)
  * css-watcher  — node sidecar running `tailwindcss --watch` (dev only)
  * runtime      — PHP/Apache prod image (renamed, otherwise unchanged)
  * tests        — FROM runtime + composer install w/ dev deps for phpunit

docker-compose.yml stays prod-shaped (now with explicit target=runtime).
docker-compose.dev.yml is an explicit overlay (no auto-load) that adds:
  * APP_ENV=development on app (flips Twig auto_reload via View.php:36)
  * source bind mounts on app — edits visible without rebuild
  * css-watcher sidecar running as ${HOST_UID}:${HOST_GID} (Makefile-
    exported) so files written into bind-mounted host paths land with
    normal ownership instead of root
  * tests service under profiles:[test] for one-shot `--rm` runs

bin/dev-css-watcher.sh seeds vendor JS bundles (alpine-csp, htmx,
sortable) into the host-mounted public/assets/js/vendor on first start,
then execs tailwindcss --watch directly (skipping npx, which would want
to write \$HOME under a non-root user).

Makefile wraps the long `docker compose -f … -f …` invocations:
  make dev / dev-build / dev-down
  make prod / prod-build / prod-down
  make test / lint / check     (one-shot tests container)
  make shell / logs / help

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClaudePriv@chiappa.zhdk.ch 1 天之前
父節點
當前提交
a004c1a3a1
共有 5 個文件被更改,包括 189 次插入3 次删除
  1. 26 2
      Dockerfile
  2. 72 0
      Makefile
  3. 24 0
      bin/dev-css-watcher.sh
  4. 64 0
      docker-compose.dev.yml
  5. 3 1
      docker-compose.yml

+ 26 - 2
Dockerfile

@@ -28,8 +28,22 @@ RUN mkdir -p /build/vendor \
     && cp node_modules/htmx.org/dist/htmx.min.js     /build/vendor/htmx.min.js \
     && cp node_modules/htmx.org/dist/htmx.min.js     /build/vendor/htmx.min.js \
     && cp node_modules/sortablejs/Sortable.min.js    /build/vendor/sortable.min.js
     && cp node_modules/sortablejs/Sortable.min.js    /build/vendor/sortable.min.js
 
 
-# --- Stage 2: the actual PHP runtime ------------------------------------
-FROM php:8.3-apache
+# --- Stage 2: tailwind --watch (dev only) -------------------------------
+# Used by docker-compose.dev.yml. Source dirs are bind-mounted from the
+# host at /build/* and `app.css` is regenerated on save. Vendor JS bundles
+# are seeded into the host-mounted public/assets/js/vendor on first start
+# by bin/dev-css-watcher.sh — they don't change between iterations so a
+# single seed at startup is enough.
+FROM node:20-alpine AS css-watcher
+WORKDIR /build
+COPY package.json package-lock.json* ./
+RUN npm ci --no-audit --no-fund
+COPY bin/dev-css-watcher.sh /usr/local/bin/dev-css-watcher.sh
+RUN chmod +x /usr/local/bin/dev-css-watcher.sh
+CMD ["/usr/local/bin/dev-css-watcher.sh"]
+
+# --- Stage 3: the actual PHP runtime ------------------------------------
+FROM php:8.3-apache AS runtime
 
 
 RUN apt-get update && apt-get install -y --no-install-recommends \
 RUN apt-get update && apt-get install -y --no-install-recommends \
         libsqlite3-dev libzip-dev libpng-dev libjpeg-dev libfreetype6-dev unzip git \
         libsqlite3-dev libzip-dev libpng-dev libjpeg-dev libfreetype6-dev unzip git \
@@ -79,3 +93,13 @@ RUN install -m 0755 /var/www/html/bin/docker-entrypoint.sh /usr/local/bin/docker
 EXPOSE 80
 EXPOSE 80
 ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
 ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
 CMD ["apache2-foreground"]
 CMD ["apache2-foreground"]
+
+# --- Stage 4: phpunit + dev composer deps (test runner) -----------------
+# Built on top of `runtime` so the test environment matches prod exactly,
+# then layers in the composer dev dependencies (phpunit, mocks). Used by
+# `make test` / `make check` via `docker compose run --rm tests`.
+# Inherits the entrypoint, which runs migrations against an in-container
+# SQLite before phpunit starts — tests get a fresh, fully-migrated schema.
+FROM runtime AS tests
+RUN composer install --no-interaction --prefer-dist --no-progress
+CMD ["composer", "test"]

+ 72 - 0
Makefile

@@ -0,0 +1,72 @@
+# Convenience wrappers for the dev/prod compose split. Targets are split
+# along the same axis as the compose files: anything `dev-*` uses the
+# overlay, anything else hits prod.
+
+COMPOSE_PROD = docker compose
+COMPOSE_DEV  = docker compose -f docker-compose.yml -f docker-compose.dev.yml
+
+# Exported so the css-watcher service can run as the host user via
+# `user: "${HOST_UID}:${HOST_GID}"` in docker-compose.dev.yml. Files the
+# watcher writes into bind-mounted host paths (public/assets/css/app.css,
+# public/assets/js/vendor/*) then land with normal ownership.
+export HOST_UID := $(shell id -u)
+export HOST_GID := $(shell id -g)
+
+.PHONY: help dev dev-build dev-down prod prod-build prod-down \
+        test lint check shell logs
+
+help:
+	@echo "Dev:"
+	@echo "  make dev          start dev stack (app + css-watcher) in foreground"
+	@echo "  make dev-build    rebuild dev images"
+	@echo "  make dev-down     stop and remove dev containers"
+	@echo "  make shell        bash into the running app container"
+	@echo "  make logs         tail logs from the dev stack"
+	@echo ""
+	@echo "Prod:"
+	@echo "  make prod         start prod stack detached"
+	@echo "  make prod-build   rebuild prod image"
+	@echo "  make prod-down    stop and remove prod containers"
+	@echo ""
+	@echo "Checks (one-shot containers, no running stack required):"
+	@echo "  make lint         php -l on src/ + tests/"
+	@echo "  make test         phpunit"
+	@echo "  make check        lint + test (used by /check skill)"
+
+# --- dev ----------------------------------------------------------------
+dev:
+	$(COMPOSE_DEV) up
+
+dev-build:
+	$(COMPOSE_DEV) build
+
+dev-down:
+	$(COMPOSE_DEV) down
+
+shell:
+	$(COMPOSE_DEV) exec app bash
+
+logs:
+	$(COMPOSE_DEV) logs -f
+
+# --- prod ---------------------------------------------------------------
+prod:
+	$(COMPOSE_PROD) up -d
+
+prod-build:
+	$(COMPOSE_PROD) build
+
+prod-down:
+	$(COMPOSE_PROD) down
+
+# --- checks (one-shot, profile=test) ------------------------------------
+# `run --rm` builds the tests image if needed, runs the command, and
+# tears the container down. Doesn't require `make dev` to be running.
+lint:
+	$(COMPOSE_DEV) --profile test run --rm tests \
+		sh -c 'find src tests -name "*.php" -print0 | xargs -0 -n1 -P 4 php -l > /dev/null && echo "lint: OK"'
+
+test:
+	$(COMPOSE_DEV) --profile test run --rm tests
+
+check: lint test

+ 24 - 0
bin/dev-css-watcher.sh

@@ -0,0 +1,24 @@
+#!/bin/sh
+# CMD for the css-watcher dev container. Bind-mounts from compose.dev:
+#   /build/assets, /build/views, /build/src, /build/public, tailwind.config.js
+# Output goes to /build/public/assets/css/app.css, which is the host's
+# public/assets/css/app.css (gitignored).
+set -eu
+
+# Vendor JS bundles never change between iterations — copy once on start
+# so a host bind-mount over public/assets/js/vendor stays populated.
+# `cp -u` skips when the destination is already up-to-date, so subsequent
+# container restarts are no-ops.
+mkdir -p public/assets/js/vendor
+cp -u node_modules/@alpinejs/csp/dist/cdn.min.js public/assets/js/vendor/alpine-csp.min.js
+cp -u node_modules/htmx.org/dist/htmx.min.js     public/assets/js/vendor/htmx.min.js
+cp -u node_modules/sortablejs/Sortable.min.js    public/assets/js/vendor/sortable.min.js
+
+# Direct binary path instead of `npx` — when the container runs as a
+# non-root host user (see compose.dev.yml `user:` directive), npx may try
+# to write to a $HOME it can't access. The locally-installed binary just
+# works.
+exec ./node_modules/.bin/tailwindcss \
+    -i assets/css/input.css \
+    -o public/assets/css/app.css \
+    --watch

+ 64 - 0
docker-compose.dev.yml

@@ -0,0 +1,64 @@
+# Dev overlay — load alongside docker-compose.yml:
+#   docker compose -f docker-compose.yml -f docker-compose.dev.yml up
+# (or `make dev`).
+#
+# Differences from prod:
+#   * APP_ENV=development → Twig auto_reload kicks in (see View.php:36)
+#   * Source dirs bind-mounted from host so edits show up without rebuild
+#   * css-watcher sidecar runs `tailwindcss --watch` and seeds vendor JS
+#   * tests profile builds the `tests` Dockerfile target on demand
+#
+# Volumes intentionally mount specific subdirs, NOT the project root —
+# mounting `.` would mask the composer-installed /var/www/html/vendor.
+services:
+  app:
+    environment:
+      APP_ENV: development
+    volumes:
+      - ./src:/var/www/html/src
+      - ./views:/var/www/html/views
+      - ./public:/var/www/html/public
+      - ./assets:/var/www/html/assets
+      - ./migrations:/var/www/html/migrations
+      - ./bin:/var/www/html/bin
+      - ./tailwind.config.js:/var/www/html/tailwind.config.js
+      - ./composer.json:/var/www/html/composer.json
+      - ./composer.lock:/var/www/html/composer.lock
+      - ./phpunit.xml:/var/www/html/phpunit.xml
+
+  css-watcher:
+    build:
+      context: .
+      target: css-watcher
+    # Run as the host user so files written into the bind-mounted host dirs
+    # (public/assets/css/app.css, public/assets/js/vendor/*) land with the
+    # right ownership and don't need a `sudo chown` later. The Makefile
+    # exports HOST_UID/HOST_GID; the :-1000 fallback covers the typical
+    # case when someone invokes compose directly without going through make.
+    user: "${HOST_UID:-1000}:${HOST_GID:-1000}"
+    volumes:
+      - ./assets:/build/assets
+      - ./views:/build/views
+      - ./src:/build/src
+      - ./public:/build/public
+      - ./tailwind.config.js:/build/tailwind.config.js
+    restart: unless-stopped
+
+  tests:
+    build:
+      context: .
+      target: tests
+    profiles: ["test"]
+    env_file: .env
+    # Read-only bind mounts — tests should never mutate source. The data
+    # volume is anonymous and per-run, so each `docker compose run --rm
+    # tests` gets a fresh SQLite via the entrypoint's migrate step.
+    volumes:
+      - ./src:/var/www/html/src:ro
+      - ./tests:/var/www/html/tests:ro
+      - ./views:/var/www/html/views:ro
+      - ./migrations:/var/www/html/migrations:ro
+      - ./bin:/var/www/html/bin:ro
+      - ./phpunit.xml:/var/www/html/phpunit.xml:ro
+      - ./composer.json:/var/www/html/composer.json:ro
+      - ./composer.lock:/var/www/html/composer.lock:ro

+ 3 - 1
docker-compose.yml

@@ -1,6 +1,8 @@
 services:
 services:
   app:
   app:
-    build: .
+    build:
+      context: .
+      target: runtime
     ports:
     ports:
       - "8088:80"
       - "8088:80"
     env_file: .env
     env_file: .env