feat(deploy): add center host docker compose stack for git, registry and s3 hosting
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
GITEA_DOMAIN=git.juwan.xhttp.zip
|
||||||
|
RUNNER_TOKEN=
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/secrets/
|
||||||
|
/garage/garage.toml
|
||||||
|
/zot/htpasswd
|
||||||
|
/.env
|
||||||
@@ -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 注册信息 |
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Executable
+31
@@ -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 <<EOF
|
||||||
|
|
||||||
|
S3_ENDPOINT=https://s3.juwan.xhttp.zip
|
||||||
|
S3_REGION=garage
|
||||||
|
S3_ACCESS_KEY=$ACCESS_KEY
|
||||||
|
S3_SECRET_KEY=$SECRET_KEY
|
||||||
|
S3_BUCKET_NAME=juwan-objectstory
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
metadata_dir = "/var/lib/garage/meta"
|
||||||
|
data_dir = "/var/lib/garage/data"
|
||||||
|
db_engine = "sqlite"
|
||||||
|
|
||||||
|
replication_factor = 1
|
||||||
|
consistency_mode = "consistent"
|
||||||
|
|
||||||
|
rpc_bind_addr = "[::]:3901"
|
||||||
|
rpc_public_addr = "garage:3901"
|
||||||
|
rpc_secret = "@RPC_SECRET@"
|
||||||
|
|
||||||
|
[s3_api]
|
||||||
|
api_bind_addr = "[::]:3900"
|
||||||
|
s3_region = "garage"
|
||||||
|
root_domain = ".s3.juwan.xhttp.zip"
|
||||||
|
|
||||||
|
[s3_web]
|
||||||
|
bind_addr = "[::]:3902"
|
||||||
|
root_domain = ".web.juwan.xhttp.zip"
|
||||||
|
index = "index.html"
|
||||||
|
|
||||||
|
[admin]
|
||||||
|
api_bind_addr = "[::]:3903"
|
||||||
|
admin_token = "@ADMIN_TOKEN@"
|
||||||
|
metrics_token = "@METRICS_TOKEN@"
|
||||||
|
metrics_require_token = true
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CENTER_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$CENTER_DIR"
|
||||||
|
|
||||||
|
mkdir -p secrets
|
||||||
|
chmod 700 secrets
|
||||||
|
|
||||||
|
write_secret() {
|
||||||
|
local name="$1" value="$2"
|
||||||
|
printf '%s\n' "$value" > "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"
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user