feat: support standalone Docker runtime and internal CMS API

This commit is contained in:
2026-11-03 19:21:59 +08:00
parent 43edc632a5
commit d5c0aa7562
6 changed files with 99 additions and 4 deletions
+7
View File
@@ -0,0 +1,7 @@
.git
node_modules
.next
.env
.env.*
*.log
*.tsbuildinfo
+23
View File
@@ -0,0 +1,23 @@
# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ARG NEXT_PUBLIC_API_URL=http://localhost:18000
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
RUN node --test tests/*.test.cjs && npm run build
FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
HOSTNAME=0.0.0.0 \
PORT=3000
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
COPY --from=build --chown=node:node /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]
+1
View File
@@ -1,6 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone",
images: { images: {
// Wagtail 后端媒体地址(开发环境 + 生产环境国内 OSS/CDN 域名)。 // Wagtail 后端媒体地址(开发环境 + 生产环境国内 OSS/CDN 域名)。
// 详见 documents/设计方案分析与完善版.md §2.3。 // 详见 documents/设计方案分析与完善版.md §2.3。
+49 -3
View File
@@ -281,10 +281,33 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@emnapi/core": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.3",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.2.1", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -2243,6 +2266,29 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+4 -1
View File
@@ -3,7 +3,10 @@
* 详见 documents/设计方案分析与完善版.md §2.7 Headless API 设计规范。 * 详见 documents/设计方案分析与完善版.md §2.7 Headless API 设计规范。
*/ */
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; const PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
const API_BASE_URL = typeof window === "undefined"
? process.env.API_INTERNAL_URL || PUBLIC_API_URL
: PUBLIC_API_URL;
export class ApiError extends Error { export class ApiError extends Error {
constructor( constructor(
+15
View File
@@ -137,6 +137,21 @@ test("page fetches share the publish cache tag without changing snippet tags", a
assert.equal(calls[1].next.tags.join(","), "site-settings"); assert.equal(calls[1].next.tags.join(","), "site-settings");
}); });
test("API client separates container internal and browser addresses", async () => {
const env = { NEXT_PUBLIC_API_URL: "http://localhost:18000", API_INTERNAL_URL: "http://backend:8000" };
for (const browser of [false, true]) {
let requestedURL;
const globals = {
process: { env },
fetch: async (url) => { requestedURL = url; return { ok: true, json: async () => ({}) }; },
...(browser ? { window: {} } : {}),
};
const { fetchAPI } = loadModule("services/api-client.ts", {}, globals);
await fetchAPI("/api/v2/pages/");
assert.equal(requestedURL, `${browser ? env.NEXT_PUBLIC_API_URL : env.API_INTERNAL_URL}/api/v2/pages/`);
}
});
test("authenticated webhook expires shared cache immediately and rejects unauthorized requests", async () => { test("authenticated webhook expires shared cache immediately and rejects unauthorized requests", async () => {
const calls = []; const calls = [];
const { POST } = loadModule("app/api/revalidate/route.ts", { const { POST } = loadModule("app/api/revalidate/route.ts", {