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
|
.phpunit.result.cache
|
||||||
docs
|
docs
|
||||||
README.md
|
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).
|
# How long an issued token stays valid, in seconds (default: 86400 = 24h).
|
||||||
JWT_TTL=86400
|
JWT_TTL=86400
|
||||||
|
|
||||||
# Base URL of the frontend. Verification magic links point here, e.g.
|
# Base URL the app is reached at. Verification magic links point here, e.g.
|
||||||
# <APP_URL>/verify-email?token=... (default: http://localhost:5173).
|
# <APP_URL>/verify-email?token=... The Docker image serves the SPA and the API
|
||||||
APP_URL=http://localhost:5173
|
# together on http://localhost:8080; a host `npm run dev` serves it on :5173.
|
||||||
|
APP_URL=http://localhost:8080
|
||||||
|
|
||||||
# Email delivery.
|
# Email delivery.
|
||||||
# mail — PHP's built-in mail() function (default)
|
# mail — PHP's built-in mail() function (default)
|
||||||
|
|||||||
+17
@@ -1,5 +1,19 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# 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
|
FROM php:8.3-apache
|
||||||
|
|
||||||
# --- PHP extensions and CLI tools ----------------------------------------------
|
# --- PHP extensions and CLI tools ----------------------------------------------
|
||||||
@@ -30,6 +44,9 @@ RUN composer dump-autoload --optimize --no-dev \
|
|||||||
&& mkdir -p storage \
|
&& mkdir -p storage \
|
||||||
&& chown -R www-data:www-data 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 ------------------------
|
# --- Entrypoint: migrate, then hand off to Apache ------------------------
|
||||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|||||||
+7
-27
@@ -8,14 +8,14 @@ services:
|
|||||||
- "8080:80"
|
- "8080:80"
|
||||||
environment:
|
environment:
|
||||||
APP_DEBUG: "${APP_DEBUG:-false}"
|
APP_DEBUG: "${APP_DEBUG:-false}"
|
||||||
# Generated files (SQLite DB + JWT signing key) go here, on their own
|
# Generated files (SQLite DB + JWT signing key) go here, on the `storage`
|
||||||
# volume so they don't overlap the bind mount below.
|
# volume below.
|
||||||
STORAGE_PATH: /var/www/storage
|
STORAGE_PATH: /var/www/storage
|
||||||
# Leave blank to auto-generate a secret into the storage volume on first run.
|
# Leave blank to auto-generate a secret into the storage volume on first run.
|
||||||
JWT_SECRET: "${JWT_SECRET:-}"
|
JWT_SECRET: "${JWT_SECRET:-}"
|
||||||
JWT_TTL: "${JWT_TTL:-86400}"
|
JWT_TTL: "${JWT_TTL:-86400}"
|
||||||
# Magic links point at the frontend dev server.
|
# The SPA and the API are both served from this container.
|
||||||
APP_URL: "${APP_URL:-http://localhost:5173}"
|
APP_URL: "${APP_URL:-http://localhost:8080}"
|
||||||
# Deliver to the Mailpit catcher below; read mail at http://localhost:8025.
|
# Deliver to the Mailpit catcher below; read mail at http://localhost:8025.
|
||||||
MAIL_TRANSPORT: "${MAIL_TRANSPORT:-smtp}"
|
MAIL_TRANSPORT: "${MAIL_TRANSPORT:-smtp}"
|
||||||
MAIL_FROM: "${MAIL_FROM:-no-reply@todo.test}"
|
MAIL_FROM: "${MAIL_FROM:-no-reply@todo.test}"
|
||||||
@@ -25,9 +25,9 @@ services:
|
|||||||
MAIL_SMTP_PASSWORD: "${MAIL_SMTP_PASSWORD:-}"
|
MAIL_SMTP_PASSWORD: "${MAIL_SMTP_PASSWORD:-}"
|
||||||
MAIL_SMTP_ENCRYPTION: "${MAIL_SMTP_ENCRYPTION:-none}"
|
MAIL_SMTP_ENCRYPTION: "${MAIL_SMTP_ENCRYPTION:-none}"
|
||||||
volumes:
|
volumes:
|
||||||
# Live source: edit on the host, no image rebuild needed.
|
# Only generated state is mounted. The app itself — PHP source and the
|
||||||
- .:/var/www/html
|
# built frontend — is baked into the image; rebuild to pick up changes:
|
||||||
# Persistent storage, kept outside /var/www/html to avoid a nested mount.
|
# docker compose up -d --build
|
||||||
- storage:/var/www/storage
|
- storage:/var/www/storage
|
||||||
depends_on:
|
depends_on:
|
||||||
- mailpit
|
- mailpit
|
||||||
@@ -42,25 +42,5 @@ services:
|
|||||||
- "1025:1025" # SMTP (also reachable in-network as mailpit:1025)
|
- "1025:1025" # SMTP (also reachable in-network as mailpit:1025)
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
storage:
|
storage:
|
||||||
web_node_modules:
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
Options -Indexes +FollowSymLinks
|
Options -Indexes +FollowSymLinks
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
Require all granted
|
Require all granted
|
||||||
DirectoryIndex index.php
|
DirectoryIndex index.html index.php
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
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
|
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 { defineConfig } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
// The API the dev server proxies `/api` to. Defaults to the Dockerised API on
|
// Host-only dev server: proxy `/api` to the Dockerised API published on the
|
||||||
// the host; the compose `web` service overrides it to the internal address.
|
// host. Override with VITE_PROXY_TARGET if the API runs elsewhere.
|
||||||
const proxyTarget = process.env.VITE_PROXY_TARGET ?? 'http://localhost:8080'
|
const proxyTarget = process.env.VITE_PROXY_TARGET ?? 'http://localhost:8080'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
|
|||||||
Reference in New Issue
Block a user