From 2a419697717b3d556144e6e976cf0549557ce289 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Tue, 5 May 2026 08:45:58 +0800 Subject: [PATCH] feat(deploy): add center host docker compose stack for git, registry and s3 hosting --- deploy/center/.env.example | 2 + deploy/center/.gitignore | 4 + deploy/center/README.md | 100 ++++++++++++++++++ deploy/center/caddy/Caddyfile | 63 ++++++++++++ deploy/center/docker-compose.yml | 117 ++++++++++++++++++++++ deploy/center/garage/bootstrap.sh | 31 ++++++ deploy/center/garage/garage.toml.template | 26 +++++ deploy/center/init.sh | 50 +++++++++ deploy/center/zot/config.json | 50 +++++++++ 9 files changed, 443 insertions(+) create mode 100644 deploy/center/.env.example create mode 100644 deploy/center/.gitignore create mode 100644 deploy/center/README.md create mode 100644 deploy/center/caddy/Caddyfile create mode 100644 deploy/center/docker-compose.yml create mode 100755 deploy/center/garage/bootstrap.sh create mode 100644 deploy/center/garage/garage.toml.template create mode 100755 deploy/center/init.sh create mode 100644 deploy/center/zot/config.json diff --git a/deploy/center/.env.example b/deploy/center/.env.example new file mode 100644 index 0000000..be8122e --- /dev/null +++ b/deploy/center/.env.example @@ -0,0 +1,2 @@ +GITEA_DOMAIN=git.juwan.xhttp.zip +RUNNER_TOKEN= diff --git a/deploy/center/.gitignore b/deploy/center/.gitignore new file mode 100644 index 0000000..65c47c8 --- /dev/null +++ b/deploy/center/.gitignore @@ -0,0 +1,4 @@ +/secrets/ +/garage/garage.toml +/zot/htpasswd +/.env diff --git a/deploy/center/README.md b/deploy/center/README.md new file mode 100644 index 0000000..0204070 --- /dev/null +++ b/deploy/center/README.md @@ -0,0 +1,100 @@ +# 管理机部署 + +Zot(容器仓库)、Garage(对象存储)、Gitea(代码 + Actions Runner)、Caddy(HTTPS 反代 + 业务入口),全部在同一台 center 机器上以 Docker Compose 运行。业务服务部署在另一台 k01 机器上,公网流量经由 center 的 Caddy 反代到 k01 上的 envoy-gateway NodePort。 + +部署机参考:center(Vultr High Frequency / 1 vCPU / 1 GB RAM / 32 GB NVMe)。 + +## 前置条件 + +- Docker Engine 与 compose v2 +- `apache2-utils`(提供 `htpasswd` 命令,用于给 Zot 生成 bcrypt 密码) +- DNS:`git` / `registry` / `s3` / `juwan` 四条 A 记录全部指向 66.135.5.101,灰云直连 +- 防火墙入站规则允许 TCP 80、443、UDP 443 + +## 首次部署 + +```bash +cd deploy/center + +# 生成所有随机密码与 token,渲染 garage.toml / zot.htpasswd / .env +bash init.sh + +# 启动 Caddy + Zot + Garage + Gitea +docker compose up -d caddy zot garage gitea + +# 创建 Gitea 管理员 +docker compose exec -u git gitea gitea admin user create \ + --username admin \ + --email admin@juwan.xhttp.zip \ + --password "$(cat secrets/gitea-admin-password)" \ + --admin --must-change-password=false + +# 在浏览器打开 https://git.juwan.xhttp.zip +# → Site Administration → Actions → Runners → 生成 runner token +# → 把 token 写入 .env 的 RUNNER_TOKEN +# → 回到终端执行: +docker compose up -d runner + +# 初始化 Garage:创建 layout、两个 bucket、生成 access key +bash garage/bootstrap.sh +``` + +`bootstrap.sh` 最后会打印 S3 连接信息,其中 `S3_ACCESS_KEY` / `S3_SECRET_KEY` 留给 k01 的 `objectstory-rpc` 和 CNPG backup 配置。 + +## 访问入口 + +| 子域 | 内容 | +| -------------------------- | ------------------------------------------------- | +| `git.juwan.xhttp.zip` | Gitea 代码仓库 | +| `registry.juwan.xhttp.zip` | Zot 镜像仓库 + 内置 zui 浏览器 | +| `s3.juwan.xhttp.zip` | Garage S3 API | +| `juwan.xhttp.zip` | 业务前端,Caddy 反代至 k01 envoy-gateway NodePort | + +## 凭据与认证 + +`init.sh` 会把所有密码写入 `secrets/` 目录(权限 600,`.gitignore` 已排除)。`garage/garage.toml` 和 `zot/htpasswd` 由模板渲染生成,同样不在仓库中跟踪。 + +### Zot + +匿名用户可浏览 zui、`docker pull` 镜像。推送或删除需要登录: + +```bash +docker login registry.juwan.xhttp.zip -u admin -p "$(cat secrets/zot-admin-password)" +``` + +### Gitea + +注册链接默认关闭。管理员登录后通过以下方式创建新用户: + +```bash +docker compose exec -u git gitea gitea admin user create \ + --username NAME --email MAIL --password PASS +``` + +## Runner + +通过宿主 `/var/run/docker.sock` 启动 job 容器。Workflow 里写 `runs-on: ubuntu-latest` 时,runner 会拉取 `gitea/runner-images:ubuntu-latest-slim` 作为临时工作环境。`docker build` 命令在此容器内调用宿主的 dockerd,生成的镜像可直接推送到本机 Zot。 + +## 日常维护 + +```bash +docker compose restart # 全部重启 +docker compose logs -f caddy # 查看 Caddy 日志(含 ACME 信息) +docker compose logs -f runner # 查看 Runner 日志(含 job 输出) + +# 彻底重置:删除所有 Compose 卷与 init.sh 生成的本地文件 +docker compose down -v +rm -rf secrets garage/garage.toml zot/htpasswd +``` + +持久化数据所在的 Docker 卷: + +| 卷 | 内容 | +| -------------------- | ------------------------ | +| `juwan-caddy-data` | ACME 证书 | +| `juwan-caddy-config` | Caddy 自动配置 | +| `juwan-zot-data` | 容器镜像层 | +| `juwan-garage-meta` | Garage 元数据 | +| `juwan-garage-data` | S3 对象数据 | +| `juwan-gitea-data` | Git 仓库与 SQLite 数据库 | +| `juwan-runner-data` | Runner 注册信息 | diff --git a/deploy/center/caddy/Caddyfile b/deploy/center/caddy/Caddyfile new file mode 100644 index 0000000..2cf8466 --- /dev/null +++ b/deploy/center/caddy/Caddyfile @@ -0,0 +1,63 @@ +{ + email admin@juwan.xhttp.zip +} + +(common_log) { + log { + output stdout + format console { + time_format common_log + time_local + } + } +} + +(stream_proxy) { + flush_interval -1 + transport http { + read_timeout 0 + write_timeout 0 + response_header_timeout 0 + } +} + +git.juwan.xhttp.zip { + import common_log + request_body { + max_size 2GB + } + reverse_proxy http://gitea:3000 { + import stream_proxy + } +} + +registry.juwan.xhttp.zip { + import common_log + request_body { + max_size 2GB + } + reverse_proxy http://zot:5000 { + import stream_proxy + } +} + +s3.juwan.xhttp.zip { + import common_log + request_body { + max_size 5GB + } + reverse_proxy http://garage:3900 { + import stream_proxy + } +} + +juwan.xhttp.zip { + import common_log + reverse_proxy http://140.82.15.92:30080 { + lb_policy round_robin + health_uri /healthz + health_interval 10s + fail_duration 30s + import stream_proxy + } +} diff --git a/deploy/center/docker-compose.yml b/deploy/center/docker-compose.yml new file mode 100644 index 0000000..969abf7 --- /dev/null +++ b/deploy/center/docker-compose.yml @@ -0,0 +1,117 @@ +services: + # ==================== 反代 ==================== + caddy: + image: caddy:2.11.2-alpine + container_name: juwan-caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + depends_on: + - gitea + - zot + - garage + + # ==================== 容器仓库 ==================== + zot: + image: ghcr.io/project-zot/zot:v2.1.16 + container_name: juwan-zot + restart: unless-stopped + command: ["serve", "/etc/zot/config.json"] + volumes: + - ./zot/config.json:/etc/zot/config.json:ro + - ./zot/htpasswd:/etc/zot/htpasswd:ro + - zot-data:/var/lib/registry + expose: + - "5000" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5000/v2/ >/dev/null || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + + # ==================== S3 对象存储 ==================== + garage: + image: dxflrs/garage:v2.3.0 + container_name: juwan-garage + restart: unless-stopped + command: ["/garage", "server"] + volumes: + - ./garage/garage.toml:/etc/garage.toml:ro + - garage-meta:/var/lib/garage/meta + - garage-data:/var/lib/garage/data + expose: + - "3900" + - "3901" + - "3902" + - "3903" + + # ==================== Git 服务 ==================== + gitea: + image: docker.gitea.com/gitea:1.26.1 + container_name: juwan-gitea + restart: unless-stopped + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: sqlite3 + GITEA__server__DOMAIN: ${GITEA_DOMAIN} + GITEA__server__ROOT_URL: https://${GITEA_DOMAIN}/ + GITEA__server__PROTOCOL: http + GITEA__server__HTTP_PORT: "3000" + GITEA__server__DISABLE_SSH: "true" + GITEA__service__DISABLE_REGISTRATION: "true" + GITEA__security__INSTALL_LOCK: "true" + GITEA__actions__ENABLED: "true" + volumes: + - gitea-data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + expose: + - "3000" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/healthz >/dev/null || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + + # ==================== CI/CD 执行器 ==================== + runner: + image: gitea/act_runner:0.6.1 + container_name: juwan-runner + restart: unless-stopped + environment: + GITEA_INSTANCE_URL: http://gitea:3000 + GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN} + GITEA_RUNNER_NAME: juwan-center + GITEA_RUNNER_LABELS: ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest-slim + CONFIG_FILE: /data/config.yaml + volumes: + - runner-data:/data + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + gitea: + condition: service_healthy + +volumes: + caddy-data: + name: juwan-caddy-data + caddy-config: + name: juwan-caddy-config + zot-data: + name: juwan-zot-data + garage-meta: + name: juwan-garage-meta + garage-data: + name: juwan-garage-data + gitea-data: + name: juwan-gitea-data + runner-data: + name: juwan-runner-data diff --git a/deploy/center/garage/bootstrap.sh b/deploy/center/garage/bootstrap.sh new file mode 100755 index 0000000..7564d77 --- /dev/null +++ b/deploy/center/garage/bootstrap.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +GARAGE="docker compose exec -T garage /garage" + +NODE_ID="$($GARAGE node id -q | cut -d@ -f1 | tr -d '\r')" +echo "node id: $NODE_ID" + +$GARAGE layout assign -z dc1 -c 10G "$NODE_ID" +$GARAGE layout apply --version 1 + +$GARAGE bucket create juwan-objectstory +$GARAGE bucket create juwan-pg-backup + +KEY_INFO="$($GARAGE key create juwan-app)" +echo "$KEY_INFO" + +ACCESS_KEY="$(echo "$KEY_INFO" | awk '/Key ID:/ {print $3}')" +SECRET_KEY="$(echo "$KEY_INFO" | awk '/Secret key:/ {print $3}')" + +$GARAGE bucket allow --read --write --owner juwan-objectstory --key juwan-app +$GARAGE bucket allow --read --write --owner juwan-pg-backup --key juwan-app + +cat < "secrets/$name" + chmod 600 "secrets/$name" +} + +RPC_SECRET="$(openssl rand -hex 32)" +ADMIN_TOKEN="$(openssl rand -base64 32 | tr -d '\n')" +METRICS_TOKEN="$(openssl rand -base64 32 | tr -d '\n')" +ZOT_PASSWORD="$(openssl rand -hex 16)" +GITEA_PASSWORD="$(openssl rand -hex 16)" + +write_secret garage-rpc-secret "$RPC_SECRET" +write_secret garage-admin-token "$ADMIN_TOKEN" +write_secret garage-metrics-token "$METRICS_TOKEN" +write_secret zot-admin-password "$ZOT_PASSWORD" +write_secret gitea-admin-password "$GITEA_PASSWORD" + +if [ ! -f .env ]; then + cp .env.example .env +fi + +python3 - "$RPC_SECRET" "$ADMIN_TOKEN" "$METRICS_TOKEN" <<'PY' +import sys, pathlib +rpc, admin, metrics = sys.argv[1:4] +src = pathlib.Path("garage/garage.toml.template").read_text() +out = (src + .replace("@RPC_SECRET@", rpc) + .replace("@ADMIN_TOKEN@", admin) + .replace("@METRICS_TOKEN@", metrics)) +pathlib.Path("garage/garage.toml").write_text(out) +PY + +htpasswd -bBn admin "$ZOT_PASSWORD" > zot/htpasswd +chmod 600 zot/htpasswd + +echo +echo "secrets/ 写入完成,garage/garage.toml、zot/htpasswd 已渲染" +echo +echo "Zot: admin / $ZOT_PASSWORD" +echo "Gitea: admin / $GITEA_PASSWORD" diff --git a/deploy/center/zot/config.json b/deploy/center/zot/config.json new file mode 100644 index 0000000..6c1085b --- /dev/null +++ b/deploy/center/zot/config.json @@ -0,0 +1,50 @@ +{ + "distSpecVersion": "1.1.1", + "storage": { + "rootDirectory": "/var/lib/registry", + "dedupe": true, + "gc": true, + "gcDelay": "1h", + "gcInterval": "24h" + }, + "http": { + "address": "0.0.0.0", + "port": "5000", + "realm": "zot", + "compat": ["docker2s2"], + "auth": { + "htpasswd": { + "path": "/etc/zot/htpasswd" + }, + "failDelay": 5 + }, + "accessControl": { + "repositories": { + "**": { + "anonymousPolicy": ["read"], + "defaultPolicy": ["read", "create", "update", "delete"] + } + }, + "metrics": { + "users": ["admin"] + } + } + }, + "log": { + "level": "info" + }, + "extensions": { + "search": { + "enable": true + }, + "ui": { + "enable": true + }, + "metrics": { + "enable": true, + "prometheus": { + "path": "/metrics" + } + } + } +}