diff --git a/AGENTS.md b/AGENTS.md index ff09b57..afc5dc7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ not maintain a second copy of the machine table here. | SMTP `mx2.windy.me:587` (STARTTLS) or `:465` | mx2 | client submission; full email + mailbox password — [runbook](runbooks/mailcow-smtp-client.md) | | IMAP `mx2.windy.me:993` | mx2 | same mailbox credentials | | https://auth.wsvc.info | us2 (`/opt/vaultwarden`) | Vaultwarden (Postgres, **operational**) — client Server URL | -| `repo.windy.me:2222` | us2 (`/opt/soft-serve`) | Soft Serve git — v0.12.2 pinned, backup sidecar; details in [hosts/us2.wsvc.info.md](hosts/us2.wsvc.info.md) | +| `repo.windy.me` (git SSH `:2222` / web HTTPS) | us2 (`/opt/gitea`) | Gitea — 1.27.3-rootless pinned, backup sidecar; details in [hosts/us2.wsvc.info.md](hosts/us2.wsvc.info.md) | | DNS `ns1.wsvc.info:53` | hk2 (`/opt/pdns`, Auth **5.0.6**) | PowerDNS auth — zones `windy.me`, `wsvc.info`, `chans.xyz` | | https://pdns.wsvc.info | hk2 (`poweradmin`) | Poweradmin UI | | https://pgweb.wsvc.info | hk2 (`pgweb`) | PowerDNS Postgres browser | diff --git a/compose/README.md b/compose/README.md index 3754eba..946d69d 100644 --- a/compose/README.md +++ b/compose/README.md @@ -11,7 +11,8 @@ files: every secret is a `${VAR}` reference resolved from the **server-local | `vaultwarden` | us2 (`/opt/vaultwarden`) | `compose/vaultwarden/compose.yml` | static file + `compose-deploy.yml` | | `pdns` | hk2 (`/opt/pdns`) | `compose/pdns/compose.yml` | static file + `compose-deploy.yml` | | `pgdb` | pgdb (`/opt/database`, 无 ansible) | `compose/pgdb/compose.yml` | static file(手动部署:scp → `docker compose config -q` → `up -d`;服务器文件名 `docker-compose.yml`) | -| `soft-serve` | us2 (`/opt/soft-serve`) | `compose/soft-serve/compose.yml` (+ `Dockerfile.backup`, `scripts/`) | static file(参考镜像, 未接入 compose-deploy; 服务器文件为准) | +| `soft-serve` | us2 (`/opt/soft-serve`, 已退役停用) | `compose/soft-serve/compose.yml` (+ `Dockerfile.backup`, `scripts/`) | static file(参考镜像; 2026-09-18 被 gitea 替换 VPS-94, 数据保留作回滚) | +| `gitea` | us2 (`/opt/gitea`) | `compose/gitea/compose.yml` (+ `Dockerfile.backup`, `scripts/`) | static file(参考镜像, 未接入 compose-deploy; 服务器文件为准; 2026-09-18 替换 soft-serve, VPS-94) | | `adguardhome` | dns.windy.lan (`/opt/adguardhome`) | — (待从 LAN 提取) | static file (pending) | | `unifi` | ubnt (`/home/windy/unifi-9`) | — (待从 LAN 提取) | static file (pending) | | `wireguard` | us4 (`/opt/wireguard`) | `ansible/templates/wireguard-compose.yml.j2` | role-rendered (inventory vars) | diff --git a/compose/gitea/.env.example b/compose/gitea/.env.example new file mode 100644 index 0000000..405b5af --- /dev/null +++ b/compose/gitea/.env.example @@ -0,0 +1,4 @@ +# compose/gitea — 秘密一律走服务器本地 .env, 不入库 +# 迁移期一次性: Gitea 管理员生成的 token (mirror-migrate.sh 读取, 用后撤销) +GITEA_MIGRATE_USER= +GITEA_MIGRATE_TOKEN= diff --git a/compose/gitea/Dockerfile.backup b/compose/gitea/Dockerfile.backup new file mode 100644 index 0000000..bfe3630 --- /dev/null +++ b/compose/gitea/Dockerfile.backup @@ -0,0 +1,3 @@ +FROM alpine:3.20 +RUN apk add --no-cache sqlite rsync tzdata +WORKDIR /scripts diff --git a/compose/gitea/compose.yml b/compose/gitea/compose.yml new file mode 100644 index 0000000..be408ac --- /dev/null +++ b/compose/gitea/compose.yml @@ -0,0 +1,59 @@ +# Gitea on us2 — reference compose (Plane VPS-94, 迁移完成 2026-09-18) +# 参考镜像, 服务器 /opt/gitea 文件为准 (同 soft-serve 约定, 未接入 compose-deploy) +# rootless 镜像: uid 1000 原生非 root; 数据 /var/lib/gitea (宿主 ./data), 配置 /etc/gitea (宿主 ./config) +# SSH: 容器内监听 2322 (非特权, SSH_LISTEN_PORT), 对外 repo.windy.me:2222 经 Traefik TCP entrypoint `ssh` +services: + gitea: + image: gitea/gitea@sha256:1c17ecaead42eb3b5391553d8708103a4beb0e86edf5b9ebc1eb269c318845f2 # 1.27.3-rootless + container_name: gitea + restart: unless-stopped + user: "1000:1000" + environment: + TZ: Asia/Shanghai + volumes: + - ./data:/var/lib/gitea + - ./config:/etc/gitea + - ./secrets:/secrets:ro # 复用的 soft-serve host key (SSH_SERVER_HOST_KEYS) + networks: + - traefik + labels: + - traefik.enable=true + # Web UI: repo.windy.me (2026-09-18 操作者决定复用现有域名, 免 DNS 变更) + - traefik.http.routers.gitea-web.rule=Host(`repo.windy.me`) + - traefik.http.routers.gitea-web.entrypoints=websecure + - traefik.http.routers.gitea-web.tls.certresolver=letsencrypt + - traefik.http.services.gitea-web.loadbalancer.server.port=3000 + # SSH: 接管 :2222 (entrypoint 已存在, router 动态生效, 无需重启 Traefik) + - traefik.tcp.routers.gitea-ssh.entrypoints=ssh + - traefik.tcp.routers.gitea-ssh.rule=HostSNI(`*`) + - traefik.tcp.routers.gitea-ssh.tls=false + - traefik.tcp.services.gitea-ssh.loadbalancer.server.port=2322 + + gitea-backup: + build: + context: . + dockerfile: Dockerfile.backup + container_name: gitea-backup + restart: unless-stopped + volumes: + - ./data:/data:ro + - ./config:/config:ro + - ./backups:/backup + - ./scripts:/scripts + environment: + TZ: Asia/Shanghai + BACKUP_UID: 1000 + BACKUP_GID: 1000 + entrypoint: > + /bin/sh -ec " + umask 077 && + touch /backup/backup.log && + crontab /scripts/crontab.txt && + echo '[INFO] gitea backup cron installed' && + crond -f -l 8 + " + +networks: + traefik: + external: true + name: vw-net diff --git a/compose/gitea/scripts/backup.sh b/compose/gitea/scripts/backup.sh new file mode 100755 index 0000000..400ef2c --- /dev/null +++ b/compose/gitea/scripts/backup.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu +umask 077 +D() { date "+%Y-%m-%d %H:%M:%S"; } +TS=$(date +%Y%m%d_%H%M%S) +OUT="/backup/gitea_${TS}" +mkdir -p "$OUT" +echo "[$(D)] Starting gitea backup -> $OUT" +# rootless 布局: app.ini=/etc/gitea(宿主 ./config), db+repos=/var/lib/gitea/data(宿主 ./data/data) +# app.ini 含 SECRET_KEY/INTERNAL_TOKEN — 恢复 2FA/session/mirror 凭据必需 +tar czf "$OUT/app.ini.tar.gz" -C /config app.ini +sqlite3 /data/data/gitea.db ".backup '$OUT/gitea.db'" +rsync -a /data/data/git/repositories/ "$OUT/repos/" +tar czf "$OUT/repos.tar.gz" -C "$OUT" repos +rm -rf "$OUT/repos" +chmod 600 "$OUT"/* +if [ -n "${BACKUP_UID:-}" ] && [ -n "${BACKUP_GID:-}" ]; then + chown -R "$BACKUP_UID:$BACKUP_GID" "$OUT" /backup/backup.log +fi +echo "[$(D)] Backup OK: $(du -sh "$OUT" | cut -f1)" diff --git a/compose/gitea/scripts/crontab.txt b/compose/gitea/scripts/crontab.txt new file mode 100644 index 0000000..4f3b013 --- /dev/null +++ b/compose/gitea/scripts/crontab.txt @@ -0,0 +1,4 @@ +# Run gitea backup daily at 02:00 +0 2 * * * /bin/sh /scripts/backup.sh >> /backup/backup.log 2>&1 +# Prune backups older than 14 days daily at 03:00 +0 3 * * * /bin/sh /scripts/prune.sh >> /backup/backup.log 2>&1 diff --git a/compose/gitea/scripts/mirror-migrate.sh b/compose/gitea/scripts/mirror-migrate.sh new file mode 100755 index 0000000..9e95c6f --- /dev/null +++ b/compose/gitea/scripts/mirror-migrate.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# 一次性迁移辅助 (Plane VPS-94 Phase 2): 在 gitea 容器内执行。 +# 已于 2026-09-18 执行完成 (16 仓), 留档备查; 复用时按 VPS-94 流程重生成一次性 token。 +# 用法: +# GITEA_MIGRATE_USER= GITEA_MIGRATE_TOKEN= \ +# docker exec -e GITEA_MIGRATE_USER -e GITEA_MIGRATE_TOKEN gitea \ +# /scripts/mirror-migrate.sh [public_repo ...] +# 每仓: API 建仓 (默认 private, 参数中列出的为 public) -> push --mirror。 +# default_branch 按源仓 symbolic-ref HEAD 设置, 避免非 main 源仓在 Gitea 显示为空。 +# 结束后按 VPS-94 Phase 3 逐仓核对 git ls-remote ref 全集。 +set -eu +MUSER="${GITEA_MIGRATE_USER:?need GITEA_MIGRATE_USER}" +TOKEN="${GITEA_MIGRATE_TOKEN:?need GITEA_MIGRATE_TOKEN}" +SRC="/migration-src" +API="http://localhost:3000/api/v1" +PUBLIC_REPOS=" $* " + +migrate_one() { + dir="$1" + git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || { echo "[SKIP] $dir (not a git repo)"; return 0; } + name=$(basename "$dir"); name=${name%.git} + def_branch=$(git -C "$dir" symbolic-ref --short HEAD) + case "$PUBLIC_REPOS" in *" $name "*) private=false ;; *) private=true ;; esac + echo "[MIGRATE] $name (default=$def_branch private=$private)" + code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$API/user/repos" \ + -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ + -d "{\"name\":\"$name\",\"private\":$private,\"default_branch\":\"$def_branch\",\"auto_init\":false}") + case "$code" in + 201) : ;; + 409) echo " [WARN] $name 已存在, 直接补推" ;; + *) echo " [FAIL] create HTTP $code"; return 1 ;; + esac + git -C "$dir" push --mirror "http://$MUSER:$TOKEN@localhost:3000/$MUSER/$name.git" + echo " [OK] $name pushed" +} + +for dir in "$SRC"/*.git "$SRC"/cdia; do + [ -d "$dir" ] || continue + migrate_one "$dir" +done +echo "[DONE] 全部处理完毕; 迁移后记得撤销一次性 token" diff --git a/compose/gitea/scripts/prune.sh b/compose/gitea/scripts/prune.sh new file mode 100755 index 0000000..872eeed --- /dev/null +++ b/compose/gitea/scripts/prune.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu +D() { date "+%Y-%m-%d %H:%M:%S"; } +ls -dt /backup/gitea_* 2>/dev/null | tail -n +15 | xargs -r rm -rf +echo "[$(D)] Pruned. Kept $(ls -d /backup/gitea_* 2>/dev/null | wc -l) backups (max 14)" diff --git a/hosts/us2.wsvc.info.md b/hosts/us2.wsvc.info.md index d1db96f..2647a57 100644 --- a/hosts/us2.wsvc.info.md +++ b/hosts/us2.wsvc.info.md @@ -5,7 +5,7 @@ | Role | Multi-service VPS (Vaultwarden, Traefik, Soft Serve, …) | | SSH | `ssh -4 windy@us2.wsvc.info` (prefer IPv4 from WSL) | | IPv4 | `193.9.44.165` | -| Also DNS | `auth.wsvc.info` → this host; `repo.windy.me` → this host (Soft Serve) | +| Also DNS | `auth.wsvc.info` → this host; `repo.windy.me` → this host (Gitea) | | Public HTTPS | Traefik on `:80` / `:443` (`/opt/traefik`) | ## Vaultwarden (Bitwarden-compatible) @@ -80,22 +80,25 @@ ansible-playbook playbooks/compose-reconcile.yml --limit vaultwarden \ | Container | Status | Image / notes | |-----------|--------|---------------| -| `soft-serve` | Up | `charmcli/soft-serve:v0.12.2` (`repo.windy.me:2222`) | -| `soft-serve-backup` | Up | alpine + sqlite3 sidecar (daily backup 02:00 / prune 03:00, crond) | +| `gitea` | Up | `gitea/gitea@sha256:1c17ecaead42e…` (1.27.3-rootless) — SSH `repo.windy.me:2222`, web `https://repo.windy.me` | +| `gitea-backup` | Up | alpine + sqlite3/rsync sidecar (daily backup 02:00 / prune 03:00, crond) | -Soft Serve details (verified/updated 2026-08-30; 核查 [W1N-244](https://linear.app/w1ndy/issue/W1N-244), 修复 [W1N-245](https://linear.app/w1ndy/issue/W1N-245)/[W1N-246](https://linear.app/w1ndy/issue/W1N-246)/[W1N-247](https://linear.app/w1ndy/issue/W1N-247)): -- `/opt/soft-serve/compose.yml` (+ `Dockerfile.backup`, `scripts/`, `backups/`); data `/opt/soft-serve/data` → `/var/lib/soft-serve` (sqlite `soft-serve.db`, 12 repos + `windyboy`); env `.env` (`SOFT_SERVE_INITIAL_ADMIN_KEYS` = admin pubkey, first-boot only). 仓库镜像: `compose/soft-serve/`(参考, 服务器文件为准) -- No host ports published: Traefik TCP entrypoint `ssh` (`:2222` → `soft-serve:23231`, `HostSNI(*)`, `tls=false`) on `vw-net`. Container listens 23231 SSH / 23232 HTTP (git smart-http, no web UI) / 9418 git / 23233 stats (localhost only) -- **镜像已固定** `charmcli/soft-serve:v0.12.2`(digest `sha256:554cc770…`, Docker Hub 稳定源; GHCR 为 dev/nightly 源且无 v0.12.x tag); `/soft-serve` 由 named volume `soft-serve_soft-serve-app` 承载, 旧匿名卷已清理(2026-08-30) -- **非 root 运行**(2026-08-30, [W1N-248](https://linear.app/w1ndy/issue/W1N-248)): compose `user: "1000:1000"`(uid=windy, 镜像无内置用户); data 全量 `chown 1000:1000`, 容器内 `id` = uid 1000, 功能验证通过 -- **`ssh.public_url` 已修复**(2026-08-30): `config.yaml` → `ssh://git@repo.windy.me:2222`; 失效 env `SOFT_SERVE_SSH_PUBLIC_URL` 已删(v0.12 不读取); `http`/`git` public_url 保持 `localhost`(未对外暴露) -- **备份**: sidecar `soft-serve-backup` 每日 02:00 → `backups/soft-serve_/{repos-config.tar.gz, soft-serve.db}`(db 用 `sqlite3 .backup` 在线快照), 03:00 prune 保留 14 份; 产物 chown windy:windy 600(含 `ssh/` host keys)。**恢复**: `docker compose stop soft-serve` → 解包 `repos-config.tar.gz` + 放回 `soft-serve.db` 到 `data/` → `docker compose up -d`。异地副本(hk2/WSL 每日拉取)= follow-up(见 W1N-247) +### Gitea (replaced Soft Serve 2026-09-18; [Plane VPS-94](https://plane.chans.xyz)) + +- `/opt/gitea/compose.yml` (+ `Dockerfile.backup`, `scripts/`, `config/app.ini`, `data/`, `secrets/`, `backups/`); 镜像: `compose/gitea/`(参考, 服务器文件为准) +- **rootless 镜像** uid 1000:1000; SQLite `/opt/gitea/data/data/gitea.db`; repos `/opt/gitea/data/data/git/repositories/`; app.ini `/opt/gitea/config/app.ini`(600, 含 SECRET_KEY) +- SSH: 内置 server 容器内 `:2322`(`SSH_LISTEN_PORT` 非特权), Traefik TCP entrypoint `ssh`(`:2222` → `gitea:2322`, `HostSNI(*)`, `tls=false`) on `vw-net`; clone URL `ssh://git@repo.windy.me:2222/windy/.git`(owner 段 `windy`) +- **host key 复用 soft-serve**(`SSH_SERVER_HOST_KEYS=/secrets/soft_serve_host_ed25519`, ed25519, 指纹 `SHA256:PdxZRe74…`): 客户端 known_hosts 零变更; 仅公钥认证(密码认证未启用) +- Web: `https://repo.windy.me`(Traefik websecure + letsencrypt); `DISABLE_REGISTRATION=true`, Actions 关闭; 管理员 `windy`(凭据仅存服务器 `/opt/gitea/.admin-credentials`, 勿入库/入 Plane) +- 仓库: 16 个(顶层 11 + `cdia/` 4 + `windyboy/go-caatsm`), 2026-09-18 自 soft-serve `push --mirror` 迁移, 逐仓 `ls-remote` ref 全集 + HEAD symref 两端一致; 可见性仅 `dotfiles-personal` private, 其余 public(与 soft-serve 现状一致) +- 备份: sidecar 每日 02:00 → `backups/gitea_/{app.ini.tar.gz, gitea.db, repos.tar.gz}`(app.ini 含恢复必需 SECRET_KEY), 03:00 prune 保留 14 份; 已验证手动备份产物 109.9M +- 回滚: `/opt/soft-serve` 未删(compose stop + sidecar 停, 数据与旧备份冻结保留), 回滚 = Traefik `:2222` 指回 `soft-serve:23231` + 客户端 remote 回改旧无 owner 段路径; 观察 2–4 周后清理(历史: W1N-244~248) | `traefik` | Up | `traefik:v3.6.2` (`/opt/traefik`, public `:80`/`:443`) | | `nghttpx-proxy` + `squid-backend` | Up | HTTP forward-proxy stack (`/opt/nghttpx`), network `nghttpx_internal-net`; details TBD | **Disk cleanup 2026-09-18** ([Plane vps VPS-93](https://plane.chans.xyz)): root 71% → **23%** (~33G freed) keeping soft-serve / vaultwarden / traefik (nghttpx kept running per operator choice). Removed: unused Docker images + orphan volumes (incl. `zitadel_data` 801M), dormant `/opt` dirs (dendrite + its disabled `dendrite.service` unit, mastodon, dailysync, keycloak, media-repo, authelia, conduit, npm, manager, fusion, zitadel, rustdesk), rootless podman storage (6.4G stale goauthentik), home dev caches, apt cache, journal 3.8G→162M (+`SystemMaxUse=200M` drop-in, active next boot), truncated container logs (nghttpx 550M / traefik / squid). Follow-up: nghttpx-proxy logs grow ~25M/day (INFO per-connection); root-cause log-level/rotation fix still open (needs container restart approval). -Remaining running services on this host: `soft-serve`, `vaultwarden` stack, `traefik`, `nghttpx-proxy` + `squid-backend` (undocumented forward proxy, `/opt/nghttpx`). `/home/windy/authelia` (76M) left in place — outside approved cleanup scope. +Remaining running services on this host: `gitea`, `vaultwarden` stack, `traefik`, `nghttpx-proxy` + `squid-backend` (undocumented forward proxy, `/opt/nghttpx`). `/opt/soft-serve` kept stopped as rollback (2–4 weeks, data intact). `/home/windy/authelia` (76M) left in place — outside approved cleanup scope. ## Verified diff --git a/inventory/hosts.md b/inventory/hosts.md index c5628cd..7268f22 100644 --- a/inventory/hosts.md +++ b/inventory/hosts.md @@ -17,7 +17,7 @@ the software deployed there, see [the LAN overview](../docs/lan-overview.md). | mx2.windy.me | mailcow (primary MX prio 20) | `ssh -4 windy@mx2.windy.me` | 194.163.160.244 | ✓ (mx2) | active | [hosts/mx2.windy.me.md](../hosts/mx2.windy.me.md) | | us2.wsvc.info | Vaultwarden/Postgres (+ Traefik, Soft Serve, …) | `ssh -4 windy@us2.wsvc.info` | 193.9.44.165 | ✓ (us2) | active | [hosts/us2.wsvc.info.md](../hosts/us2.wsvc.info.md) | | mx.windy.me | mail (secondary MX prio 30) | TBD | see AAAA/A | — (stub) | stub | — | -| repo.windy.me | Soft Serve git (on us2) | `ssh -p 2222 windy@repo.windy.me` | 193.9.44.165 | — (service on us2) | stub | see us2 | +| repo.windy.me | Gitea git (on us2) | `ssh -p 2222 windy@repo.windy.me` | 193.9.44.165 | — (service on us2) | stub | see us2 | | auth.wsvc.info | Vaultwarden public hostname | — (HTTPS) | → us2 | — (alias) | active | see us2 | | us1.wsvc.info | PowerDNS secondary (ns2 host) | TBD | 202.91.35.141 | — (stub) | stub | Auth 5.0.5; see hk2 | | us4.wsvc.info | WireGuard VPN | `ssh -4 windy@us4.wsvc.info` | 185.201.226.122 | ✓ (us4) | active | [hosts/us4.wsvc.info.md](../hosts/us4.wsvc.info.md) |