From 9c6fc2e1dbf70e22a07696e11863f8c3a50d50f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=B2=A9=E5=B2=A9?= Date: Thu, 25 Dec 2025 17:26:32 +0800 Subject: [PATCH] =?UTF-8?q?init:=20=E5=A2=9E=E5=8A=A0=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build.yaml | 46 ++++++++++++++ .gitea/workflows/dispatch.yaml | 64 +++++++++++++++++++ Dockerfile | 8 +++ README.md | 109 +++++++++++++++++++++++++++++++++ example/docker-compose.yml | 12 ++++ example/site/index.html | 17 +++++ nginx/default.conf | 47 ++++++++++++++ 7 files changed, 303 insertions(+) create mode 100644 .gitea/workflows/build.yaml create mode 100644 .gitea/workflows/dispatch.yaml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 example/docker-compose.yml create mode 100644 example/site/index.html create mode 100644 nginx/default.conf diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..738a957 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,46 @@ +name: Build and Push Image + +on: + push: + tags: + - v** + - V** + +env: + REGISTRY_HOST: gitea.proxy.liyanyan.work + +jobs: + build: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout env + run: | + echo "目标名称:${{ gitea.ref_name }}" + + TAG_NAME=${{ gitea.ref_name }} + IMAGE_TAG="${REGISTRY_HOST}/${{ gitea.repository }}:${TAG_NAME}" + echo "IMAGE_TAG=${IMAGE_TAG}" + IMAGE_LATEST_TAG="${REGISTRY_HOST}/${{ gitea.repository }}:latest" + echo "IMAGE_LATEST_TAG=${IMAGE_LATEST_TAG}" + + if [ -z "${{ secrets.REGISTRY_USER }}" ]; then echo "REGISTRY_USER is empty"; else echo "REGISTRY_USER is set"; fi + if [ -z "${{ secrets.REGISTRY_PASSWORD }}" ]; then echo "REGISTRY_PASSWORD is empty"; else echo "REGISTRY_PASSWORD is set"; fi + + - name: Checkout + uses: https://gitea.proxy.liyanyan.work/github/checkout@v4 + with: + ref: ${{ gitea.ref_name }} + + - name: Build Docker image + run: | + docker build -t "${IMAGE_TAG}" -t "${IMAGE_LATEST_TAG}" . + + - name: Docker login + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY_HOST}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin + + - name: Build and push + run: | + docker push ${IMAGE_TAG} + docker push ${IMAGE_LATEST_TAG} diff --git a/.gitea/workflows/dispatch.yaml b/.gitea/workflows/dispatch.yaml new file mode 100644 index 0000000..87dffbe --- /dev/null +++ b/.gitea/workflows/dispatch.yaml @@ -0,0 +1,64 @@ +on: + workflow_dispatch: + inputs: + myChoice: + description: '请选择分支' + type: choice + required: true + options: + - branch + - tag + +jobs: + Explore-Gitea-Actions: + runs-on: ubuntu-22.04 + env: + # 手动设置 + MY_CHOICE: ${{ inputs.myChoice }} + REGISTRY_HOST: gitea.proxy.liyanyan.work + + # 内置 + GITEA_REPO: ${{ gitea.repository }} + GITEA_REF_NAME: ${{ gitea.ref_name }} + GITEA_SERVER_URL: ${{ gitea.server_url }} + + steps: + - run: | + echo MY_CHOICE: ${MY_CHOICE}; + echo REGISTRY_HOST: ${REGISTRY_HOST}; + echo GITEA_REPO: ${GITEA_REPO}; + echo GITEA_REF_NAME: ${GITEA_REF_NAME}; + echo GITEA_SERVER_URL: ${GITEA_SERVER_URL}; + + - name: Checkout env + run: | + echo "目标名称:${GITEA_REF_NAME}" + + if [ "${MY_CHOICE}" = "tag" ]; then + TAG_NAME="${GITEA_REF_NAME}" + else + TAG_NAME="latest" + fi + + IMAGE_TAG="${REGISTRY_HOST}/${GITEA_REPO}:${TAG_NAME}" + echo "IMAGE_TAG=${IMAGE_TAG}" + + if [ -z "${{ secrets.REGISTRY_USER }}" ]; then echo "REGISTRY_USER is empty"; else echo "REGISTRY_USER is set"; fi + if [ -z "${{ secrets.REGISTRY_PASSWORD }}" ]; then echo "REGISTRY_PASSWORD is empty"; else echo "REGISTRY_PASSWORD is set"; fi + + - name: Checkout + uses: https://gitea.proxy.liyanyan.work/github/checkout@v4 + with: + ref: ${{ GITEA_REF_NAME }} + + - name: Build Docker image + run: | + docker build -t "${IMAGE_TAG}" . + + - name: Docker login + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY_HOST}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin + + - name: Build and push + run: | + docker push ${IMAGE_TAG} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e91691b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM nginx:alpine + +# Copy our default server config; site content is not baked into the image. +COPY nginx/default.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..95391a0 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +### 项目说明 + +本项目用于构建通用的静态web服务docker模版,纯静态web服务可以使用本项目产出的docker镜像快速搭建稳定的线上服务。 + + +### 原理说明 + +每次创建新 tag 时触发工作流,同时发布 tag名和latest两个镜像版本到gitea自动的docker管理平台。 +使用Dockerfile构建基于nginx的镜像,具体使用中,可以宿主机 default.conf / nginx 工作目录到宿主机,宿主机只需要首次部署时构建 docker-compose,后续部署只需替换nginx目录即可。 + + +### 用法说明 + +#### 设计约束 + +- 基础镜像:nginx:alpine(体积小) +- 不考虑 TLS(HTTPS 请由外部反向代理/网关处理) +- 站点内容不随镜像打包,运行时挂载 `./site` 到容器 `/usr/share/nginx/html` + +#### 目录结构 + +``` +. +├─ Dockerfile +├─ nginx/ +│ └─ default.conf # Nginx 静态站点配置(可按项目调整) +├─ example/ +│ ├─ docker-compose.yml # 本地/线上均可复用的示例 +│ └─ site/ # 示例静态资源(仅本地验证用) +└─ .gitea/workflows/ + └─ build.yml # 基于 tag 的构建与推送 +``` + +#### 本地构建镜像 + +```bash +docker build -t static-web:latest . +``` + +#### 使用 docker 命令启动服务 + +```bash +# 在仓库根目录下准备你的站点目录 +mkdir -p site && echo '

