fix(部署): 补齐直连前端时的 API 反代

This commit is contained in:
2026-07-03 16:44:44 +08:00
parent 7f4d83f57a
commit 8c4945a03c
7 changed files with 71 additions and 0 deletions

View File

@@ -18,6 +18,8 @@ REDIS_URL=redis://redis:6379
# Web # Web
NEXT_PUBLIC_API_URL=/api/v1 NEXT_PUBLIC_API_URL=/api/v1
# Fallback used by the Next.js server if the web container is exposed directly.
NEXT_API_PROXY_TARGET=http://server:3001
NEXTAUTH_URL=http://localhost:8080 NEXTAUTH_URL=http://localhost:8080
NEXTAUTH_SECRET=local-server-change-me NEXTAUTH_SECRET=local-server-change-me

View File

@@ -19,6 +19,8 @@ REDIS_URL=redis://redis:6379
# Web # Web
# For same-domain Nginx deployment, keep this relative path so browser requests go to /api/v1. # For same-domain Nginx deployment, keep this relative path so browser requests go to /api/v1.
NEXT_PUBLIC_API_URL=/api/v1 NEXT_PUBLIC_API_URL=/api/v1
# Fallback used by the Next.js server if the web container is exposed directly.
NEXT_API_PROXY_TARGET=http://server:3001
NEXTAUTH_URL=https://pm.example.com NEXTAUTH_URL=https://pm.example.com
NEXTAUTH_SECRET=change-me-random-32-byte-secret NEXTAUTH_SECRET=change-me-random-32-byte-secret

View File

@@ -18,7 +18,9 @@ RUN pnpm config set fetch-retries 5 \
FROM deps AS builder FROM deps AS builder
ARG NEXT_PUBLIC_API_URL=/api/v1 ARG NEXT_PUBLIC_API_URL=/api/v1
ARG NEXT_API_PROXY_TARGET=
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_API_PROXY_TARGET=$NEXT_API_PROXY_TARGET
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production ENV NODE_ENV=production
COPY packages/shared packages/shared COPY packages/shared packages/shared
@@ -28,7 +30,9 @@ RUN pnpm --filter web build
FROM base AS runner FROM base AS runner
ARG NEXT_PUBLIC_API_URL=/api/v1 ARG NEXT_PUBLIC_API_URL=/api/v1
ARG NEXT_API_PROXY_TARGET=
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_API_PROXY_TARGET=$NEXT_API_PROXY_TARGET
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production ENV NODE_ENV=production
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json

View File

@@ -20,3 +20,45 @@ test('next dev keeps watch ignored entries compatible with webpack schema', () =
assert.deepEqual(ignoredList, ['**/.tmp-test/**']); assert.deepEqual(ignoredList, ['**/.tmp-test/**']);
}); });
test('next config proxies API requests when the web container is exposed directly', async () => {
const previousTarget = process.env.NEXT_API_PROXY_TARGET;
process.env.NEXT_API_PROXY_TARGET = 'http://server:3001';
try {
const rewrites = await nextConfig.rewrites();
assert.deepEqual(rewrites, [
{
source: '/api/v1/:path*',
destination: 'http://server:3001/api/v1/:path*',
},
]);
} finally {
if (previousTarget === undefined) {
delete process.env.NEXT_API_PROXY_TARGET;
} else {
process.env.NEXT_API_PROXY_TARGET = previousTarget;
}
}
});
test('next config accepts API proxy targets that already include the API prefix', async () => {
const previousTarget = process.env.NEXT_API_PROXY_TARGET;
process.env.NEXT_API_PROXY_TARGET = 'http://server:3001/api/v1/';
try {
const rewrites = await nextConfig.rewrites();
assert.deepEqual(rewrites, [
{
source: '/api/v1/:path*',
destination: 'http://server:3001/api/v1/:path*',
},
]);
} finally {
if (previousTarget === undefined) {
delete process.env.NEXT_API_PROXY_TARGET;
} else {
process.env.NEXT_API_PROXY_TARGET = previousTarget;
}
}
});

View File

@@ -7,8 +7,25 @@ function toIgnoredList(ignored) {
return entries.filter((entry) => typeof entry === 'string' && entry.length > 0); return entries.filter((entry) => typeof entry === 'string' && entry.length > 0);
} }
function toApiProxyDestination(target) {
const cleanTarget = target.trim().replace(/\/+$/, '');
if (!cleanTarget) return null;
const apiBase = cleanTarget.endsWith('/api/v1') ? cleanTarget : `${cleanTarget}/api/v1`;
return `${apiBase}/:path*`;
}
const nextConfig = { const nextConfig = {
transpilePackages: ['@ftb/shared'], transpilePackages: ['@ftb/shared'],
async rewrites() {
const destination = toApiProxyDestination(process.env.NEXT_API_PROXY_TARGET || '');
if (!destination) return [];
return [
{
source: '/api/v1/:path*',
destination,
},
];
},
webpack(config, { dev }) { webpack(config, { dev }) {
if (dev) { if (dev) {
const ignored = toIgnoredList(config.watchOptions?.ignored); const ignored = toIgnoredList(config.watchOptions?.ignored);

View File

@@ -68,10 +68,12 @@ services:
dockerfile: Dockerfile.web dockerfile: Dockerfile.web
args: args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1} NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1}
NEXT_API_PROXY_TARGET: ${NEXT_API_PROXY_TARGET:-http://server:3001}
restart: unless-stopped restart: unless-stopped
environment: environment:
NODE_ENV: production NODE_ENV: production
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1} NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1}
NEXT_API_PROXY_TARGET: ${NEXT_API_PROXY_TARGET:-http://server:3001}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:8080} NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:8080}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-local-server-change-me} NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-local-server-change-me}
depends_on: depends_on:

View File

@@ -68,10 +68,12 @@ services:
dockerfile: Dockerfile.web dockerfile: Dockerfile.web
args: args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1} NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1}
NEXT_API_PROXY_TARGET: ${NEXT_API_PROXY_TARGET:-http://server:3001}
restart: unless-stopped restart: unless-stopped
environment: environment:
NODE_ENV: production NODE_ENV: production
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1} NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1}
NEXT_API_PROXY_TARGET: ${NEXT_API_PROXY_TARGET:-http://server:3001}
NEXTAUTH_URL: ${NEXTAUTH_URL:-} NEXTAUTH_URL: ${NEXTAUTH_URL:-}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-} NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-}
depends_on: depends_on: