This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# 依赖目录:必须排除,否则 COPY . . 会把本机 node_modules
|
||||
# 覆盖容器内刚装好的 Linux 版本
|
||||
node_modules
|
||||
|
||||
# 构建产物(容器内会重新构建)
|
||||
.next
|
||||
out
|
||||
dist
|
||||
|
||||
# 环境变量与密钥,绝不进镜像
|
||||
.env
|
||||
.env.local
|
||||
.env*.local
|
||||
|
||||
# 版本控制与开发文件
|
||||
.git
|
||||
.gitignore
|
||||
.gitea
|
||||
*.md
|
||||
@@ -0,0 +1,123 @@
|
||||
name: aboutme
|
||||
run-name: "${{ gitea.actor }} 正在部署 aboutme"
|
||||
|
||||
env:
|
||||
IMAGE_NAME: aboutme
|
||||
BASE_IMAGE: aboutme-base
|
||||
HOST_PORT: 22081
|
||||
CONTAINER_PORT: 3000
|
||||
DOCKER_BUILDKIT: "0"
|
||||
|
||||
on:
|
||||
- push
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on:
|
||||
- vps_jp
|
||||
steps:
|
||||
- name: 环境准备
|
||||
run: |
|
||||
STEP_START=$(date +%s)
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "📦 安装 docker cli..."
|
||||
apk add docker-cli
|
||||
fi
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "📦 安装 git..."
|
||||
apk add git
|
||||
fi
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "📦 安装 nodejs..."
|
||||
apk add nodejs
|
||||
fi
|
||||
docker --version
|
||||
echo "🧱 BuildKit: ${DOCKER_BUILDKIT}"
|
||||
echo "✅ 环境准备耗时: $(( $(date +%s) - STEP_START ))s"
|
||||
|
||||
- name: 克隆仓库代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 构建基础镜像(首次或 Dockerfile.base 变更时)
|
||||
run: |
|
||||
STEP_START=$(date +%s)
|
||||
|
||||
BASE_HASH=$(sha256sum Dockerfile.base | cut -c1-12)
|
||||
CURRENT_HASH=$(docker inspect $BASE_IMAGE:latest --format '{{index .Config.Labels "base.hash"}}' 2>/dev/null || echo "none")
|
||||
|
||||
if [ "$BASE_HASH" != "$CURRENT_HASH" ]; then
|
||||
echo "📦 基础镜像需要构建 (hash: $BASE_HASH)"
|
||||
docker build \
|
||||
--label "base.hash=$BASE_HASH" \
|
||||
-f Dockerfile.base \
|
||||
-t $BASE_IMAGE:latest .
|
||||
echo "✅ 基础镜像构建完成"
|
||||
else
|
||||
echo "✅ 基础镜像已是最新,跳过 (hash: $BASE_HASH)"
|
||||
fi
|
||||
echo "✅ 基础镜像步骤耗时: $(( $(date +%s) - STEP_START ))s"
|
||||
|
||||
- name: 构建应用镜像
|
||||
run: |
|
||||
STEP_START=$(date +%s)
|
||||
|
||||
echo "📦 开始构建应用镜像..."
|
||||
docker build -t $IMAGE_NAME:latest .
|
||||
echo "✅ 应用镜像构建完成"
|
||||
docker images $IMAGE_NAME:latest --format " 大小: {{.Size}}"
|
||||
echo "✅ 应用镜像步骤耗时: $(( $(date +%s) - STEP_START ))s"
|
||||
|
||||
- name: 部署容器
|
||||
run: |
|
||||
STEP_START=$(date +%s)
|
||||
|
||||
echo "🔄 停止旧容器..."
|
||||
if docker ps -a --filter "name=^${IMAGE_NAME}$" -q | grep -q .; then
|
||||
docker stop --timeout 15 $IMAGE_NAME || true
|
||||
docker rm $IMAGE_NAME || true
|
||||
else
|
||||
echo " 无旧容器需要清理"
|
||||
fi
|
||||
|
||||
echo "🚀 启动新容器..."
|
||||
docker run -d \
|
||||
--name $IMAGE_NAME \
|
||||
-p 127.0.0.1:$HOST_PORT:$CONTAINER_PORT \
|
||||
-e TZ=Asia/Shanghai \
|
||||
--restart unless-stopped \
|
||||
$IMAGE_NAME:latest
|
||||
|
||||
echo "⏳ 等待容器启动/健康检查..."
|
||||
MAX_WAIT=60
|
||||
WAITED=0
|
||||
while [ $WAITED -lt $MAX_WAIT ]; do
|
||||
RUNNING_ID=$(docker ps --filter "name=^${IMAGE_NAME}$" --filter "status=running" -q)
|
||||
if [ -n "$RUNNING_ID" ]; then
|
||||
HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' $IMAGE_NAME 2>/dev/null || echo "unknown")
|
||||
if [ "$HEALTH" = "healthy" ] || [ "$HEALTH" = "none" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
WAITED=$((WAITED + 2))
|
||||
done
|
||||
|
||||
if docker ps --filter "name=$IMAGE_NAME" --filter "status=running" -q | grep -q .; then
|
||||
echo "✅ 容器运行中"
|
||||
docker ps --filter "name=$IMAGE_NAME" --format " 状态: {{.Status}}"
|
||||
echo " 启动等待: ${WAITED}s"
|
||||
else
|
||||
echo "❌ 容器启动失败"
|
||||
docker inspect $IMAGE_NAME --format ' ExitCode={{.State.ExitCode}} Error={{.State.Error}} StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' 2>/dev/null || true
|
||||
docker logs --tail 30 $IMAGE_NAME
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 部署步骤耗时: $(( $(date +%s) - STEP_START ))s"
|
||||
|
||||
- name: 查看启动日志
|
||||
run: |
|
||||
STEP_START=$(date +%s)
|
||||
echo "📋 容器最近日志:"
|
||||
docker logs --tail 15 $IMAGE_NAME
|
||||
echo "✅ 日志步骤耗时: $(( $(date +%s) - STEP_START ))s"
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
FROM aboutme-base:latest AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# 复制包管理文件,利用 Docker 层缓存
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 复制源码并构建(standalone 产物给运行阶段)
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN npm install -g pnpm@10
|
||||
@@ -1,5 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Docker 运行阶段只拷 .next/standalone + static
|
||||
output: 'standalone',
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user