chore:Initial addition of Docker related content

This commit is contained in:
david_bai
2025-09-11 06:44:43 +08:00
parent 6f8f4f65bb
commit 158433bb0b
19 changed files with 3653 additions and 3 deletions
+68
View File
@@ -0,0 +1,68 @@
# 多阶段构建 - 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
# 安装构建依赖
RUN apk add --no-cache libc6-compat
# 复制package文件
COPY package*.json ./
COPY pnpm-lock.yaml ./
# 安装pnpm
RUN npm install -g pnpm
# 安装依赖
RUN pnpm install --frozen-lockfile
# 复制源代码
COPY . .
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_ENV production
# 构建应用
RUN pnpm build
# 生产阶段
FROM node:18-alpine AS runner
WORKDIR /app
# 安装运行时依赖
RUN apk add --no-cache \
curl \
dumb-init \
&& rm -rf /var/cache/apk/*
# 创建非root用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001 -G nodejs
# 复制构建产物
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 设置环境变量
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
USER nextjs
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1
# 使用dumb-init作为PID 1
ENTRYPOINT ["dumb-init", "--"]
# 启动应用
CMD ["node", "server.js"]
+117
View File
@@ -0,0 +1,117 @@
import { NextRequest, NextResponse } from 'next/server';
const startTime = Date.now();
export async function GET(request: NextRequest) {
try {
const errors: string[] = [];
let status = 'healthy';
// 基础健康信息
const basicHealth = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-frontend',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development'
};
// 检查后端API连接
const backendHealth = await checkBackendHealth();
if (backendHealth.status !== 'connected') {
errors.push('Backend API connection failed');
status = 'degraded';
}
// 系统信息
const systemInfo = {
runtime: process.env.NEXT_RUNTIME || 'nodejs',
nextjs: {
version: process.version,
platform: process.platform,
arch: process.arch
},
memory: process.memoryUsage ? {
used: formatBytes(process.memoryUsage().heapUsed),
total: formatBytes(process.memoryUsage().heapTotal),
percent: Math.round((process.memoryUsage().heapUsed / process.memoryUsage().heapTotal) * 100)
} : null
};
const detailedHealth = {
...basicHealth,
status,
dependencies: {
backend: backendHealth
},
system: systemInfo,
...(errors.length > 0 && { errors })
};
const httpStatus = status === 'healthy' ? 200 : 503;
return NextResponse.json(detailedHealth, { status: httpStatus });
} catch (error) {
console.error('Detailed frontend health check error:', error);
return NextResponse.json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-frontend',
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 503 });
}
}
// 检查后端API健康状态
async function checkBackendHealth() {
try {
const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const start = Date.now();
const response = await fetch(`${backendUrl}/health`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
// 设置超时时间
signal: AbortSignal.timeout(5000)
});
const responseTime = Date.now() - start;
if (response.ok) {
const data = await response.json();
return {
status: 'connected',
responseTime,
backendUrl,
backendService: data.service || 'unknown'
};
} else {
return {
status: 'error',
responseTime,
backendUrl,
httpStatus: response.status,
error: `HTTP ${response.status}`
};
}
} catch (error) {
return {
status: 'disconnected',
backendUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// 格式化字节数
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
+30
View File
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
const startTime = Date.now();
export async function GET(request: NextRequest) {
try {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-frontend',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
nextjs: {
version: process.env.NEXT_RUNTIME || 'nodejs'
}
};
return NextResponse.json(health, { status: 200 });
} catch (error) {
console.error('Frontend health check error:', error);
return NextResponse.json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-frontend',
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 503 });
}
}
+6 -1
View File
@@ -20,7 +20,12 @@ const nextConfig = {
},
]
},
// 启用standalone输出模式,用于Docker部署
output: 'standalone',
// 禁用telemetry
experimental: {
instrumentationHook: true,
},
}
export default withMDX(nextConfig);