diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0eb61ee --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +.gitea +node_modules +dist +dist-ssr +coverage +.vite +*.log +README.md +Dockerfile +.dockerignore diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..a26dabf --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,59 @@ +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +# 同一分支只保留最新一次部署,避免并发发布互相覆盖容器 +concurrency: + group: deploy-main + cancel-in-progress: true + +env: + IMAGE: demo + CONTAINER: demo + HOST_PORT: "9121" + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 构建镜像 + run: | + docker build \ + --label "org.opencontainers.image.revision=${{ github.sha }}" \ + -t "$IMAGE:latest" \ + -t "$IMAGE:${{ github.sha }}" \ + . + + - name: 滚动重启容器 + run: | + docker rm -f "$CONTAINER" 2>/dev/null || true + docker run -d \ + --name "$CONTAINER" \ + --restart unless-stopped \ + -p "127.0.0.1:${HOST_PORT}:80" \ + "$IMAGE:latest" + + - name: 健康检查 + run: | + for i in $(seq 1 15); do + code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${HOST_PORT}/" || true) + if [ "$code" = "200" ]; then + echo "容器已就绪,HTTP $code" + exit 0 + fi + sleep 2 + done + echo "健康检查失败,最后状态码:$code" + docker logs --tail 50 "$CONTAINER" || true + exit 1 + + - name: 清理旧镜像 + run: | + # 保留 latest 与当前提交的镜像,其余同仓库的悬空/历史镜像清掉 + docker image prune -f || true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..211b288 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# ---- 构建阶段 ---- +FROM node:24-alpine AS builder + +WORKDIR /app + +# 先只拷贝依赖清单,配合 Docker 层缓存:源码变动时不会重复装依赖 +COPY package.json package-lock.json ./ +RUN npm ci --no-audit --no-fund + +COPY . . +RUN npm run build + +# ---- 运行阶段 ---- +FROM nginx:1.27-alpine + +COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/README.md b/README.md index f3ea3bd..ae00d0c 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,23 @@ npm run typecheck # 仅做 TypeScript 类型检查 ## 下一步 把 `src/pages/HomePage.tsx` 和 `src/components/DemoForm.tsx` 换成自己的页面即可。如需路由、状态管理或请求库,可自行添加 React Router、Zustand、TanStack Query 等依赖。 + +## 部署 + +推送到 `main` 分支会触发 Gitea Actions(`.gitea/workflows/deploy.yml`),自动构建 Docker 镜像并在本机滚动重启容器。 + +| 项目 | 值 | +| --- | --- | +| 生产域名 | https://demo.chener.ymen.site | +| 容器名 / 镜像 | `demo` | +| 宿主机监听 | `127.0.0.1:9121` → 容器 `80` | +| 反向代理 | Caddy(`/etc/caddy/Caddyfile`)自动签发证书 | + +镜像为多阶段构建:`node:24-alpine` 里执行 `npm ci && npm run build`,产物交给 `nginx:1.27-alpine` 提供服务,`deploy/nginx.conf` 负责 SPA 回退与静态资源缓存策略。 + +本地手动验证构建: + +```bash +docker build -t demo:latest . +docker run --rm -p 127.0.0.1:9121:80 demo:latest +``` diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..1befdfb --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # 容器内健康检查、日志都走 stdout + access_log /dev/stdout; + error_log /dev/stderr warn; + + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + + # 带 hash 的构建产物可以长缓存 + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # 入口 HTML 不缓存,保证发版后立刻生效 + location = /index.html { + add_header Cache-Control "no-cache, must-revalidate"; + } + + # SPA 路由回退 + location / { + try_files $uri $uri/ /index.html; + } +}