From cb4df782a75681348b5eb67d8834dbaa232fc409 Mon Sep 17 00:00:00 2001 From: biss Date: Tue, 21 Jul 2026 11:10:51 +0800 Subject: [PATCH] docker --- .dockerignore | 21 +++++++++ .env.docker.example | 22 ++++++++++ .gitea/workflows/docker-publish.yml | 66 +++++++++++++++++++++++++++++ .gitignore | 1 + Dockerfile | 25 +++++++++++ README.md | 53 +++++++++++++++++++++++ compose.yaml | 22 ++++++++++ 7 files changed, 210 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.docker.example create mode 100644 .gitea/workflows/docker-publish.yml create mode 100644 Dockerfile create mode 100644 compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8f1d274 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +.git +.agents +.codex +node_modules +tests + +.env +.env.* +!.env.example +!.env.docker.example + +data/* +!data/.gitkeep + +*.log +npm-debug.log* +Dockerfile* +compose*.yml +compose*.yaml +README.md +LICENSE diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..fc777bb --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,22 @@ +# 首次启动前请复制为 .env.docker,并替换下面两个值。 +# TOTP_ENCRYPTION_KEY 必须至少 32 个字符,部署后不得随意更换。 +TOTP_ENCRYPTION_KEY=replace-with-a-random-secret-of-at-least-32-characters + +INITIAL_ADMIN_USERNAME=admin +INITIAL_ADMIN_PASSWORD=replace-with-a-strong-admin-password +INITIAL_ADMIN_DISPLAY_NAME=系统管理员 + +# 下列公开站点信息可按需修改。 +PUBLIC_SITE_NAME=海州市教育考试中心 +PUBLIC_SITE_CODE=HZ-EDU-032 +PUBLIC_SITE_PHONE=0518-8602 3158 +PUBLIC_SITE_ADDRESS=江苏省连云港市海州区文教路 18 号 +PUBLIC_SITE_EMAIL= +PUBLIC_SITE_HERO_EYEBROW=HAIZHOU EXAMINATION SERVICE +PUBLIC_SITE_HERO_TITLE=一个报名号, +PUBLIC_SITE_HERO_HIGHLIGHT=贯穿每一次考试。 +PUBLIC_SITE_HERO_DESCRIPTION=使用学校下发的报名号登录,完成密码更新和个人信息核验后,即可办理所有考试事项。 +PUBLIC_SITE_FOOTER_NOTICE=本平台展示数据仅用于系统演示 + +# 可选 Redis 缓存;容器外的 Redis 不应填写 127.0.0.1。 +# REDIS_URL=redis://redis-host:6379/0 diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml new file mode 100644 index 0000000..39c234e --- /dev/null +++ b/.gitea/workflows/docker-publish.yml @@ -0,0 +1,66 @@ +name: Build and publish Docker image + +on: + push: + branches: + - master + tags: + - "v*" + workflow_dispatch: + +jobs: + publish: + name: Test, build and publish + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: https://github.com/actions/checkout@v4 + + - name: Set up Node.js + uses: https://github.com/actions/setup-node@v4 + with: + node-version: "22" + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Set up QEMU + uses: https://github.com/docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: https://github.com/docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: https://github.com/docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate image tags and labels + id: metadata + uses: https://github.com/docker/metadata-action@v5 + with: + images: docker.io/${{ vars.DOCKERHUB_IMAGE }} + flavor: latest=false + tags: | + type=raw,value=latest,enable=${{ gitea.ref == 'refs/heads/master' }} + type=sha,format=short,prefix=sha- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and push image + uses: https://github.com/docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=registry,ref=docker.io/${{ vars.DOCKERHUB_IMAGE }}:buildcache + cache-to: type=registry,ref=docker.io/${{ vars.DOCKERHUB_IMAGE }}:buildcache,mode=max diff --git a/.gitignore b/.gitignore index 0c4a5a4..d316597 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ data/*.sqlite-shm data/*.sqlite-wal node_modules/ .env +.env.docker *.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0f3b319 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-bookworm-slim + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev --ignore-scripts \ + && npm cache clean --force + +COPY --chown=node:node . . + +ENV NODE_ENV=production \ + HOST=0.0.0.0 \ + PORT=4173 \ + DATABASE_CLIENT=sqlite \ + SQLITE_PATH=/app/data/exam.sqlite + +EXPOSE 4173 +VOLUME ["/app/data"] + +USER node + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || '4173') + '/api/public/home').then(response => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1));"] + +CMD ["node", "server.mjs"] diff --git a/README.md b/README.md index a2d5577..89cac56 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,59 @@ npm start 本地开发无需额外配置,首次运行会自动创建 `data/exam.sqlite` 和完整关系型数据库结构,但不会导入学校、考生、考试或报名测试数据。首次建库只写入系统基础配置和一个超级管理员;账号、密码和显示名可通过 `INITIAL_ADMIN_USERNAME`、`INITIAL_ADMIN_PASSWORD`、`INITIAL_ADMIN_DISPLAY_NAME` 设置。当前数据库结构版本为 v17;v16 数据库会自动增加 TOTP 字段,低于 v15 的开发库会提示重建。 +### Docker + +项目根目录包含生产镜像和 Docker Compose 配置。默认使用 SQLite,数据库保存在命名卷 `exam-information-data` 中,因此重建容器不会丢失数据。 + +先创建容器环境文件,并将其中的 TOTP 主密钥和初始管理员密码替换为安全随机值: + +```powershell +Copy-Item .env.docker.example .env.docker +``` + +然后构建并启动: + +```powershell +docker compose up --build --detach +``` + +启动完成后访问 。查看状态和日志可运行: + +```powershell +docker compose ps +docker compose logs --follow app +``` + +停止服务使用 `docker compose down`;该命令会保留数据库卷。只有明确需要删除全部 SQLite 数据时才使用 `docker compose down --volumes`。 + +也可以只构建镜像: + +```powershell +docker build --tag hengzhun-exam-system:local . +``` + +镜像默认监听 `0.0.0.0:4173`,以非 root 用户运行,并通过 `/api/public/home` 执行健康检查。需要连接 MySQL 或 Redis 时,用运行环境变量覆盖 `DATABASE_CLIENT`、`DATABASE_URL`/`MYSQL_*`、`REDIS_URL`;此时 SQLite 数据卷可以移除。 + +#### Gitea Actions 自动发布到 Docker Hub + +工作流位于 `.gitea/workflows/docker-publish.yml`。它会先安装依赖并运行测试,然后构建 `linux/amd64`、`linux/arm64` 双架构镜像并推送到 Docker Hub。 + +使用前需要完成以下配置: + +1. 在 Docker Hub 创建目标仓库,并创建具有该仓库 Read & Write 权限的访问令牌。 +2. 在 Gitea 仓库的 Actions Variables 中添加 `DOCKERHUB_IMAGE`,值为不带 registry 和 tag 的完整镜像名,例如 `yourname/exam-information-system`。 +3. 在 Gitea 仓库的 Actions Secrets 中添加 `DOCKERHUB_USERNAME` 和 `DOCKERHUB_TOKEN`。前者填写 Docker Hub 用户名,后者填写访问令牌,不要填写账户密码。 +4. 确保 Gitea Actions 已启用,并且 `ubuntu-latest` Runner 能访问 Docker daemon、GitHub 和 Docker Hub。双架构构建还需要 Runner 允许 QEMU 注册步骤运行。 + +推送到 `master` 后会发布 `latest` 和 `sha-<短提交号>`;推送形如 `v1.2.3` 的 Git 标签后会发布 `1.2.3`、`1.2` 和对应的提交标签。例如: + +```powershell +git tag v1.2.3 +git push origin v1.2.3 +``` + +工作流也支持从 Gitea Actions 页面手动运行。构建缓存保存为同一 Docker Hub 仓库中的 `buildcache` 标签,以加快后续构建。 + 需要清空并重建空业务库时运行 `npm run reset-db`。需要测试数据时再手动运行 `npm run seed-test-data`;导入脚本会读取项目根目录的 `.env`,并根据 `DATABASE_CLIENT` 选择 SQLite 或 MySQL。它会生成 4 所学校、360 名批量考生及 360 条不同状态的报名数据,并明确不生成考场编排计划和准考证。省市区县下拉数据位于 `src/data/china-regions.mjs`,当前版本为国家地名信息库截至 2025-12-31 的三级快照,并补入和康县(653228)与和安县(653229);从新版 CSV 更新时可运行 `node scripts/build-regions.mjs src/data/china-regions.mjs`。 ## 数据库配置 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..2aab980 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,22 @@ +services: + app: + build: + context: . + image: hengzhun-exam-system:local + init: true + restart: unless-stopped + env_file: + - .env.docker + environment: + NODE_ENV: production + HOST: 0.0.0.0 + PORT: 4173 + DATABASE_CLIENT: sqlite + SQLITE_PATH: /app/data/exam.sqlite + ports: + - "4173:4173" + volumes: + - exam-information-data:/app/data + +volumes: + exam-information-data: