Serve the built SPA from the PHP image; drop the web container
The Dockerfile is now multi-stage: a Node stage runs `npm run build`, and the PHP/Apache stage copies the result into public/. Apache + public/.htaccess route /api* to the Slim front controller, serve real files, and fall back to index.html for client-side routes. docker-compose.yml loses the `web` service, its volume, and the source bind-mount -- the image is the artifact now (rebuild to pick up changes). Frontend dev moves to `npm run dev` on the host; APP_URL defaults to :8080 since the one container serves both halves. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,3 +8,8 @@ storage
|
||||
.phpunit.result.cache
|
||||
docs
|
||||
README.md
|
||||
|
||||
# Frontend: the build runs `npm ci` in its own stage, and dist/ is emitted there.
|
||||
web/node_modules
|
||||
web/dist
|
||||
web/dev-dist
|
||||
|
||||
+4
-3
@@ -19,9 +19,10 @@ JWT_SECRET=
|
||||
# How long an issued token stays valid, in seconds (default: 86400 = 24h).
|
||||
JWT_TTL=86400
|
||||
|
||||
# Base URL of the frontend. Verification magic links point here, e.g.
|
||||
# <APP_URL>/verify-email?token=... (default: http://localhost:5173).
|
||||
APP_URL=http://localhost:5173
|
||||
# Base URL the app is reached at. Verification magic links point here, e.g.
|
||||
# <APP_URL>/verify-email?token=... The Docker image serves the SPA and the API
|
||||
# together on http://localhost:8080; a host `npm run dev` serves it on :5173.
|
||||
APP_URL=http://localhost:8080
|
||||
|
||||
# Email delivery.
|
||||
# mail — PHP's built-in mail() function (default)
|
||||
|
||||
+17
@@ -1,5 +1,19 @@
|
||||
# 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.
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
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
|
||||
|
||||
# --- PHP extensions and CLI tools ----------------------------------------------
|
||||
@@ -30,6 +44,9 @@ RUN composer dump-autoload --optimize --no-dev \
|
||||
&& mkdir -p storage \
|
||||
&& chown -R www-data:www-data 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
|
||||
|
||||
+7
-27
@@ -8,14 +8,14 @@ services:
|
||||
- "8080:80"
|
||||
environment:
|
||||
APP_DEBUG: "${APP_DEBUG:-false}"
|
||||
# Generated files (SQLite DB + JWT signing key) go here, on their own
|
||||
# volume so they don't overlap the bind mount below.
|
||||
# Generated files (SQLite DB + JWT signing key) go here, on the `storage`
|
||||
# volume below.
|
||||
STORAGE_PATH: /var/www/storage
|
||||
# Leave blank to auto-generate a secret into the storage volume on first run.
|
||||
JWT_SECRET: "${JWT_SECRET:-}"
|
||||
JWT_TTL: "${JWT_TTL:-86400}"
|
||||
# Magic links point at the frontend dev server.
|
||||
APP_URL: "${APP_URL:-http://localhost:5173}"
|
||||
# The SPA and the API are both served from this container.
|
||||
APP_URL: "${APP_URL:-http://localhost:8080}"
|
||||
# Deliver to the Mailpit catcher below; read mail at http://localhost:8025.
|
||||
MAIL_TRANSPORT: "${MAIL_TRANSPORT:-smtp}"
|
||||
MAIL_FROM: "${MAIL_FROM:-no-reply@todo.test}"
|
||||
@@ -25,9 +25,9 @@ services:
|
||||
MAIL_SMTP_PASSWORD: "${MAIL_SMTP_PASSWORD:-}"
|
||||
MAIL_SMTP_ENCRYPTION: "${MAIL_SMTP_ENCRYPTION:-none}"
|
||||
volumes:
|
||||
# Live source: edit on the host, no image rebuild needed.
|
||||
- .:/var/www/html
|
||||
# Persistent storage, kept outside /var/www/html to avoid a nested mount.
|
||||
# Only generated state is mounted. The app itself — PHP source and the
|
||||
# built frontend — is baked into the image; rebuild to pick up changes:
|
||||
# docker compose up -d --build
|
||||
- storage:/var/www/storage
|
||||
depends_on:
|
||||
- mailpit
|
||||
@@ -42,25 +42,5 @@ services:
|
||||
- "1025:1025" # SMTP (also reachable in-network as mailpit:1025)
|
||||
restart: unless-stopped
|
||||
|
||||
# Optional Vite dev server. Start it with: docker compose --profile frontend up -d
|
||||
# Without the profile, `docker compose up -d` runs the API alone.
|
||||
web:
|
||||
build: ./web
|
||||
image: php-project-manager-web
|
||||
profiles: ["frontend"]
|
||||
ports:
|
||||
- "5173:5173"
|
||||
environment:
|
||||
# /api is proxied to the API container on the compose network.
|
||||
VITE_PROXY_TARGET: "http://app:80"
|
||||
volumes:
|
||||
- ./web:/app
|
||||
# Keep the image's platform-specific node_modules; don't let the host's shadow them.
|
||||
- web_node_modules:/app/node_modules
|
||||
depends_on:
|
||||
- app
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
storage:
|
||||
web_node_modules:
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
DirectoryIndex index.php
|
||||
DirectoryIndex index.html index.php
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
|
||||
+16
-3
@@ -1,4 +1,17 @@
|
||||
# Route every request that isn't a real file through the front controller.
|
||||
# The built Vue SPA is served from this directory (baked into the Docker image);
|
||||
# the Slim front controller handles the API. Anything that isn't a real file is
|
||||
# a client-side route and falls back to index.html.
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [QSA,L]
|
||||
|
||||
# API: hand off to the front controller.
|
||||
RewriteRule ^api(/|$) index.php [QSA,L]
|
||||
|
||||
# Real files (hashed assets, favicon, manifest, service worker): serve directly.
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# SPA fallback when a build is present; otherwise the API front controller
|
||||
# (e.g. `composer serve` with no frontend built).
|
||||
RewriteCond %{DOCUMENT_ROOT}/index.html -f
|
||||
RewriteRule ^ index.html [L]
|
||||
RewriteRule ^ index.php [L]
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Development image: runs the Vite dev server. A production build image is a
|
||||
# later concern.
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies against this image's platform (musl), kept in a volume
|
||||
# by docker-compose so the host's node_modules never shadow them.
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 5173
|
||||
CMD ["npm", "run", "dev"]
|
||||
+2
-2
@@ -2,8 +2,8 @@ import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// The API the dev server proxies `/api` to. Defaults to the Dockerised API on
|
||||
// the host; the compose `web` service overrides it to the internal address.
|
||||
// Host-only dev server: proxy `/api` to the Dockerised API published on the
|
||||
// host. Override with VITE_PROXY_TARGET if the API runs elsewhere.
|
||||
const proxyTarget = process.env.VITE_PROXY_TARGET ?? 'http://localhost:8080'
|
||||
|
||||
// https://vite.dev/config/
|
||||
|
||||
Reference in New Issue
Block a user