Hello

' > site/index.html + +docker run -d \ + --name static-web \ + -p 8080:80 \ + -v "$(pwd)/nginx":/etc/nginx/conf.d:ro \ + -v "$(pwd)/site":/usr/share/nginx/html:ro \ + static-web:latest + +# 访问: http://localhost:8080 +``` + + +#### 使用 docker-compose.yml 命令启动服务 + +```bash +cd example +# 可直接使用示例站点:example/site/index.html +docker compose up -d + +# 访问: http://localhost:8080 +``` + +### 版本与发布 + +- 推荐使用语义化标签发布:例如 `v1.0.0`、`v1.0.1`。 +- 推送 tag 后,CI 会构建并推送两个镜像标签:`` 与 `latest`。 + +### CI/CD(Gitea Actions) + +工作流文件:`.gitea/workflows/build.yml` + +- 触发条件:push tag(匹配 `v*`)。 +- 必需 secrets(在仓库设置中配置): + - `REGISTRY_HOST`:Docker Registry 主机(例如 `registry.example.com`)。 + - `REGISTRY_USERNAME` / `REGISTRY_PASSWORD`:用于登录 Registry 的凭据。 + - `REGISTRY_IMAGE`:完整镜像名(例如 `registry.example.com/org/static-web`)。 +- Runner 需具备 Docker 执行环境;`runs-on` 使用的标签可按你的 Runner 调整。 + +本地验证镜像成功推送后,线上 `docker-compose.yml` 可使用远程镜像: + +```yaml +services: + web: + image: registry.example.com/org/static-web:latest + ports: + - "8080:80" + volumes: + - ./nginx:/etc/nginx/conf.d:ro + - ./site:/usr/share/nginx/html:ro +``` + +### Nginx 配置要点 + +- 根目录:`/usr/share/nginx/html`(通过卷挂载提供内容)。 +- Gzip:对 CSS/JS/JSON/SVG 等启用压缩。 +- 缓存:对静态资源(js/css/img/fonts)设置长缓存;HTML 禁止缓存。 +- 安全:默认开启 `X-Content-Type-Options`、`X-Frame-Options`、`Referrer-Policy`。`CSP` 默认注释,如需请按项目定制。 +- SPA 场景:若需前端路由降级到 `index.html`,可将 `location /` 中的 `try_files` 调整为 `try_files $uri /index.html;`。 + +### 常见问题 + +- 端口冲突:修改 `-p 8080:80` 映射或释放宿主机端口。 +- 权限问题:确保宿主机 `./nginx` 与 `./site` 对 Runner/部署主机可读;挂载只读可避免误改。 +- 缓存过期:生产环境建议为静态资源加内容哈希;或调整 `Cache-Control` 策略。 +- CI 无法登录 Registry:核对 `REGISTRY_HOST`、用户名/密码与网络可达性,Runner 需具备 `docker login` 能力。 \ No newline at end of file diff --git a/example/docker-compose.yml b/example/docker-compose.yml new file mode 100644 index 0000000..ded9e3c --- /dev/null +++ b/example/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" + +services: + static-web: + image: gitea.proxy.liyanyan.work/liyy/static-web:latest + container_name: static-web + ports: + - "8080:80" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:rw + - ./site:/usr/share/nginx/html:ro + restart: unless-stopped diff --git a/example/site/index.html b/example/site/index.html new file mode 100644 index 0000000..bbb5acc --- /dev/null +++ b/example/site/index.html @@ -0,0 +1,17 @@ + + + + + + 静态站点示例 + + + +

静态站点运行正常 ✅

+

这是用于本地验证的示例页面。你可以把自己的静态资源挂载到 ./site

+

容器内访问路径为 /usr/share/nginx/html

+ + diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..1adc3ab --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,47 @@ +# Minimal static site config (no TLS). Site is mounted at runtime. + +server { + listen 80; + server_name _; + + # Static root (mounted from host: ./site -> /usr/share/nginx/html) + root /usr/share/nginx/html; + index index.html; + + # Gzip for common text types + gzip on; + gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml; + gzip_min_length 1024; + gzip_comp_level 5; + + # Security headers (adjust per project needs) + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + # add_header Content-Security-Policy "default-src 'self'" always; # Consider tailoring CSP; may break 3rd-party assets + + # Default: serve files directly; 404 if missing + location / { + try_files $uri $uri/ =404; + } + + # Long cache for static assets by extension + location ~* \.(?:ico|css|js|mjs|gif|jpe?g|png|svg|webp|woff2?|ttf)$ { + expires 30d; + access_log off; + add_header Cache-Control "public, max-age=2592000, immutable"; + } + + # Avoid caching HTML to ensure fresh content + location ~* \.(?:html)$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Optional: enable basic status (restrict in production) + # location /__status { + # stub_status; + # allow 127.0.0.1; + # deny all; + # } +}