From e5f841c62455d6e90d964171a34d5f2fcc191057 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 8 Apr 2026 09:58:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0portainer=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 45 ++- base/setup.sh | 63 ++- certd/README.md | 60 +++ certd/backup.sh | 1 + certd/deploy.sh | 1 + certd/nginx/certd.conf | 14 +- certd/uninstall.sh | 155 ++++++++ gitea/README.md | 156 ++++---- gitea/backup.sh | 1 + gitea/deploy.sh | 37 +- gitea/migrate.sh | 513 ------------------------ gitea/nginx/gitea.conf | 5 +- gitea/uninstall.sh | 186 +++++++++ gitea/upgrade.sh | 617 ----------------------------- portainer/.env.example | 21 + portainer/README.md | 328 +++++++++++++++ portainer/backup.sh | 69 ++++ portainer/deploy.sh | 177 +++++++++ portainer/docker-compose.yml | 24 ++ portainer/nginx/portainer.conf | 55 +++ portainer/uninstall.sh | 154 +++++++ siyuan/README.md | 60 +++ siyuan/backup.sh | 1 + siyuan/deploy.sh | 1 + siyuan/nginx/siyuan.conf | 14 +- siyuan/uninstall.sh | 154 +++++++ vaultwarden/README.md | 60 +++ vaultwarden/backup.sh | 1 + vaultwarden/deploy.sh | 1 + vaultwarden/nginx/vaultwarden.conf | 15 +- vaultwarden/uninstall.sh | 154 +++++++ 31 files changed, 1882 insertions(+), 1261 deletions(-) create mode 100755 certd/uninstall.sh delete mode 100755 gitea/migrate.sh create mode 100755 gitea/uninstall.sh delete mode 100755 gitea/upgrade.sh create mode 100644 portainer/.env.example create mode 100644 portainer/README.md create mode 100755 portainer/backup.sh create mode 100755 portainer/deploy.sh create mode 100644 portainer/docker-compose.yml create mode 100644 portainer/nginx/portainer.conf create mode 100755 portainer/uninstall.sh create mode 100755 siyuan/uninstall.sh create mode 100755 vaultwarden/uninstall.sh diff --git a/README.md b/README.md index 78613e5..2a1cb99 100644 --- a/README.md +++ b/README.md @@ -11,23 +11,24 @@ | [certd/](certd/) | SSL 证书自动化管理(Certd) | 443 | | [vaultwarden/](vaultwarden/) | 密码管理器(Vaultwarden / Bitwarden 兼容) | 443 | | [siyuan/](siyuan/) | 知识管理笔记(思源笔记 SiYuan) | 443 | +| [portainer/](portainer/) | Docker 可视化管理(Portainer CE) | 443 | ## 架构概览 ``` - ┌─────────────┐ - │ Nginx │ ← 统一 HTTPS 入口 - │ (系统包) │ ← Let's Encrypt 证书 - └──────┬──────┘ - │ - ┌──────────────┼──────────────┬──────────────┐ - │ │ │ │ - ┌──────▼──────┐ ┌────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ - │ Gitea │ │ Certd │ │ Vaultwarden │ │ SiYuan │ - │ :3000(内部) │ │ :7001 │ │ :8080(内部) │ │ :6806 │ - │ + MySQL 8.4 │ │ │ │ │ │ │ - └─────────────┘ └──────────┘ └─────────────┘ └──────────┘ - Docker Docker Docker Docker + ┌─────────────┐ + │ Nginx │ ← 统一 HTTPS 入口 + │ (系统包) │ ← Let's Encrypt 证书 + └──────┬──────┘ + │ + ┌──────────────┬───────┼───────┬──────────────┐ + │ │ │ │ │ + ┌──────▼──────┐ ┌────▼────┐ ┌▼─────┐ ┌▼─────────┐ ┌─▼────────┐ + │ Gitea │ │ Certd │ │SiYuan│ │Vaultwarden│ │Portainer │ + │ :3000(内部) │ │ :7001 │ │:6806 │ │ :8080 │ │ :9000 │ + │ + MySQL 8.4 │ │ │ │ │ │ │ │ │ + └─────────────┘ └─────────┘ └──────┘ └───────────┘ └──────────┘ + Docker Docker Docker Docker Docker ``` 所有服务通过 Nginx 反向代理提供 HTTPS 访问,容器端口仅监听 `127.0.0.1`。 @@ -61,11 +62,17 @@ docker/ │ ├── backup.sh │ ├── nginx/vaultwarden.conf │ └── README.md -└── siyuan/ # SiYuan 思源笔记 +├── siyuan/ # SiYuan 思源笔记 +│ ├── docker-compose.yml +│ ├── deploy.sh # 部署脚本(依赖 base/) +│ ├── backup.sh +│ ├── nginx/siyuan.conf +│ └── README.md +└── portainer/ # Portainer CE Docker 管理 ├── docker-compose.yml ├── deploy.sh # 部署脚本(依赖 base/) ├── backup.sh - ├── nginx/siyuan.conf + ├── nginx/portainer.conf └── README.md ``` @@ -75,7 +82,7 @@ docker/ ```bash # 1. 上传所有文件到服务器 -scp -r base/ gitea/ certd/ vaultwarden/ siyuan/ root@:/opt/ +scp -r base/ gitea/ certd/ vaultwarden/ siyuan/ portainer/ root@:/opt/ # 2. 安装基础环境 ssh root@ @@ -106,6 +113,12 @@ cd /opt/siyuan bash deploy.sh vi .env bash deploy.sh + +# 7. 部署 Portainer(Docker 可视化管理) +cd /opt/portainer +bash deploy.sh +vi .env +bash deploy.sh ``` ### 场景二:已有 Gitea 的服务器,追加部署 Certd diff --git a/base/setup.sh b/base/setup.sh index ca77018..0f601c5 100755 --- a/base/setup.sh +++ b/base/setup.sh @@ -24,6 +24,15 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } step() { echo -e "\n${CYAN}========== $* ==========${NC}"; } +# ===== 修复 Windows CRLF 行尾 ===== +# 从 Windows scp 上传的文件可能包含 \r,source 时会导致错误 +# 用法: fix_crlf file1 [file2 ...] +fix_crlf() { + for f in "$@"; do + [ -f "$f" ] && sed -i 's/\r$//' "$f" + done +} + # ===== 检查 root ===== check_root() { if [ "$(id -u)" -ne 0 ]; then @@ -164,7 +173,7 @@ EOF fi } -# ===== 安装 Nginx ===== +# ===== 安装 Nginx(官方稳定版仓库)===== install_nginx() { step "安装 Nginx" detect_pkg_mgr @@ -172,10 +181,46 @@ install_nginx() { if command -v nginx &> /dev/null; then log "Nginx 已安装: $(nginx -v 2>&1)" else - log "正在安装 Nginx..." + log "正在安装 Nginx(官方 stable 仓库)..." case "$PKG_MGR" in - apt) apt-get install -y -qq nginx ;; - dnf|yum) $PKG_MGR install -y -q nginx ;; + apt) + # 添加 Nginx 官方 GPG 密钥 + curl -fsSL https://nginx.org/keys/nginx_signing.key \ + | gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg + # 添加官方 stable 仓库 + echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ + http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \ + > /etc/apt/sources.list.d/nginx.list + # 优先使用官方仓库 + printf "Package: *\nPin: origin nginx.org\nPin-Priority: 900\n" \ + > /etc/apt/preferences.d/99nginx + apt-get update -qq + apt-get install -y -qq nginx + ;; + dnf) + cat > /etc/yum.repos.d/nginx.repo <<'REPO' +[nginx-stable] +name=nginx stable repo +baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ +gpgcheck=1 +enabled=1 +gpgkey=https://nginx.org/keys/nginx_signing.key +module_hotfixes=true +REPO + dnf install -y -q nginx + ;; + yum) + cat > /etc/yum.repos.d/nginx.repo <<'REPO' +[nginx-stable] +name=nginx stable repo +baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ +gpgcheck=1 +enabled=1 +gpgkey=https://nginx.org/keys/nginx_signing.key +module_hotfixes=true +REPO + yum install -y -q nginx + ;; esac log "Nginx 安装完成" fi @@ -184,8 +229,13 @@ install_nginx() { mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /var/www/certbot + # Nginx 官方包使用 conf.d/ 而非 sites-enabled/,添加 include if ! grep -q "sites-enabled" /etc/nginx/nginx.conf; then - sed -i '/^http {/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf + if grep -q "^http {" /etc/nginx/nginx.conf; then + sed -i '/^http {/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf + else + sed -i '/include.*conf\.d/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf + fi fi log "Nginx 配置就绪" @@ -278,7 +328,9 @@ server { NGINX_TEMP ln -sf "/etc/nginx/sites-available/${site_name}" "/etc/nginx/sites-enabled/${site_name}" + # 移除默认站点避免冲突(Ubuntu 包和官方包路径不同) rm -f /etc/nginx/sites-enabled/default + rm -f /etc/nginx/conf.d/default.conf nginx -t && systemctl reload nginx # 申请证书 @@ -336,6 +388,7 @@ deploy_nginx_conf() { load_base_env() { local base_dir="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" if [ -f "$base_dir/.env" ]; then + fix_crlf "$base_dir/.env" set -a source "$base_dir/.env" set +a diff --git a/certd/README.md b/certd/README.md index daac533..42d8256 100644 --- a/certd/README.md +++ b/certd/README.md @@ -33,6 +33,7 @@ certd/ ├── .env.example # 配置模板 ├── deploy.sh # 一键部署脚本 ├── backup.sh # 备份脚本 +├── uninstall.sh # 完全卸载脚本 ├── nginx/ │ └── certd.conf # Nginx 反向代理配置 └── README.md # 本文件 @@ -198,6 +199,65 @@ docker compose up -d # 启动 docker compose restart # 重启 ``` +## 完全卸载 + +如果需要从服务器上完全移除 Certd,使用卸载脚本: + +```bash +cd /opt/certd +bash uninstall.sh +``` + +脚本会**交互式确认**每个危险操作,按顺序执行: + +| 步骤 | 操作 | 确认方式 | +|------|------|----------| +| 0 | 卸载前备份(可选) | y/N | +| 1 | 停止并删除 Certd 容器 | 输入 YES | +| 2 | 删除 Docker 镜像 | 自动 | +| 3 | 删除 Nginx 站点配置并重载 | 自动 | +| 4 | 删除 Let's Encrypt SSL 证书 | 自动 | +| 5 | 清理 Certbot 定时任务(仅当无其他证书时) | 自动 | +| 6 | 删除数据目录 | 输入 DELETE | +| 7 | 删除部署目录 `/opt/certd` | y/N | + +**备份目录 `/var/backups/certd/` 始终保留**,不会被删除。 + +> 卸载后如需恢复,参考上方「恢复备份」章节。 + +
+手动卸载步骤(不使用脚本) + +```bash +cd /opt/certd + +# 1. 建议先备份 +bash backup.sh + +# 2. 停止并删除容器 +docker compose down -v + +# 3. 删除 Docker 镜像(可选) +docker image rm registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest + +# 4. 删除 Nginx 配置 +rm -f /etc/nginx/sites-enabled/certd /etc/nginx/sites-available/certd +nginx -t && systemctl reload nginx + +# 5. 删除 SSL 证书 +certbot delete --cert-name 你的域名 + +# 6. 删除数据目录(⚠ 不可恢复) +rm -rf /data/certd + +# 7. 删除部署目录(可选) +rm -rf /opt/certd + +# 备份目录保留在 /var/backups/certd/ +``` + +
+ ## 使用 Certd 管理证书 ### 添加 DNS 授权 diff --git a/certd/backup.sh b/certd/backup.sh index c04b3c6..75d33c5 100755 --- a/certd/backup.sh +++ b/certd/backup.sh @@ -11,6 +11,7 @@ cd "$SCRIPT_DIR" # 加载配置 if [ -f .env ]; then + sed -i 's/\r$//' .env set -a; source .env; set +a else echo "[ERROR] .env 文件不存在,请先运行 deploy.sh" >&2 diff --git a/certd/deploy.sh b/certd/deploy.sh index ff6acf0..49fe4f1 100755 --- a/certd/deploy.sh +++ b/certd/deploy.sh @@ -47,6 +47,7 @@ init_env() { exit 0 fi + fix_crlf .env set -a; source .env; set +a local has_error=0 diff --git a/certd/nginx/certd.conf b/certd/nginx/certd.conf index bf4a2d6..47cadac 100644 --- a/certd/nginx/certd.conf +++ b/certd/nginx/certd.conf @@ -13,8 +13,9 @@ server { } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl; + listen [::]:443 ssl; + http2 on; server_name __DOMAIN__; # SSL 证书 @@ -23,16 +24,15 @@ server { # SSL 参数 ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header Referrer-Policy strict-origin-when-cross-origin always; # 反向代理到 Certd location / { diff --git a/certd/uninstall.sh b/certd/uninstall.sh new file mode 100755 index 0000000..83ae5e2 --- /dev/null +++ b/certd/uninstall.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================ +# Certd 卸载脚本 +# 停止容器 → 备份数据 → 清理容器/镜像/配置/数据 +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# ===== 检查 root ===== +if [ "$(id -u)" -ne 0 ]; then + error "请使用 root 用户运行: sudo bash uninstall.sh" + exit 1 +fi + +# ===== 加载配置 ===== +if [ -f .env ]; then + sed -i 's/\r$//' .env + set -a + source .env + set +a +fi + +CERTD_DATA_DIR="${CERTD_DATA_DIR:-/data/certd}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/certd}" +CERTD_DOMAIN="${CERTD_DOMAIN:-}" + +# ===== 确认操作 ===== +echo "" +echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}" +echo -e "${RED}║ ⚠ 即将卸载 Certd 及所有数据 ⚠ ║${NC}" +echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}" +echo "" +echo "将执行以下操作:" +echo " 1. 停止并删除 Certd 容器" +echo " 2. 删除 Docker 镜像" +echo " 3. 删除 Nginx 站点配置" +echo " 4. 删除 SSL 证书" +echo " 5. 删除 Certbot 自动续期定时任务" +echo "" +echo "涉及的数据目录:" +echo " Certd 数据: ${CERTD_DATA_DIR}" +echo " 备份目录: ${BACKUP_DIR}" +echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo -e "${YELLOW}备份目录将保留,不会被删除。${NC}" +echo "" +read -r -p "确定要继续卸载吗?输入 YES 确认: " confirm +if [ "$confirm" != "YES" ]; then + log "已取消卸载" + exit 0 +fi + +# ===== 卸载前备份 ===== +echo "" +read -r -p "是否在卸载前执行一次备份?(y/N): " do_backup +if [[ "$do_backup" =~ ^[Yy]$ ]]; then + if [ -f backup.sh ]; then + log "正在执行备份..." + bash backup.sh + log "备份完成" + else + warn "backup.sh 不存在,跳过备份" + fi +fi + +# ===== 1. 停止并删除容器 ===== +echo "" +log "正在停止并删除容器..." +if docker compose ps --quiet 2>/dev/null | grep -q .; then + docker compose down -v + log "容器已停止并删除" +else + log "没有运行中的容器" +fi + +# ===== 2. 删除 Docker 镜像 ===== +log "正在删除 Docker 镜像..." +CERTD_IMAGE="${CERTD_IMAGE:-registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest}" +docker image rm "$CERTD_IMAGE" 2>/dev/null && log "已删除镜像: $CERTD_IMAGE" || true + +# ===== 3. 删除 Nginx 配置 ===== +log "正在清理 Nginx 配置..." +rm -f /etc/nginx/sites-enabled/certd +rm -f /etc/nginx/sites-available/certd +if command -v nginx &>/dev/null && nginx -t 2>/dev/null; then + systemctl reload nginx 2>/dev/null || true + log "Nginx 已重载" +fi + +# ===== 4. 删除 SSL 证书 ===== +if [ -n "$CERTD_DOMAIN" ] && [ -d "/etc/letsencrypt/live/${CERTD_DOMAIN}" ]; then + log "正在删除 SSL 证书: ${CERTD_DOMAIN}..." + certbot delete --cert-name "${CERTD_DOMAIN}" --non-interactive 2>/dev/null || true + log "SSL 证书已删除" +fi + +# ===== 5. 清理 Certbot 定时任务 ===== +# certbot renew 是共享的,只有当没有其他证书时才移除 +remaining_certs=$(certbot certificates 2>/dev/null | grep -c "Certificate Name" || true) +if [ "$remaining_certs" -eq 0 ]; then + crontab -l 2>/dev/null | grep -v "certbot renew" | crontab - 2>/dev/null || true + log "已移除 Certbot 自动续期定时任务(无剩余证书)" +else + log "保留 Certbot 定时任务(还有 ${remaining_certs} 个其他证书)" +fi + +# ===== 6. 删除数据目录 ===== +echo "" +echo -e "${RED}以下目录将被永久删除:${NC}" +echo " ${CERTD_DATA_DIR}" +echo "" +echo -e "${YELLOW}备份目录 ${BACKUP_DIR} 不会被删除。${NC}" +echo "" +read -r -p "确认删除数据目录?输入 DELETE 确认: " confirm_delete +if [ "$confirm_delete" = "DELETE" ]; then + rm -rf "$CERTD_DATA_DIR" + log "已删除: ${CERTD_DATA_DIR}" +else + warn "跳过数据目录删除" +fi + +# ===== 7. 删除部署目录 ===== +echo "" +read -r -p "是否删除部署目录 ${SCRIPT_DIR}?(y/N): " del_deploy +if [[ "$del_deploy" =~ ^[Yy]$ ]]; then + cd /opt + rm -rf "$SCRIPT_DIR" + log "已删除部署目录" +else + warn "保留部署目录: ${SCRIPT_DIR}" +fi + +# ===== 完成 ===== +echo "" +log "Certd 卸载完成" +echo "" +echo "保留的内容:" +echo " 备份目录: ${BACKUP_DIR}" +[ "$confirm_delete" != "DELETE" ] && echo " 数据目录: ${CERTD_DATA_DIR}" +[[ ! "$del_deploy" =~ ^[Yy]$ ]] && echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo "如需恢复,请参考 README.md 中的「恢复备份」章节。" diff --git a/gitea/README.md b/gitea/README.md index 1776134..9ced08c 100644 --- a/gitea/README.md +++ b/gitea/README.md @@ -322,28 +322,7 @@ docker compose down # 停止并移除容器 docker compose up -d # 启动 ``` -### 升级组件(一键脚本) - -```bash -cd /opt/gitea - -# 交互式选择要升级的组件 -bash upgrade.sh - -# 或直接指定组件 -bash upgrade.sh gitea # 仅升级 Gitea -bash upgrade.sh mysql # 仅升级 MySQL -bash upgrade.sh nginx # 仅升级 Nginx -bash upgrade.sh certbot # 仅升级 Certbot -bash upgrade.sh docker # 仅升级 Docker -bash upgrade.sh all # 升级全部 -``` - -> 脚本会自动显示当前版本、执行备份、拉取新镜像、重启服务并验证。 - ---- - -### 手动升级详细步骤 +### 升级组件 #### 升级 Gitea @@ -570,7 +549,7 @@ gunzip < /var/backups/gitea/db_20260406_030000.sql.gz | \ docker compose up -d ``` -### 迁移到新服务器(一键脚本) +### 迁移到新服务器 将已部署的 Gitea 完整迁移到另一台服务器,包含数据库、仓库、LFS、配置等全部数据。 @@ -580,49 +559,6 @@ docker compose up -d # ===== 旧服务器 ===== cd /opt/gitea -# 导出迁移包(会自动停服 → mysqldump → 打包数据 → 打包配置) -bash migrate.sh export -# 生成: /var/backups/gitea/gitea_migrate_日期.tar.gz - -# 传输到新服务器 -scp /var/backups/gitea/gitea_migrate_*.tar.gz root@新服务器IP:/opt/gitea/ -``` - -```bash -# ===== 新服务器 ===== -# 前提:已安装 Docker(可先运行 deploy.sh 的 Docker 安装步骤,或手动安装) -mkdir -p /opt/gitea -cd /opt/gitea - -# 导入迁移包(会自动恢复配置 → 恢复数据 → 导入数据库 → 启动 → regenerate hooks → doctor check) -bash migrate.sh import gitea_migrate_日期.tar.gz - -# 验证迁移完整性 -bash migrate.sh verify -``` - -**脚本自动完成的操作:** - -| 阶段 | 操作 | -|------|------| -| 导出 | 停止 Gitea → mysqldump 导出数据库 → 打包 Gitea 数据目录 → 打包部署配置 → 生成迁移包 | -| 导入 | 解压 → 恢复配置 → 恢复数据目录 → 启动 MySQL → 导入数据库 → 启动 Gitea → regenerate hooks → doctor check | -| 验证 | 检查容器状态 → API 可达性 → 数据库连接 → 仓库/用户数量 → 数据目录完整性 → Nginx 状态 | - -**迁移后注意事项:** -- 如域名或 IP 变更,导入前需修改 `.env` 中的 `GITEA_DOMAIN` -- 域名变更后需更新 DNS 解析并重新申请 SSL 证书:`certbot certonly --webroot -w /var/www/certbot -d 新域名` -- 如新服务器未安装 Nginx/Certbot,可运行 `bash deploy.sh` 补装(脚本会跳过已有组件) -- 确保新服务器 Gitea 版本 ≥ 旧服务器版本(不支持降级) -- 官方建议使用 `mysqldump` 而非 `gitea dump` 的 XORM 导出(脚本已采用此方案) - -
-手动迁移步骤(不使用脚本) - -```bash -# ===== 旧服务器 ===== -cd /opt/gitea - # 1. 停止 Gitea docker compose stop server @@ -677,11 +613,82 @@ docker compose exec -u git server \ /usr/local/bin/gitea -c /data/gitea/conf/app.ini doctor check --all --fix ``` +**迁移后注意事项:** +- 如域名或 IP 变更,导入前需修改 `.env` 中的 `GITEA_DOMAIN` +- 域名变更后需更新 DNS 解析并重新申请 SSL 证书:`certbot certonly --webroot -w /var/www/certbot -d 新域名` +- 如新服务器未安装 Nginx/Certbot,可运行 `bash deploy.sh` 补装(脚本会跳过已有组件) +- 确保新服务器 Gitea 版本 ≥ 旧服务器版本(不支持降级) +- 官方建议使用 `mysqldump` 而非 `gitea dump` 的 XORM 导出 + +--- + +## 五、完全卸载 + +如果需要从服务器上完全移除 Gitea,使用卸载脚本: + +```bash +cd /opt/gitea +bash uninstall.sh +``` + +脚本会**交互式确认**每个危险操作,按顺序执行: + +| 步骤 | 操作 | 确认方式 | +|------|------|----------| +| 0 | 卸载前备份(可选) | y/N | +| 1 | 停止并删除 Gitea + MySQL 容器和卷 | 输入 YES | +| 2 | 删除 Docker 镜像 | 自动 | +| 3 | 删除 Nginx 站点配置并重载 | 自动 | +| 4 | 删除 Let's Encrypt SSL 证书 | 自动 | +| 5 | 关闭防火墙 SSH 端口 (2222) | 自动 | +| 6 | 清理 Certbot 定时任务(仅当无其他证书时) | 自动 | +| 7 | 删除数据目录(Gitea + MySQL) | 输入 DELETE | +| 8 | 删除部署目录 `/opt/gitea` | y/N | + +**备份目录 `/var/backups/gitea/` 始终保留**,不会被删除。 + +> 卸载后如需恢复,参考上方「恢复备份」章节。 + +
+手动卸载步骤(不使用脚本) + +```bash +cd /opt/gitea + +# 1. 建议先备份 +bash backup.sh + +# 2. 停止并删除容器、网络、卷 +docker compose down -v + +# 3. 删除 Docker 镜像(可选) +docker image rm gitea/gitea:1.25 mysql:8.4 + +# 4. 删除 Nginx 配置 +rm -f /etc/nginx/sites-enabled/gitea /etc/nginx/sites-available/gitea +nginx -t && systemctl reload nginx + +# 5. 删除 SSL 证书 +certbot delete --cert-name 你的域名 + +# 6. 关闭防火墙端口 +ufw delete allow 2222/tcp && ufw reload + +# 7. 删除数据目录(⚠ 不可恢复) +rm -rf /var/lib/gitea +rm -rf /var/lib/mysql/gitea + +# 8. 删除部署目录(可选) +rm -rf /opt/gitea + +# 备份目录保留在 /var/backups/gitea/ +``` +
--- -## 五、安全加固清单 +## 六、安全加固清单 - [ ] 域名已启用 HTTPS(deploy.sh 自动完成) - [ ] SSL 证书自动续期(deploy.sh 自动配置 cron) @@ -696,7 +703,7 @@ docker compose exec -u git server \ --- -## 六、常见问题 +## 七、常见问题 **Q: 部署完成后访问显示 502 Bad Gateway?** - 这是正常现象,说明 Nginx 已就绪但 Gitea 容器尚未启动完成 @@ -731,16 +738,6 @@ docker compose exec server gitea admin user change-password -u 管理员用户 - 或安装 [Gpg4win](https://www.gpg4win.org/) - 生成密钥时请在 **Git Bash** 中执行 `gpg --full-generate-key`(PowerShell/CMD 下交互式生成会失败) -**Q: 如何迁移到新服务器?** -使用迁移脚本一键完成,详见上方「迁移到新服务器」章节: -```bash -# 旧服务器导出 -bash migrate.sh export -# 新服务器导入 -bash migrate.sh import gitea_migrate_xxx.tar.gz -bash migrate.sh verify -``` - --- ## 服务器目录结构 @@ -751,9 +748,8 @@ bash migrate.sh verify ├── .env.example # 环境变量模板 ├── .env # 运行时配置(自动生成) ├── deploy.sh # 全新服务器一键部署脚本 -├── upgrade.sh # 组件升级脚本(Gitea/MySQL/Nginx/Certbot/Docker) -├── migrate.sh # 服务器迁移脚本(导出/导入/验证) ├── backup.sh # MySQL + 数据备份脚本 +├── uninstall.sh # 完全卸载脚本 ├── .gitignore ├── README.md └── nginx/ diff --git a/gitea/backup.sh b/gitea/backup.sh index ffe4346..cf2a00c 100755 --- a/gitea/backup.sh +++ b/gitea/backup.sh @@ -16,6 +16,7 @@ if [ ! -f .env ]; then exit 1 fi set -a +sed -i 's/\r$//' .env source .env set +a diff --git a/gitea/deploy.sh b/gitea/deploy.sh index b6d60b9..ef4f077 100755 --- a/gitea/deploy.sh +++ b/gitea/deploy.sh @@ -147,19 +147,39 @@ install_docker() { log "Docker Compose 已就绪: $(docker compose version --short)" } -# ===== 3. 安装 Nginx ===== +# ===== 3. 安装 Nginx(官方稳定版仓库)===== install_nginx() { step "3/8 安装 Nginx" if command -v nginx &> /dev/null; then log "Nginx 已安装: $(nginx -v 2>&1)" else - log "正在安装 Nginx..." + log "正在安装 Nginx(官方 stable 仓库)..." case "$PKG_MGR" in apt) + # 添加 Nginx 官方 GPG 密钥 + curl -fsSL https://nginx.org/keys/nginx_signing.key \ + | gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg + # 添加官方 stable 仓库 + echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ + http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \ + > /etc/apt/sources.list.d/nginx.list + # 优先使用官方仓库 + printf "Package: *\nPin: origin nginx.org\nPin-Priority: 900\n" \ + > /etc/apt/preferences.d/99nginx + apt-get update -qq apt-get install -y -qq nginx ;; dnf|yum) + cat > /etc/yum.repos.d/nginx.repo <<'REPO' +[nginx-stable] +name=nginx stable repo +baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ +gpgcheck=1 +enabled=1 +gpgkey=https://nginx.org/keys/nginx_signing.key +module_hotfixes=true +REPO $PKG_MGR install -y -q nginx ;; esac @@ -171,10 +191,13 @@ install_nginx() { # 确保 Nginx 配置目录结构存在 mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /var/www/certbot - # 确保 nginx.conf 包含 sites-enabled + # Nginx 官方包使用 conf.d/ 而非 sites-enabled/,添加 include if ! grep -q "sites-enabled" /etc/nginx/nginx.conf; then - # 在 http { } 块内添加 include - sed -i '/^http {/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf + if grep -q "^http {" /etc/nginx/nginx.conf; then + sed -i '/^http {/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf + else + sed -i '/include.*conf\.d/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf + fi fi log "Nginx 配置就绪" @@ -213,6 +236,7 @@ init_env() { fi # 加载并验证配置 + sed -i 's/\r$//' .env set -a source .env set +a @@ -331,8 +355,9 @@ server { NGINX_TEMP ln -sf /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/gitea - # 移除默认站点避免冲突 + # 移除默认站点避免冲突(Ubuntu 包和官方包路径不同) rm -f /etc/nginx/sites-enabled/default + rm -f /etc/nginx/conf.d/default.conf nginx -t && systemctl reload nginx # 申请 SSL 证书 diff --git a/gitea/migrate.sh b/gitea/migrate.sh deleted file mode 100755 index 222e1a4..0000000 --- a/gitea/migrate.sh +++ /dev/null @@ -1,513 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ============================================ -# Gitea 迁移脚本(旧服务器 → 新服务器) -# 用法: -# bash migrate.sh export # 旧服务器:导出迁移包 -# bash migrate.sh import <迁移包路径> # 新服务器:导入迁移包 -# bash migrate.sh verify # 迁移后:验证完整性 -# -# 迁移流程: -# 旧服务器 → export → scp 传输 → 新服务器 → import → verify -# ============================================ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -log() { echo -e "${GREEN}[INFO]${NC} $*"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } -step() { echo -e "\n${CYAN}========== $* ==========${NC}"; } -confirm() { - echo -en "${YELLOW}[确认]${NC} $* [y/N]: " - read -r reply - [[ "$reply" =~ ^[Yy]$ ]] -} - -# ===== 前置检查 ===== -preflight() { - if [ "$(id -u)" -ne 0 ]; then - error "请使用 root 用户运行: sudo bash migrate.sh" - exit 1 - fi -} - -load_env() { - if [ ! -f .env ]; then - error ".env 文件不存在" - exit 1 - fi - set -a - source .env - set +a -} - -# ============================================================= -# 导出模式 — 在旧服务器执行 -# ============================================================= -do_export() { - step "迁移导出 — 旧服务器" - - load_env - - GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}" - MYSQL_DATA_DIR="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}" - BACKUP_DIR="${BACKUP_DIR:-/var/backups/gitea}" - DATE=$(date +%Y%m%d_%H%M%S) - EXPORT_DIR="${BACKUP_DIR}/migrate_${DATE}" - EXPORT_ARCHIVE="${BACKUP_DIR}/gitea_migrate_${DATE}.tar.gz" - - mkdir -p "$EXPORT_DIR" - - # --- 1. 显示当前状态 --- - step "1/6 当前环境信息" - log "域名: ${GITEA_DOMAIN:-未设置}" - log "数据目录: ${GITEA_DATA_DIR}" - log "MySQL 数据: ${MYSQL_DATA_DIR}" - - if docker compose ps --status running 2>/dev/null | grep -q gitea; then - GITEA_VER=$(curl -sf http://127.0.0.1:3000/api/v1/version 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "未知") - log "Gitea 版本: ${GITEA_VER}" - fi - - MYSQL_VER=$(docker compose exec -T db mysql --version 2>/dev/null | grep -oP 'Distrib \K[0-9.]+' || echo "未知") - log "MySQL 版本: ${MYSQL_VER}" - - echo "" - warn "导出过程中 Gitea 将停止服务,以保证数据一致性。" - if ! confirm "是否继续?"; then - log "已取消" - rm -rf "$EXPORT_DIR" - exit 0 - fi - - # --- 2. 停止 Gitea,保留 MySQL 用于导出 --- - step "2/6 停止 Gitea 容器" - docker compose stop server 2>/dev/null || true - log "Gitea 已停止" - - # --- 3. 导出 MySQL 数据库 --- - step "3/6 导出 MySQL 数据库" - log "使用 mysqldump 导出(比 gitea dump 的 XORM 更可靠)..." - docker compose exec -T db mysqldump \ - -u root -p"${DB_ROOT_PASSWORD}" \ - --single-transaction \ - --routines \ - --triggers \ - --databases gitea \ - > "${EXPORT_DIR}/gitea-db.sql" - - DB_SIZE=$(du -h "${EXPORT_DIR}/gitea-db.sql" | cut -f1) - log "数据库导出完成: gitea-db.sql (${DB_SIZE})" - - # --- 4. 停止所有容器 --- - step "4/6 停止全部容器" - docker compose down - log "全部容器已停止" - - # --- 5. 打包 Gitea 数据 --- - step "5/6 打包 Gitea 数据目录" - if [ -d "$GITEA_DATA_DIR" ]; then - log "正在打包 ${GITEA_DATA_DIR} ..." - tar czf "${EXPORT_DIR}/gitea-data.tar.gz" \ - -C "$(dirname "$GITEA_DATA_DIR")" \ - "$(basename "$GITEA_DATA_DIR")" - DATA_SIZE=$(du -h "${EXPORT_DIR}/gitea-data.tar.gz" | cut -f1) - log "数据打包完成: gitea-data.tar.gz (${DATA_SIZE})" - else - warn "数据目录 ${GITEA_DATA_DIR} 不存在,跳过" - fi - - # --- 5b. 打包部署配置 --- - log "正在打包部署配置..." - tar czf "${EXPORT_DIR}/gitea-config.tar.gz" \ - -C "$SCRIPT_DIR" \ - --exclude='.git' \ - --exclude='data' \ - --exclude='backups' \ - .env docker-compose.yml .env.example \ - $([ -d nginx ] && echo "nginx/") \ - $([ -f backup.sh ] && echo "backup.sh") \ - $([ -f deploy.sh ] && echo "deploy.sh") \ - $([ -f upgrade.sh ] && echo "upgrade.sh") \ - $([ -f migrate.sh ] && echo "migrate.sh") \ - $([ -f .gitignore ] && echo ".gitignore") \ - $([ -f README.md ] && echo "README.md") \ - 2>/dev/null || true - log "配置打包完成: gitea-config.tar.gz" - - # --- 5c. 记录元信息 --- - cat > "${EXPORT_DIR}/migrate-meta.txt" <" - exit 1 - fi - - step "迁移导入 — 新服务器" - - # --- 1. 解压迁移包 --- - step "1/9 解压迁移包" - WORK_DIR=$(mktemp -d) - tar xzf "$archive" -C "$WORK_DIR" - MIGRATE_DIR=$(find "$WORK_DIR" -maxdepth 1 -type d -name 'migrate_*' | head -1) - - if [ -z "$MIGRATE_DIR" ]; then - error "无效的迁移包格式" - rm -rf "$WORK_DIR" - exit 1 - fi - - # 显示元信息 - if [ -f "${MIGRATE_DIR}/migrate-meta.txt" ]; then - log "迁移包信息:" - grep -v '^#' "${MIGRATE_DIR}/migrate-meta.txt" | while IFS='=' read -r key val; do - printf " %-20s %s\n" "$key:" "$val" - done - fi - - # --- 2. 检查必要文件 --- - step "2/9 检查迁移包内容" - local has_db=false has_data=false has_config=false - - [ -f "${MIGRATE_DIR}/gitea-db.sql" ] && has_db=true - [ -f "${MIGRATE_DIR}/gitea-data.tar.gz" ] && has_data=true - [ -f "${MIGRATE_DIR}/gitea-config.tar.gz" ] && has_config=true - - log "数据库导出: $(${has_db} && echo '✓' || echo '✗')" - log "数据目录: $(${has_data} && echo '✓' || echo '✗')" - log "部署配置: $(${has_config} && echo '✓' || echo '✗')" - - if ! $has_db || ! $has_data; then - error "迁移包缺少必要文件(需要 gitea-db.sql 和 gitea-data.tar.gz)" - rm -rf "$WORK_DIR" - exit 1 - fi - - echo "" - warn "导入将覆盖当前服务器上的 Gitea 数据。" - if ! confirm "是否继续?"; then - log "已取消" - rm -rf "$WORK_DIR" - exit 0 - fi - - # --- 3. 恢复配置文件 --- - step "3/9 恢复部署配置" - if $has_config; then - tar xzf "${MIGRATE_DIR}/gitea-config.tar.gz" -C "$SCRIPT_DIR" - log "配置文件已恢复到 ${SCRIPT_DIR}/" - fi - - # 加载 .env - load_env - GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}" - MYSQL_DATA_DIR="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}" - - # --- 4. 提示修改配置 --- - step "4/9 检查配置" - echo "" - warn "如果新服务器的域名或 IP 与旧服务器不同,请先修改 .env:" - echo -e " ${CYAN}vi ${SCRIPT_DIR}/.env${NC}" - echo "" - echo " 当前配置:" - echo " GITEA_DOMAIN=${GITEA_DOMAIN:-未设置}" - echo " GITEA_DATA_DIR=${GITEA_DATA_DIR}" - echo " MYSQL_DATA_DIR=${MYSQL_DATA_DIR}" - echo "" - - if ! confirm "配置正确,继续导入?(如需修改请选 N,改完后重新运行 import)"; then - log "已暂停。修改 .env 后重新运行:" - echo -e " ${CYAN}bash migrate.sh import ${archive}${NC}" - rm -rf "$WORK_DIR" - exit 0 - fi - - # --- 5. 停止现有服务 --- - step "5/9 停止现有服务" - docker compose down 2>/dev/null || true - - # --- 6. 安装基础设施(如新服务器未部署过) --- - step "6/9 检查基础设施" - if ! command -v docker &>/dev/null; then - warn "Docker 未安装。请先运行 deploy.sh 安装基础设施,或手动安装 Docker。" - echo -e " ${CYAN}bash deploy.sh${NC}" - rm -rf "$WORK_DIR" - exit 1 - fi - - if ! docker compose version &>/dev/null; then - error "Docker Compose V2 未安装" - rm -rf "$WORK_DIR" - exit 1 - fi - log "Docker 和 Compose 已就绪" - - # --- 7. 恢复 Gitea 数据目录 --- - step "7/9 恢复 Gitea 数据" - mkdir -p "$(dirname "$GITEA_DATA_DIR")" - if [ -d "$GITEA_DATA_DIR" ] && [ "$(ls -A "$GITEA_DATA_DIR" 2>/dev/null)" ]; then - warn "数据目录 ${GITEA_DATA_DIR} 非空" - if confirm "是否清空后恢复?(选 N 则覆盖合并)"; then - rm -rf "${GITEA_DATA_DIR:?}/"* - fi - fi - tar xzf "${MIGRATE_DIR}/gitea-data.tar.gz" -C "$(dirname "$GITEA_DATA_DIR")" - chown -R 1000:1000 "$GITEA_DATA_DIR" - log "Gitea 数据已恢复到 ${GITEA_DATA_DIR}" - - # --- 8. 恢复数据库 --- - step "8/9 恢复 MySQL 数据库" - - # 确保 MySQL 数据目录存在 - mkdir -p "$MYSQL_DATA_DIR" - - # 启动 MySQL(清空后让其自动初始化) - log "启动 MySQL 容器..." - docker compose up -d db - - # 等待 MySQL 就绪 - log "等待 MySQL 就绪..." - local retries=0 - while ! docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD}" --silent 2>/dev/null; do - retries=$((retries + 1)) - if [ "$retries" -ge 60 ]; then - error "MySQL 启动超时(60 次重试)" - rm -rf "$WORK_DIR" - exit 1 - fi - sleep 2 - done - log "MySQL 已就绪" - - # 导入数据库 - log "正在导入数据库(这可能需要几分钟)..." - docker compose exec -T db mysql \ - -u root -p"${DB_ROOT_PASSWORD}" \ - --default-character-set=utf8mb4 \ - < "${MIGRATE_DIR}/gitea-db.sql" - log "数据库导入完成" - - # --- 9. 启动 Gitea 并执行迁移后操作 --- - step "9/9 启动 Gitea 并验证" - docker compose up -d - log "容器已启动,等待 Gitea 就绪..." - - local retries=0 - while ! curl -sf http://127.0.0.1:3000/api/v1/version &>/dev/null; do - retries=$((retries + 1)) - if [ "$retries" -ge 60 ]; then - warn "Gitea 启动超时,请手动检查: docker compose logs server" - break - fi - sleep 2 - done - - if curl -sf http://127.0.0.1:3000/api/v1/version &>/dev/null; then - GITEA_VER=$(curl -sf http://127.0.0.1:3000/api/v1/version | grep -oP '"version"\s*:\s*"\K[^"]+') - log "Gitea ${GITEA_VER} 启动成功" - fi - - # 重新生成 Git Hooks(迁移后必须执行) - log "重新生成 Git Hooks..." - docker compose exec -u git server \ - /usr/local/bin/gitea -c /data/gitea/conf/app.ini admin regenerate hooks || \ - warn "regenerate hooks 失败,请手动执行" - - # 运行 doctor 检查 - log "运行 doctor 诊断..." - docker compose exec -u git server \ - /usr/local/bin/gitea -c /data/gitea/conf/app.ini doctor check --all --fix 2>&1 | \ - tail -5 || warn "doctor check 返回异常,请检查日志" - - # 清理 - rm -rf "$WORK_DIR" - - echo "" - log "===== 导入完成 =====" - log "Gitea 已在新服务器运行" - echo "" - warn "迁移后请检查以下事项:" - echo " 1. 访问 https://${GITEA_DOMAIN:-你的域名} 确认页面正常" - echo " 2. 如域名/IP 变更,需更新 DNS 解析" - echo " 3. 如需 HTTPS,需重新申请 SSL 证书:" - echo -e " ${CYAN}certbot certonly --webroot -w /var/www/certbot -d ${GITEA_DOMAIN:-你的域名}${NC}" - echo " 4. 测试 Git clone / push 操作" - echo " 5. 测试 SSH 克隆: git clone ssh://git@${GITEA_DOMAIN:-你的域名}:${SSH_PORT:-2222}/用户/仓库.git" - echo "" - log "运行验证命令检查完整性: bash migrate.sh verify" -} - -# ============================================================= -# 验证模式 — 迁移后验证完整性 -# ============================================================= -do_verify() { - step "迁移后验证" - - load_env - GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}" - local pass=0 fail=0 - - check() { - if eval "$2" &>/dev/null; then - echo -e " ${GREEN}✓${NC} $1" - pass=$((pass + 1)) - else - echo -e " ${RED}✗${NC} $1" - fail=$((fail + 1)) - fi - } - - echo "" - log "检查服务状态:" - check "Gitea 容器运行中" "docker compose ps --status running | grep -q gitea" - check "MySQL 容器运行中" "docker compose ps --status running | grep -q gitea-db" - - log "检查 API:" - check "Gitea API 可达" "curl -sf http://127.0.0.1:3000/api/v1/version" - - if curl -sf http://127.0.0.1:3000/api/v1/version &>/dev/null; then - GITEA_VER=$(curl -sf http://127.0.0.1:3000/api/v1/version | grep -oP '"version"\s*:\s*"\K[^"]+') - log " Gitea 版本: ${GITEA_VER}" - fi - - log "检查数据库:" - check "MySQL 连接正常" "docker compose exec -T db mysqladmin ping -h localhost -u root -p'${DB_ROOT_PASSWORD}' --silent" - - # 检查仓库数量 - REPO_COUNT=$(docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD}" -N -e \ - "SELECT COUNT(*) FROM gitea.repository;" 2>/dev/null || echo "0") - log " 仓库数量: ${REPO_COUNT}" - - USER_COUNT=$(docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD}" -N -e \ - "SELECT COUNT(*) FROM gitea.user;" 2>/dev/null || echo "0") - log " 用户数量: ${USER_COUNT}" - - log "检查数据目录:" - check "Gitea 数据目录存在" "[ -d '${GITEA_DATA_DIR}/gitea' ]" - check "仓库目录存在" "[ -d '${GITEA_DATA_DIR}/gitea/repositories' ] || [ -d '${GITEA_DATA_DIR}/git/repositories' ]" - check "app.ini 存在" "[ -f '${GITEA_DATA_DIR}/gitea/conf/app.ini' ]" - - # 统计仓库目录下的裸仓库数量 - if [ -d "${GITEA_DATA_DIR}/gitea/repositories" ]; then - DISK_REPOS=$(find "${GITEA_DATA_DIR}/gitea/repositories" -name "*.git" -type d -maxdepth 3 2>/dev/null | wc -l) - log " 磁盘上的仓库目录: ${DISK_REPOS}" - fi - - log "检查网络:" - check "SSH 端口可监听" "docker compose port server 2222" - - if command -v nginx &>/dev/null; then - check "Nginx 运行中" "systemctl is-active nginx" - check "Nginx 配置正确" "nginx -t" - else - warn " Nginx 未安装(如需 HTTPS 请运行 deploy.sh 或手动安装)" - fi - - echo "" - log "===== 验证结果: ${pass} 通过, ${fail} 失败 =====" - if [ "$fail" -gt 0 ]; then - warn "存在失败项,请检查上方输出并排查问题" - echo " 查看日志: docker compose logs" - return 1 - else - log "所有检查通过!迁移成功。" - fi -} - -# ============================================================= -# 主入口 -# ============================================================= -usage() { - echo "Gitea 迁移脚本" - echo "" - echo "用法:" - echo " bash migrate.sh export # 旧服务器:导出迁移包" - echo " bash migrate.sh import <迁移包路径> # 新服务器:导入迁移包" - echo " bash migrate.sh verify # 迁移后:验证完整性" - echo "" - echo "迁移流程:" - echo " 1. 旧服务器: bash migrate.sh export" - echo " 2. 传输: scp gitea_migrate_xxx.tar.gz root@新服务器:/opt/gitea/" - echo " 3. 新服务器: bash migrate.sh import gitea_migrate_xxx.tar.gz" - echo " 4. 新服务器: bash migrate.sh verify" -} - -main() { - preflight - - case "${1:-}" in - export) - do_export - ;; - import) - if [ -z "${2:-}" ]; then - error "请指定迁移包路径" - echo "用法: bash migrate.sh import <迁移包路径>" - exit 1 - fi - do_import "$2" - ;; - verify) - do_verify - ;; - -h|--help|help) - usage - ;; - *) - usage - exit 1 - ;; - esac -} - -main "$@" diff --git a/gitea/nginx/gitea.conf b/gitea/nginx/gitea.conf index 89b853b..2209ae9 100644 --- a/gitea/nginx/gitea.conf +++ b/gitea/nginx/gitea.conf @@ -20,8 +20,9 @@ server { # HTTPS 主站点 server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl; + listen [::]:443 ssl; + http2 on; server_name __GITEA_DOMAIN__; # SSL 证书(Certbot 自动管理) diff --git a/gitea/uninstall.sh b/gitea/uninstall.sh new file mode 100755 index 0000000..11464de --- /dev/null +++ b/gitea/uninstall.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================ +# Gitea 卸载脚本 +# 停止容器 → 备份数据 → 清理容器/镜像/配置/数据 +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# ===== 检查 root ===== +if [ "$(id -u)" -ne 0 ]; then + error "请使用 root 用户运行: sudo bash uninstall.sh" + exit 1 +fi + +# ===== 加载配置 ===== +if [ -f .env ]; then + sed -i 's/\r$//' .env + set -a + source .env + set +a +fi + +GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}" +MYSQL_DATA_DIR="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/gitea}" +GITEA_DOMAIN="${GITEA_DOMAIN:-}" +SSH_PORT="${SSH_PORT:-2222}" + +# ===== 确认操作 ===== +echo "" +echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}" +echo -e "${RED}║ ⚠ 即将卸载 Gitea 及所有数据 ⚠ ║${NC}" +echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}" +echo "" +echo "将执行以下操作:" +echo " 1. 停止并删除 Gitea + MySQL 容器" +echo " 2. 删除 Docker 镜像" +echo " 3. 删除 Nginx 站点配置" +echo " 4. 删除 SSL 证书" +echo " 5. 关闭防火墙端口 ${SSH_PORT}" +echo " 6. 删除 Certbot 自动续期定时任务" +echo "" +echo "涉及的数据目录:" +echo " Gitea 数据: ${GITEA_DATA_DIR}" +echo " MySQL 数据: ${MYSQL_DATA_DIR}" +echo " 备份目录: ${BACKUP_DIR}" +echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo -e "${YELLOW}备份目录将保留,不会被删除。${NC}" +echo "" +read -r -p "确定要继续卸载吗?输入 YES 确认: " confirm +if [ "$confirm" != "YES" ]; then + log "已取消卸载" + exit 0 +fi + +# ===== 卸载前备份 ===== +echo "" +read -r -p "是否在卸载前执行一次备份?(y/N): " do_backup +if [[ "$do_backup" =~ ^[Yy]$ ]]; then + if [ -f backup.sh ]; then + # 确保容器在运行(backup.sh 需要连接 MySQL) + if ! docker compose ps --quiet db 2>/dev/null | grep -q .; then + log "容器未运行,先启动容器以执行备份..." + docker compose up -d + log "等待 MySQL 就绪..." + for i in $(seq 1 30); do + if docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD:-}" --silent &>/dev/null; then + break + fi + sleep 2 + done + fi + log "正在执行备份..." + bash backup.sh + log "备份完成" + else + warn "backup.sh 不存在,跳过备份" + fi +fi + +# ===== 1. 停止并删除容器 ===== +echo "" +log "正在停止并删除容器..." +if docker compose ps --quiet 2>/dev/null | grep -q .; then + docker compose down -v + log "容器已停止并删除" +else + log "没有运行中的容器" +fi + +# ===== 2. 删除 Docker 镜像 ===== +log "正在删除 Docker 镜像..." +GITEA_IMAGE="${GITEA_IMAGE:-gitea/gitea:1.25}" +docker image rm "$GITEA_IMAGE" 2>/dev/null && log "已删除镜像: $GITEA_IMAGE" || true +docker image rm mysql:8.4 2>/dev/null && log "已删除镜像: mysql:8.4" || true + +# ===== 3. 删除 Nginx 配置 ===== +log "正在清理 Nginx 配置..." +rm -f /etc/nginx/sites-enabled/gitea +rm -f /etc/nginx/sites-available/gitea +if command -v nginx &>/dev/null && nginx -t 2>/dev/null; then + systemctl reload nginx 2>/dev/null || true + log "Nginx 已重载" +fi + +# ===== 4. 删除 SSL 证书 ===== +if [ -n "$GITEA_DOMAIN" ] && [ -d "/etc/letsencrypt/live/${GITEA_DOMAIN}" ]; then + log "正在删除 SSL 证书: ${GITEA_DOMAIN}..." + certbot delete --cert-name "${GITEA_DOMAIN}" --non-interactive 2>/dev/null || true + log "SSL 证书已删除" +fi + +# ===== 5. 关闭防火墙端口 ===== +log "正在关闭防火墙端口 ${SSH_PORT}..." +if command -v ufw &>/dev/null; then + ufw delete allow "${SSH_PORT}/tcp" 2>/dev/null || true + ufw reload 2>/dev/null || true +elif command -v firewall-cmd &>/dev/null; then + firewall-cmd --permanent --remove-port="${SSH_PORT}/tcp" 2>/dev/null || true + firewall-cmd --reload 2>/dev/null || true +fi +log "防火墙端口已关闭" + +# ===== 6. 清理 Certbot 定时任务中的 Gitea 相关条目 ===== +# 注意:certbot renew 是共享的,只有当没有其他证书时才移除 +remaining_certs=$(certbot certificates 2>/dev/null | grep -c "Certificate Name" || true) +if [ "$remaining_certs" -eq 0 ]; then + crontab -l 2>/dev/null | grep -v "certbot renew" | crontab - 2>/dev/null || true + log "已移除 Certbot 自动续期定时任务(无剩余证书)" +else + log "保留 Certbot 定时任务(还有 ${remaining_certs} 个其他证书)" +fi + +# ===== 7. 删除数据目录 ===== +echo "" +echo -e "${RED}以下目录将被永久删除:${NC}" +echo " ${GITEA_DATA_DIR}" +echo " ${MYSQL_DATA_DIR}" +echo "" +echo -e "${YELLOW}备份目录 ${BACKUP_DIR} 不会被删除。${NC}" +echo "" +read -r -p "确认删除数据目录?输入 DELETE 确认: " confirm_delete +if [ "$confirm_delete" = "DELETE" ]; then + rm -rf "$GITEA_DATA_DIR" + log "已删除: ${GITEA_DATA_DIR}" + rm -rf "$MYSQL_DATA_DIR" + log "已删除: ${MYSQL_DATA_DIR}" +else + warn "跳过数据目录删除" +fi + +# ===== 8. 删除部署目录 ===== +echo "" +read -r -p "是否删除部署目录 ${SCRIPT_DIR}?(y/N): " del_deploy +if [[ "$del_deploy" =~ ^[Yy]$ ]]; then + cd /opt + rm -rf "$SCRIPT_DIR" + log "已删除部署目录" +else + warn "保留部署目录: ${SCRIPT_DIR}" +fi + +# ===== 完成 ===== +echo "" +log "Gitea 卸载完成" +echo "" +echo "保留的内容:" +echo " 备份目录: ${BACKUP_DIR}" +[ "$confirm_delete" != "DELETE" ] && echo " 数据目录: ${GITEA_DATA_DIR}, ${MYSQL_DATA_DIR}" +[[ ! "$del_deploy" =~ ^[Yy]$ ]] && echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo "如需恢复,请参考 README.md 中的「恢复备份」章节。" diff --git a/gitea/upgrade.sh b/gitea/upgrade.sh deleted file mode 100755 index c435945..0000000 --- a/gitea/upgrade.sh +++ /dev/null @@ -1,617 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ============================================ -# Gitea 服务组件升级脚本 -# 支持升级:Gitea / MySQL / Nginx / Certbot / Docker -# 用法:bash upgrade.sh [组件名] -# bash upgrade.sh # 交互式选择 -# bash upgrade.sh gitea # 仅升级 Gitea -# bash upgrade.sh mysql # 仅升级 MySQL -# bash upgrade.sh nginx # 仅升级 Nginx -# bash upgrade.sh certbot # 仅升级 Certbot -# bash upgrade.sh docker # 仅升级 Docker -# bash upgrade.sh all # 升级全部组件 -# ============================================ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -log() { echo -e "${GREEN}[INFO]${NC} $*"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } -step() { echo -e "\n${CYAN}========== $* ==========${NC}"; } -confirm() { - echo -en "${YELLOW}[确认]${NC} $* [y/N]: " - read -r reply - [[ "$reply" =~ ^[Yy]$ ]] -} - -# ===== 前置检查 ===== -preflight() { - if [ "$(id -u)" -ne 0 ]; then - error "请使用 root 用户运行: sudo bash upgrade.sh" - exit 1 - fi - - if [ ! -f .env ]; then - error ".env 文件不存在,请先完成部署: bash deploy.sh" - exit 1 - fi - - set -a - source .env - set +a - - # 检测包管理器 - if command -v apt-get &> /dev/null; then - PKG_MGR="apt" - elif command -v dnf &> /dev/null; then - PKG_MGR="dnf" - elif command -v yum &> /dev/null; then - PKG_MGR="yum" - else - error "不支持的系统" - exit 1 - fi -} - -# ===== 获取当前版本信息 ===== -show_versions() { - step "当前组件版本" - - # Gitea - local gitea_ver="未运行" - if docker compose ps --format json 2>/dev/null | grep -q '"server"' || \ - docker compose ps 2>/dev/null | grep -q "gitea.*Up"; then - gitea_ver=$(curl -sf http://127.0.0.1:3000/api/v1/version 2>/dev/null | grep -o '"version":"[^"]*"' | cut -d'"' -f4 || echo "未知") - fi - local gitea_image - gitea_image=$(docker compose images server 2>/dev/null | tail -1 | awk '{print $2":"$3}' || echo "未知") - echo -e " Gitea: ${CYAN}${gitea_ver}${NC} (镜像: ${gitea_image})" - - # MySQL - local mysql_ver="未运行" - if docker compose ps --format json 2>/dev/null | grep -q '"db"' || \ - docker compose ps 2>/dev/null | grep -q "gitea-db.*Up"; then - mysql_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知") - fi - echo -e " MySQL: ${CYAN}${mysql_ver}${NC}" - - # Nginx - local nginx_ver - nginx_ver=$(nginx -v 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装") - echo -e " Nginx: ${CYAN}${nginx_ver}${NC}" - - # Certbot - local certbot_ver - certbot_ver=$(certbot --version 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装") - echo -e " Certbot: ${CYAN}${certbot_ver}${NC}" - - # Docker - local docker_ver - docker_ver=$(docker --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装") - local compose_ver - compose_ver=$(docker compose version --short 2>/dev/null || echo "未安装") - echo -e " Docker: ${CYAN}${docker_ver}${NC} (Compose: ${compose_ver})" - - # SSL 证书 - local domain="${GITEA_DOMAIN:-}" - if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/${domain}" ]; then - local cert_expiry - cert_expiry=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${domain}/fullchain.pem" 2>/dev/null | cut -d= -f2 || echo "未知") - echo -e " SSL证书: 到期 ${CYAN}${cert_expiry}${NC}" - fi - echo "" -} - -# ===== 备份 ===== -do_backup() { - step "升级前备份" - - if [ -f "$SCRIPT_DIR/backup.sh" ]; then - log "正在执行完整备份..." - bash "$SCRIPT_DIR/backup.sh" - log "备份完成" - else - warn "backup.sh 不存在,跳过自动备份" - if ! confirm "继续升级(未备份)?"; then - error "已取消" - exit 1 - fi - fi -} - -# ============================================================ -# 升级 Gitea(Docker 容器) -# ============================================================ -upgrade_gitea() { - step "升级 Gitea" - - local current_image - current_image=$(grep -E '^\s*image:' docker-compose.yml | head -1 | awk '{print $2}' | envsubst || echo "unknown") - log "当前镜像配置: $(grep -E 'GITEA_IMAGE' .env 2>/dev/null | head -1 || echo '使用默认')" - - # 获取远程最新 tag - log "检查最新版本..." - local latest_tag="" - # 尝试从 Docker Hub API 获取最新稳定版 - latest_tag=$(curl -sf "https://registry.hub.docker.com/v2/repositories/gitea/gitea/tags?page_size=50&ordering=last_updated" 2>/dev/null \ - | grep -o '"name":"[0-9]\+\.[0-9]\+\.[0-9]\+"' \ - | head -1 \ - | cut -d'"' -f4 || echo "") - - if [ -n "$latest_tag" ]; then - log "Docker Hub 最新稳定版: $latest_tag" - else - warn "无法查询最新版本,请手动确认目标版本" - fi - - echo "" - echo -e " ${YELLOW}升级方式说明:${NC}" - echo " ┌──────────────────────────────────────────────────────┐" - echo " │ 方式 A(推荐):修改 .env 中的 GITEA_IMAGE 后拉取 │" - echo " │ 方式 B:直接拉取当前配置的镜像(获取 tag 内最新构建) │" - echo " └──────────────────────────────────────────────────────┘" - echo "" - - local do_change_version="n" - if [ -n "$latest_tag" ]; then - echo -en " 是否更新 GITEA_IMAGE 到 ${CYAN}gitea/gitea:${latest_tag}${NC}?[y/N]: " - read -r do_change_version - fi - - if [[ "$do_change_version" =~ ^[Yy]$ ]] && [ -n "$latest_tag" ]; then - # 更新 .env 中的 GITEA_IMAGE - if grep -q "^GITEA_IMAGE=" .env; then - sed -i "s|^GITEA_IMAGE=.*|GITEA_IMAGE=gitea/gitea:${latest_tag}|" .env - else - echo "GITEA_IMAGE=gitea/gitea:${latest_tag}" >> .env - fi - log "已更新 .env: GITEA_IMAGE=gitea/gitea:${latest_tag}" - # 重新加载 - set -a; source .env; set +a - fi - - log "正在拉取 Gitea 镜像..." - docker compose pull server - - log "正在重启 Gitea 容器(数据库迁移会自动执行)..." - docker compose up -d server - - # 等待 Gitea 就绪 - log "等待 Gitea 启动..." - local ready=0 - for i in $(seq 1 60); do - if curl -sf http://127.0.0.1:3000/api/v1/version &> /dev/null; then - ready=1 - break - fi - sleep 2 - done - - if [ "$ready" -eq 1 ]; then - local new_ver - new_ver=$(curl -sf http://127.0.0.1:3000/api/v1/version | grep -o '"version":"[^"]*"' | cut -d'"' -f4) - log "Gitea 升级成功!当前版本: $new_ver" - else - warn "Gitea 可能仍在启动,请检查日志: docker compose logs -f server" - fi - - echo "" - warn "请浏览器访问 https://${GITEA_DOMAIN:-你的域名} 确认功能正常" -} - -# ============================================================ -# 升级 MySQL(Docker 容器 — 小版本升级) -# ============================================================ -upgrade_mysql() { - step "升级 MySQL" - - local current_ver - current_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知") - log "当前 MySQL 版本: $current_ver" - - local current_image - current_image=$(grep -E 'image:\s*mysql' docker-compose.yml | awk '{print $2}' | tr -d '"' || echo "mysql:8.0") - log "当前镜像: $current_image" - - echo "" - echo -e " ${YELLOW}MySQL 升级须知:${NC}" - echo " ┌──────────────────────────────────────────────────────────┐" - echo " │ ● 小版本升级(8.0.x → 8.0.y):拉取新镜像重启即可 │" - echo " │ ● 大版本升级(8.0 → 8.4/9.0):需要额外迁移步骤 │" - echo " │ 大版本升级建议:mysqldump 导出 → 新版本容器 → 导入 │" - echo " │ ● MySQL 仅支持相邻大版本升级,不可跨版本 │" - echo " └──────────────────────────────────────────────────────────┘" - echo "" - - # 选择升级类型 - echo " 选择升级类型:" - echo " 1) 小版本升级 — 拉取 mysql:8.0 最新补丁(推荐)" - echo " 2) 大版本升级 — 升级到 MySQL 8.4 LTS" - echo " 3) 大版本升级 — 升级到 MySQL 9.x(创新版本)" - echo " 0) 跳过 MySQL 升级" - echo "" - echo -en " 请选择 [0-3]: " - read -r mysql_choice - - case "$mysql_choice" in - 1) - _mysql_minor_upgrade - ;; - 2) - _mysql_major_upgrade "mysql:8.4" "8.4" - ;; - 3) - _mysql_major_upgrade "mysql:9.0" "9.0" - ;; - *) - log "跳过 MySQL 升级" - return - ;; - esac -} - -_mysql_minor_upgrade() { - log "正在拉取 MySQL 8.0 最新镜像..." - docker compose pull db - - log "正在重启 MySQL 容器..." - docker compose up -d db - - # 等待就绪 - log "等待 MySQL 就绪..." - for i in $(seq 1 60); do - if docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD}" --silent &> /dev/null; then - break - fi - sleep 2 - done - - local new_ver - new_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知") - log "MySQL 小版本升级完成!当前版本: $new_ver" - - # 重启 Gitea 确保数据库连接正常 - log "重启 Gitea 以重新连接数据库..." - docker compose restart server -} - -_mysql_major_upgrade() { - local target_image="$1" - local target_ver="$2" - - warn "⚠️ MySQL 大版本升级有风险,确保已完成备份!" - echo "" - if ! confirm "确认要将 MySQL 升级到 ${target_ver}?"; then - log "已取消" - return - fi - - log "第 1 步:导出当前数据库..." - local dump_file="/tmp/gitea_mysql_upgrade_$(date +%Y%m%d_%H%M%S).sql" - docker compose exec -T db mysqldump \ - -u root -p"${DB_ROOT_PASSWORD}" \ - --single-transaction \ - --routines \ - --triggers \ - --databases gitea \ - > "$dump_file" - local dump_size - dump_size=$(du -h "$dump_file" | cut -f1) - log "数据库导出完成: $dump_file ($dump_size)" - - log "第 2 步:停止服务..." - docker compose down - - log "第 3 步:备份 MySQL 数据目录..." - local mysql_data="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}" - local backup_mysql="${mysql_data}.bak.$(date +%Y%m%d_%H%M%S)" - cp -a "$mysql_data" "$backup_mysql" - log "数据目录已备份至: $backup_mysql" - - log "第 4 步:清空 MySQL 数据目录(新版本将重新初始化)..." - rm -rf "${mysql_data:?}"/* - - log "第 5 步:更新 docker-compose.yml 中的 MySQL 镜像..." - sed -i "s|image: mysql:8\.0|image: ${target_image}|" docker-compose.yml - - # MySQL 8.4+ 不再需要 --mysql-native-password - if [[ "$target_ver" != "8.0" ]]; then - warn "MySQL ${target_ver} 默认使用 caching_sha2_password" - warn "移除 --mysql-native-password=ON 参数(如有兼容问题可恢复)" - sed -i '/--mysql-native-password=ON/d' docker-compose.yml - fi - - log "第 6 步:启动新版本 MySQL..." - docker compose up -d db - - log "等待 MySQL ${target_ver} 初始化..." - for i in $(seq 1 90); do - if docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD}" --silent &> /dev/null; then - break - fi - if [ "$i" -eq 90 ]; then - error "MySQL 启动超时!请检查日志: docker compose logs db" - error "如需回滚:cp -a $backup_mysql/* $mysql_data/ 并恢复 docker-compose.yml" - exit 1 - fi - sleep 2 - done - - log "第 7 步:导入数据库..." - docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD}" < "$dump_file" - log "数据库导入完成" - - log "第 8 步:启动 Gitea..." - docker compose up -d - - local new_ver - new_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知") - log "MySQL 大版本升级完成!当前版本: $new_ver" - - echo "" - warn "导出文件保留在: $dump_file" - warn "旧数据目录保留在: $backup_mysql" - warn "确认运行正常后可手动删除以上文件" -} - -# ============================================================ -# 升级 Nginx(系统包) -# ============================================================ -upgrade_nginx() { - step "升级 Nginx" - - local current_ver - current_ver=$(nginx -v 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知") - log "当前 Nginx 版本: $current_ver" - - log "正在更新 Nginx..." - case "$PKG_MGR" in - apt) - apt-get update -qq - apt-get install -y --only-upgrade nginx - ;; - dnf) - dnf upgrade -y nginx - ;; - yum) - yum update -y nginx - ;; - esac - - local new_ver - new_ver=$(nginx -v 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知") - - if [ "$current_ver" = "$new_ver" ]; then - log "Nginx 已是最新版: $new_ver" - else - log "Nginx 已更新: $current_ver → $new_ver" - fi - - # 验证配置 - log "验证 Nginx 配置..." - if nginx -t 2>&1; then - systemctl reload nginx - log "Nginx 配置验证通过并已重载" - else - error "Nginx 配置验证失败!请手动检查" - error " nginx -t" - error " vi /etc/nginx/sites-available/gitea" - fi -} - -# ============================================================ -# 升级 Certbot(系统包) -# ============================================================ -upgrade_certbot() { - step "升级 Certbot" - - local current_ver - current_ver=$(certbot --version 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装") - log "当前 Certbot 版本: $current_ver" - - log "正在更新 Certbot..." - case "$PKG_MGR" in - apt) - apt-get update -qq - apt-get install -y --only-upgrade certbot python3-certbot-nginx - ;; - dnf) - dnf upgrade -y certbot python3-certbot-nginx - ;; - yum) - yum update -y certbot python3-certbot-nginx - ;; - esac - - local new_ver - new_ver=$(certbot --version 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知") - - if [ "$current_ver" = "$new_ver" ]; then - log "Certbot 已是最新版: $new_ver" - else - log "Certbot 已更新: $current_ver → $new_ver" - fi - - # 测试证书续期 - log "测试证书续期(dry-run)..." - if certbot renew --dry-run 2>&1 | tail -3; then - log "证书续期测试通过" - else - warn "证书续期测试失败,请检查 certbot 配置" - fi - - # 检查证书有效期 - local domain="${GITEA_DOMAIN:-}" - if [ -n "$domain" ] && [ -f "/etc/letsencrypt/live/${domain}/fullchain.pem" ]; then - local expiry - expiry=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${domain}/fullchain.pem" | cut -d= -f2) - log "当前证书到期时间: $expiry" - - # 检查是否30天内到期 - local expiry_epoch - expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || echo 0) - local now_epoch - now_epoch=$(date +%s) - local days_left=$(( (expiry_epoch - now_epoch) / 86400 )) - - if [ "$days_left" -lt 30 ] && [ "$days_left" -gt 0 ]; then - warn "证书将在 ${days_left} 天后到期" - if confirm "是否立即续期?"; then - certbot renew --force-renewal --post-hook 'systemctl reload nginx' - log "证书已续期" - fi - elif [ "$days_left" -le 0 ]; then - error "证书已过期!正在强制续期..." - certbot renew --force-renewal --post-hook 'systemctl reload nginx' - else - log "证书有效期剩余: ${days_left} 天" - fi - fi -} - -# ============================================================ -# 升级 Docker(系统包) -# ============================================================ -upgrade_docker() { - step "升级 Docker" - - local current_ver - current_ver=$(docker --version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知") - log "当前 Docker 版本: $current_ver" - - log "正在更新 Docker..." - case "$PKG_MGR" in - apt) - apt-get update -qq - apt-get install -y --only-upgrade docker-ce docker-ce-cli containerd.io docker-compose-plugin - ;; - dnf) - dnf upgrade -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - ;; - yum) - yum update -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - ;; - esac - - local new_ver - new_ver=$(docker --version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知") - - if [ "$current_ver" = "$new_ver" ]; then - log "Docker 已是最新版: $new_ver" - else - log "Docker 已更新: $current_ver → $new_ver" - warn "Docker 已更新,容器将自动重启" - fi - - local compose_ver - compose_ver=$(docker compose version --short 2>/dev/null || echo "未知") - log "Docker Compose 版本: $compose_ver" - - # 确保服务正常运行 - log "确认容器运行状态..." - docker compose ps -} - -# ============================================================ -# 升级全部 -# ============================================================ -upgrade_all() { - step "升级全部组件" - echo "" - warn "将依次升级: Docker → Nginx → Certbot → MySQL → Gitea" - warn "升级前会自动执行完整备份" - echo "" - - if ! confirm "确认升级全部组件?"; then - log "已取消" - exit 0 - fi - - do_backup - upgrade_docker - upgrade_nginx - upgrade_certbot - upgrade_mysql - upgrade_gitea - - step "全部组件升级完成" - show_versions -} - -# ============================================================ -# 交互菜单 -# ============================================================ -interactive_menu() { - echo "" - echo -e " ${CYAN}可升级组件:${NC}" - echo " 1) Gitea — Docker 容器镜像升级" - echo " 2) MySQL — Docker 容器镜像升级(支持大/小版本)" - echo " 3) Nginx — 系统包升级" - echo " 4) Certbot — 系统包升级 + 证书检查" - echo " 5) Docker — Docker Engine + Compose 升级" - echo " 6) 全部升级 — 依次升级所有组件" - echo " 0) 退出" - echo "" - echo -en " 请选择 [0-6]: " - read -r choice - - case "$choice" in - 1) do_backup; upgrade_gitea ;; - 2) do_backup; upgrade_mysql ;; - 3) upgrade_nginx ;; - 4) upgrade_certbot ;; - 5) do_backup; upgrade_docker ;; - 6) upgrade_all ;; - 0) log "退出"; exit 0 ;; - *) error "无效选择"; exit 1 ;; - esac -} - -# ============================================================ -# 主入口 -# ============================================================ -main() { - echo -e "${CYAN}" - echo " ____ _ _" - echo " / ___|(_) |_ ___ __ _" - echo "| | _ | | __/ _ \\/ _\` |" - echo "| |_| || | || __/ (_| |" - echo " \\____|_|\\__\\___|\\__,_| Upgrade Script" - echo -e "${NC}" - - preflight - show_versions - - local target="${1:-}" - - case "$target" in - gitea) do_backup; upgrade_gitea ;; - mysql) do_backup; upgrade_mysql ;; - nginx) upgrade_nginx ;; - certbot) upgrade_certbot ;; - docker) do_backup; upgrade_docker ;; - all) upgrade_all ;; - "") interactive_menu ;; - *) - error "未知组件: $target" - echo " 用法: bash upgrade.sh [gitea|mysql|nginx|certbot|docker|all]" - exit 1 - ;; - esac - - echo "" - step "升级后版本信息" - show_versions - log "升级完成!请检查服务是否正常: docker compose ps" -} - -main "$@" diff --git a/portainer/.env.example b/portainer/.env.example new file mode 100644 index 0000000..26410ce --- /dev/null +++ b/portainer/.env.example @@ -0,0 +1,21 @@ +# =========================================== +# Portainer CE 配置文件 +# =========================================== + +# ----- 域名与证书 ----- +# 访问域名(必填,如 docker.example.com) +PORTAINER_DOMAIN=docker.example.com + +# Let's Encrypt 证书邮箱(必填) +CERTBOT_EMAIL=admin@example.com + +# ----- Docker 镜像 ----- +# Portainer CE 镜像(默认使用 LTS 长期支持版) +PORTAINER_IMAGE=portainer/portainer-ce:lts + +# ----- 数据目录 ----- +# Portainer 数据目录 +PORTAINER_DATA_DIR=/var/lib/portainer + +# 备份目录 +BACKUP_DIR=/var/backups/portainer diff --git a/portainer/README.md b/portainer/README.md new file mode 100644 index 0000000..23f4a7c --- /dev/null +++ b/portainer/README.md @@ -0,0 +1,328 @@ +# Portainer CE 部署指南 + +Docker 可视化管理工具,支持容器、镜像、网络、卷的 Web 管理,以及实时日志、终端接入、堆栈管理等功能。 + +## 功能特性 + +- Web 可视化管理 Docker 容器、镜像、网络、卷 +- Docker Compose 堆栈管理(在线编辑、部署、更新) +- 容器实时日志查看 +- 容器内终端(Web Console) +- 镜像拉取、构建、删除 +- 容器资源监控(CPU、内存、网络) +- 多用户权限管理 +- 支持 Docker Standalone / Swarm / Kubernetes + +## 技术栈 + +| 组件 | 版本 | 说明 | +|------|------|------| +| Portainer CE | 2.39 LTS | Docker 可视化管理(社区版) | +| Nginx | 系统包 | 反向代理 + HTTPS 接入 | +| Docker | 最新版 | 容器运行环境 | + +## 前置条件 + +1. 一台 Linux 服务器(Ubuntu 22.04/24.04 推荐) +2. 一个已解析到服务器的域名(如 `docker.example.com`) +3. 服务器 80/443 端口可从外网访问 + +## 目录结构 + +``` +portainer/ +├── docker-compose.yml # 容器编排 +├── .env.example # 配置模板 +├── deploy.sh # 一键部署脚本 +├── backup.sh # 备份脚本 +├── uninstall.sh # 完全卸载脚本 +├── nginx/ +│ └── portainer.conf # Nginx 反向代理配置 +└── README.md # 本文件 +``` + +服务器上的数据目录: + +``` +/var/lib/portainer/ # Portainer 数据(BoltDB + TLS 证书) +/var/backups/portainer/ # 备份文件 +``` + +## 快速部署 + +### 第一步:上传文件到服务器 + +```bash +# 在本地执行,上传 portainer 目录 +scp -r portainer/ root@<服务器IP>:/opt/portainer + +# 如果服务器上还没有部署过 base(首台服务或全新服务器),还需上传 base +scp -r base/ root@<服务器IP>:/opt/base +``` + +### 第二步:登录服务器执行部署 + +```bash +ssh root@<服务器IP> + +# 如果是全新服务器,先安装基础环境 +cd /opt/base +cp .env.example .env +bash setup.sh + +# 部署 Portainer +cd /opt/portainer +bash deploy.sh +# 首次运行会生成 .env,按提示修改配置后重新运行 +vi .env +bash deploy.sh +``` + +### 第三步:配置域名解析 + +在域名服务商(如阿里云 DNS)添加 A 记录: + +| 记录类型 | 主机记录 | 记录值 | +|----------|----------|--------| +| A | docker | `<服务器公网IP>` | + +### 第四步:创建管理员账号 + +1. 浏览器访问 `https://docker.yourdomain.com` +2. **首次访问必须在 5 分钟内创建管理员账号** +3. 设置管理员用户名和密码(密码至少 12 位) + +> **⚠️ 重要:如果超过 5 分钟未创建账号,Portainer 会自动锁定。需要重启容器重新计时:** +> +> ```bash +> cd /opt/portainer && docker compose restart +> ``` + +### 第五步:连接本地 Docker + +1. 登录后选择「Get Started」 +2. 点击「local」环境即可管理本机 Docker +3. 可以看到所有运行中的容器、镜像、网络、卷 + +## 配置说明 + +### .env 配置项 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `PORTAINER_DOMAIN` | 访问域名 | 必填 | +| `CERTBOT_EMAIL` | Let's Encrypt 邮箱 | 必填 | +| `PORTAINER_IMAGE` | Docker 镜像 | `portainer/portainer-ce:lts` | +| `PORTAINER_DATA_DIR` | 数据目录 | `/var/lib/portainer` | +| `BACKUP_DIR` | 备份目录 | `/var/backups/portainer` | + +### 镜像版本说明 + +| 标签 | 说明 | +|------|------| +| `lts` | 长期支持版(推荐,当前为 2.39.x) | +| `sts` | 短期支持版(功能更新更快) | +| `latest` | 最新版(等同于 sts) | +| `2.39.1` | 指定版本号 | + +## 日常运维 + +### 查看日志 + +```bash +cd /opt/portainer +docker compose logs -f +docker compose logs --tail 100 +``` + +### 备份 + +```bash +cd /opt/portainer +bash backup.sh +``` + +备份内容包括: +- Portainer 数据(BoltDB 数据库 + TLS 证书 + 设置) +- 部署配置(`docker-compose.yml` + `.env` + `nginx/`) + +备份文件保存在 `/var/backups/portainer/`,自动清理 30 天前的旧备份。 + +### 恢复备份 + +```bash +# 查看可用备份 +ls /var/backups/portainer/ + +# 停止服务 +cd /opt/portainer && docker compose down + +# 恢复数据 +tar xzf /var/backups/portainer/<日期>/portainer-data.tar.gz -C /var/lib/ + +# 重启服务 +cd /opt/portainer && docker compose up -d +``` + +### 升级 + +```bash +cd /opt/portainer + +# 1. 备份当前数据 +bash backup.sh + +# 2. 拉取新镜像 +docker compose pull + +# 3. 停止旧容器并启动新容器 +docker compose down +docker compose up -d + +# 4. 检查运行状态 +docker compose ps +docker compose logs --tail 20 +``` + +### 停止 / 启动 + +```bash +cd /opt/portainer +docker compose down # 停止 +docker compose up -d # 启动 +docker compose restart # 重启 +``` + +## 完全卸载 + +如果需要从服务器上完全移除 Portainer,使用卸载脚本: + +```bash +cd /opt/portainer +bash uninstall.sh +``` + +脚本会**交互式确认**每个危险操作,按顺序执行: + +| 步骤 | 操作 | 确认方式 | +|------|------|----------| +| 0 | 卸载前备份(可选) | y/N | +| 1 | 停止并删除 Portainer 容器 | 输入 YES | +| 2 | 删除 Docker 镜像 | 自动 | +| 3 | 删除 Nginx 站点配置并重载 | 自动 | +| 4 | 删除 Let's Encrypt SSL 证书 | 自动 | +| 5 | 清理 Certbot 定时任务(仅当无其他证书时) | 自动 | +| 6 | 删除数据目录 | 输入 DELETE | +| 7 | 删除部署目录 `/opt/portainer` | y/N | + +**备份目录 `/var/backups/portainer/` 始终保留**,不会被删除。 + +> 卸载后如需恢复,参考上方「恢复备份」章节。 + +
+手动卸载步骤(不使用脚本) + +```bash +cd /opt/portainer + +# 1. 建议先备份 +bash backup.sh + +# 2. 停止并删除容器 +docker compose down -v + +# 3. 删除 Docker 镜像(可选) +docker image rm portainer/portainer-ce:lts + +# 4. 删除 Nginx 配置 +rm -f /etc/nginx/sites-enabled/portainer /etc/nginx/sites-available/portainer +nginx -t && systemctl reload nginx + +# 5. 删除 SSL 证书 +certbot delete --cert-name 你的域名 + +# 6. 删除数据目录(⚠ 不可恢复) +rm -rf /var/lib/portainer + +# 7. 删除部署目录(可选) +rm -rf /opt/portainer + +# 备份目录保留在 /var/backups/portainer/ +``` + +
+ +## 安全加固建议 + +### 1. 设置强密码 + +管理员密码至少 12 位,包含大小写字母、数字和特殊字符。 + +### 2. 禁用不需要的功能 + +在「Settings」→「Authentication」中: +- 关闭匿名访问 +- 配置会话超时时间 + +### 3. 限制 Docker Socket 访问 + +当前使用只读挂载 Docker Socket(`:ro`),Portainer 仍可管理容器但安全性更高。如需完整功能(如构建镜像),可移除 `:ro`。 + +### 4. 定期更新 + +Portainer CE 定期发布安全更新,建议关注 [GitHub Releases](https://github.com/portainer/portainer/releases) 并及时升级。 + +## 故障排查 + +### 首次访问超时锁定 + +如果首次访问时超过 5 分钟未创建管理员账号: + +```bash +cd /opt/portainer +docker compose restart +# 然后立即访问 Web 界面创建账号 +``` + +### 容器无法启动 + +```bash +# 查看容器状态 +docker compose ps + +# 查看详细日志 +docker compose logs --tail 50 +``` + +### 访问返回 502 + +```bash +# 检查 Portainer 容器是否运行 +docker compose ps + +# 检查 9000 端口 +curl -I http://127.0.0.1:9000 + +# 检查 Nginx 配置 +nginx -t +``` + +### Docker Socket 权限问题 + +如果 Portainer 无法连接 Docker: + +```bash +# 确认 socket 文件存在 +ls -la /var/run/docker.sock + +# 确认 Docker 服务运行 +systemctl status docker +``` + +## 端口说明 + +| 端口 | 协议 | 说明 | +|------|------|------| +| 80 | TCP | HTTP → HTTPS 重定向 | +| 443 | TCP | HTTPS(Nginx 反向代理) | +| 9000 | TCP | Portainer HTTP(仅监听 127.0.0.1) | diff --git a/portainer/backup.sh b/portainer/backup.sh new file mode 100755 index 0000000..932e9b9 --- /dev/null +++ b/portainer/backup.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================ +# Portainer 备份脚本 +# 备份数据目录 + 配置文件 +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# 加载配置 +if [ -f .env ]; then + sed -i 's/\r$//' .env + set -a; source .env; set +a +else + echo "[ERROR] .env 文件不存在,请先运行 deploy.sh" >&2 + exit 1 +fi + +# ===== 配置 ===== +DATA_DIR="${PORTAINER_DATA_DIR:-/var/lib/portainer}" +BACKUP_BASE="${BACKUP_DIR:-/var/backups/portainer}" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR_FULL="${BACKUP_BASE}/${TIMESTAMP}" +RETENTION_DAYS=30 + +echo "========== Portainer 备份 ==========" +echo "时间: $(date '+%Y-%m-%d %H:%M:%S')" +echo "数据目录: $DATA_DIR" +echo "备份目录: $BACKUP_DIR_FULL" +echo "" + +mkdir -p "$BACKUP_DIR_FULL" + +# ===== 备份数据目录(BoltDB + TLS 证书 + 配置)===== +echo "[1/3] 备份 Portainer 数据..." +if [ -d "$DATA_DIR" ]; then + tar czf "$BACKUP_DIR_FULL/portainer-data.tar.gz" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")" + echo " ✓ 数据目录已备份" +else + echo " ⚠ 数据目录不存在: $DATA_DIR" +fi + +# ===== 备份 docker-compose 和配置 ===== +echo "[2/3] 备份部署配置..." +tar czf "$BACKUP_DIR_FULL/portainer-config.tar.gz" \ + -C "$SCRIPT_DIR" \ + docker-compose.yml .env nginx/ 2>/dev/null || true +echo " ✓ 配置已备份" + +# ===== 清理旧备份 ===== +echo "[3/3] 清理 ${RETENTION_DAYS} 天前的旧备份..." +find "$BACKUP_BASE" -maxdepth 1 -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} \; 2>/dev/null || true +echo " ✓ 旧备份已清理" + +# ===== 汇总 ===== +echo "" +echo "========== 备份完成 ==========" +TOTAL_SIZE=$(du -sh "$BACKUP_DIR_FULL" | cut -f1) +echo "备份位置: $BACKUP_DIR_FULL" +echo "备份大小: $TOTAL_SIZE" +echo "" +echo "备份内容:" +ls -lh "$BACKUP_DIR_FULL/" +echo "" +echo "恢复方法:" +echo " tar xzf $BACKUP_DIR_FULL/portainer-data.tar.gz -C $(dirname "$DATA_DIR")" +echo " cd $SCRIPT_DIR && docker compose restart" diff --git a/portainer/deploy.sh b/portainer/deploy.sh new file mode 100755 index 0000000..e9dfebb --- /dev/null +++ b/portainer/deploy.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================ +# Portainer CE 一键部署脚本 +# 自动安装 Docker + Nginx + SSL + Portainer +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# ===== 加载公共基础函数 ===== +BASE_DIR="$(cd "$SCRIPT_DIR/../base" 2>/dev/null && pwd)" || true +if [ -z "$BASE_DIR" ] || [ ! -f "$BASE_DIR/setup.sh" ]; then + echo "[ERROR] base/setup.sh 未找到" >&2 + echo "请确保目录结构如下:" >&2 + echo " /opt/base/setup.sh" >&2 + echo " /opt/portainer/deploy.sh (当前脚本)" >&2 + exit 1 +fi +source "$BASE_DIR/setup.sh" + +# ============================================================= +# Portainer 专用函数 +# ============================================================= + +init_env() { + step "初始化 Portainer 配置" + + if [ ! -f .env ]; then + if [ ! -f .env.example ]; then + error "缺少 .env.example 模板文件" + exit 1 + fi + cp .env.example .env + log "已生成 .env 文件" + echo "" + warn "┌─────────────────────────────────────────────────┐" + warn "│ 请编辑 .env 文件,至少修改以下配置: │" + warn "│ │" + warn "│ PORTAINER_DOMAIN=docker.yourdomain.com │" + warn "│ CERTBOT_EMAIL=you@yourdomain.com │" + warn "│ │" + warn "│ 编辑命令: vi $SCRIPT_DIR/.env │" + warn "│ 编辑完成后重新运行: bash deploy.sh │" + warn "└─────────────────────────────────────────────────┘" + exit 0 + fi + + fix_crlf .env + set -a; source .env; set +a + + local has_error=0 + if [[ -z "${PORTAINER_DOMAIN:-}" ]] || [[ "${PORTAINER_DOMAIN}" == "docker.example.com" ]]; then + error "请在 .env 中将 PORTAINER_DOMAIN 修改为你的实际域名" + has_error=1 + fi + if [[ -z "${CERTBOT_EMAIL:-}" ]] || [[ "${CERTBOT_EMAIL}" == "admin@example.com" ]]; then + error "请在 .env 中将 CERTBOT_EMAIL 修改为你的实际邮箱" + has_error=1 + fi + [ "$has_error" -eq 1 ] && { error "请修改 .env 后重新运行"; exit 1; } + + log "配置检查通过" + log " 域名: ${PORTAINER_DOMAIN}" + log " 邮箱: ${CERTBOT_EMAIL}" +} + +create_dirs() { + step "创建数据目录" + local data_dir="${PORTAINER_DATA_DIR:-/var/lib/portainer}" + local backup_dir="${BACKUP_DIR:-/var/backups/portainer}" + + mkdir -p "$data_dir" "$backup_dir" + log "数据目录: $data_dir" + log "备份目录: $backup_dir" +} + +start_services() { + step "启动 Portainer 服务" + + log "正在拉取镜像..." + docker compose pull + + log "正在启动容器..." + docker compose up -d + + log "等待 Portainer 就绪..." + local max_wait=30 + for i in $(seq 1 "$max_wait"); do + if curl -sf http://127.0.0.1:9000/api/system/status &> /dev/null; then + log "Portainer 启动成功!" + return + fi + sleep 2 + done + warn "Portainer 可能仍在启动中,请稍后检查: docker compose logs -f" +} + +show_info() { + set -a; source .env; set +a + + echo "" + echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Portainer CE 部署完成! ║${NC}" + echo -e "${GREEN}╠══════════════════════════════════════════════════════════╣${NC}" + echo -e "${GREEN}║${NC}" + echo -e "${GREEN}║${NC} Web 访问: ${CYAN}https://${PORTAINER_DOMAIN}${NC}" + echo -e "${GREEN}║${NC}" + echo -e "${GREEN}║${NC} 数据目录: ${PORTAINER_DATA_DIR:-/var/lib/portainer}" + echo -e "${GREEN}║${NC} 备份目录: ${BACKUP_DIR:-/var/backups/portainer}" + echo -e "${GREEN}║${NC}" + echo -e "${GREEN}║${NC} ${RED}⚠ 首次访问请在 5 分钟内创建管理员账号!${NC}" + echo -e "${GREEN}║${NC} ${RED}⚠ 超时未创建将需要重启容器。${NC}" + echo -e "${GREEN}║${NC}" + echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" + echo "" + echo "常用命令:" + echo " 查看日志: cd $SCRIPT_DIR && docker compose logs -f" + echo " 重启服务: cd $SCRIPT_DIR && docker compose restart" + echo " 停止服务: cd $SCRIPT_DIR && docker compose down" + echo " 备份数据: cd $SCRIPT_DIR && bash backup.sh" + echo "" +} + +# ============================================================= +# 主流程 +# ============================================================= +main() { + echo -e "${CYAN}" + echo " ____ _ _" + echo " | _ \\ ___ _ __| |_ __ _(_)_ __ ___ _ __" + echo " | |_) / _ \\| '__| __/ _\` | | '_ \\ / _ \\ '__|" + echo " | __/ (_) | | | || (_| | | | | | __/ |" + echo " |_| \\___/|_| \\__\\__,_|_|_| |_|\\___|_| Deploy Script" + echo -e "${NC}" + echo "" + + check_root + load_base_env "$BASE_DIR" + + # Step 1: 系统初始化 + init_system + + # Step 2: 安装 Docker + install_docker + + # Step 3: 安装 Nginx + install_nginx + + # Step 4: 初始化配置 + init_env + + # Step 5: 配置 Docker 镜像加速 + configure_docker_mirrors + + # Step 6: 创建数据目录 + create_dirs + + # Step 7: 配置防火墙 + setup_firewall_base + + # Step 8: 配置 SSL 证书 + setup_ssl_cert "${PORTAINER_DOMAIN}" "${CERTBOT_EMAIL}" "portainer" + + # Step 9: 部署 Nginx 反向代理 + deploy_nginx_conf "$SCRIPT_DIR/nginx/portainer.conf" "${PORTAINER_DOMAIN}" "portainer" + + # Step 10: 启动服务 + start_services + + # 显示部署信息 + show_info + log "===== Portainer CE 部署完成 =====" +} + +main "$@" diff --git a/portainer/docker-compose.yml b/portainer/docker-compose.yml new file mode 100644 index 0000000..8be02e9 --- /dev/null +++ b/portainer/docker-compose.yml @@ -0,0 +1,24 @@ +services: + portainer: + image: ${PORTAINER_IMAGE:-portainer/portainer-ce:lts} + container_name: portainer + restart: unless-stopped + environment: + - TZ=Asia/Shanghai + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ${PORTAINER_DATA_DIR:-/var/lib/portainer}:/data + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:9000:9000" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:9000/api/system/status"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/portainer/nginx/portainer.conf b/portainer/nginx/portainer.conf new file mode 100644 index 0000000..c3ecfec --- /dev/null +++ b/portainer/nginx/portainer.conf @@ -0,0 +1,55 @@ +server { + listen 80; + listen [::]:80; + server_name __DOMAIN__; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name __DOMAIN__; + + # SSL 证书 + ssl_certificate /etc/letsencrypt/live/__DOMAIN__/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/__DOMAIN__/privkey.pem; + + # SSL 参数 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + # 安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + # 反向代理到 Portainer + location / { + proxy_pass http://127.0.0.1:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket 支持(Portainer 实时日志/终端需要) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置(终端会话需要较长超时) + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } +} diff --git a/portainer/uninstall.sh b/portainer/uninstall.sh new file mode 100755 index 0000000..423505b --- /dev/null +++ b/portainer/uninstall.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================ +# Portainer 卸载脚本 +# 停止容器 → 备份数据 → 清理容器/镜像/配置/数据 +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# ===== 检查 root ===== +if [ "$(id -u)" -ne 0 ]; then + error "请使用 root 用户运行: sudo bash uninstall.sh" + exit 1 +fi + +# ===== 加载配置 ===== +if [ -f .env ]; then + sed -i 's/\r$//' .env + set -a + source .env + set +a +fi + +PORTAINER_DATA_DIR="${PORTAINER_DATA_DIR:-/var/lib/portainer}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/portainer}" +PORTAINER_DOMAIN="${PORTAINER_DOMAIN:-}" + +# ===== 确认操作 ===== +echo "" +echo -e "${RED}╔══════════════════════════════════════════════════════╗${NC}" +echo -e "${RED}║ ⚠ 即将卸载 Portainer 及所有数据 ⚠ ║${NC}" +echo -e "${RED}╚══════════════════════════════════════════════════════╝${NC}" +echo "" +echo "将执行以下操作:" +echo " 1. 停止并删除 Portainer 容器" +echo " 2. 删除 Docker 镜像" +echo " 3. 删除 Nginx 站点配置" +echo " 4. 删除 SSL 证书" +echo " 5. 删除 Certbot 自动续期定时任务" +echo "" +echo "涉及的数据目录:" +echo " Portainer 数据: ${PORTAINER_DATA_DIR}" +echo " 备份目录: ${BACKUP_DIR}" +echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo -e "${YELLOW}备份目录将保留,不会被删除。${NC}" +echo "" +read -r -p "确定要继续卸载吗?输入 YES 确认: " confirm +if [ "$confirm" != "YES" ]; then + log "已取消卸载" + exit 0 +fi + +# ===== 卸载前备份 ===== +echo "" +read -r -p "是否在卸载前执行一次备份?(y/N): " do_backup +if [[ "$do_backup" =~ ^[Yy]$ ]]; then + if [ -f backup.sh ]; then + log "正在执行备份..." + bash backup.sh + log "备份完成" + else + warn "backup.sh 不存在,跳过备份" + fi +fi + +# ===== 1. 停止并删除容器 ===== +echo "" +log "正在停止并删除容器..." +if docker compose ps --quiet 2>/dev/null | grep -q .; then + docker compose down -v + log "容器已停止并删除" +else + log "没有运行中的容器" +fi + +# ===== 2. 删除 Docker 镜像 ===== +log "正在删除 Docker 镜像..." +PORTAINER_IMAGE="${PORTAINER_IMAGE:-portainer/portainer-ce:lts}" +docker image rm "$PORTAINER_IMAGE" 2>/dev/null && log "已删除镜像: $PORTAINER_IMAGE" || true + +# ===== 3. 删除 Nginx 配置 ===== +log "正在清理 Nginx 配置..." +rm -f /etc/nginx/sites-enabled/portainer +rm -f /etc/nginx/sites-available/portainer +if command -v nginx &>/dev/null && nginx -t 2>/dev/null; then + systemctl reload nginx 2>/dev/null || true + log "Nginx 已重载" +fi + +# ===== 4. 删除 SSL 证书 ===== +if [ -n "$PORTAINER_DOMAIN" ] && [ -d "/etc/letsencrypt/live/${PORTAINER_DOMAIN}" ]; then + log "正在删除 SSL 证书: ${PORTAINER_DOMAIN}..." + certbot delete --cert-name "${PORTAINER_DOMAIN}" --non-interactive 2>/dev/null || true + log "SSL 证书已删除" +fi + +# ===== 5. 清理 Certbot 定时任务 ===== +remaining_certs=$(certbot certificates 2>/dev/null | grep -c "Certificate Name" || true) +if [ "$remaining_certs" -eq 0 ]; then + crontab -l 2>/dev/null | grep -v "certbot renew" | crontab - 2>/dev/null || true + log "已移除 Certbot 自动续期定时任务(无剩余证书)" +else + log "保留 Certbot 定时任务(还有 ${remaining_certs} 个其他证书)" +fi + +# ===== 6. 删除数据目录 ===== +echo "" +echo -e "${RED}以下目录将被永久删除:${NC}" +echo " ${PORTAINER_DATA_DIR}" +echo "" +echo -e "${YELLOW}备份目录 ${BACKUP_DIR} 不会被删除。${NC}" +echo "" +read -r -p "确认删除数据目录?输入 DELETE 确认: " confirm_delete +if [ "$confirm_delete" = "DELETE" ]; then + rm -rf "$PORTAINER_DATA_DIR" + log "已删除: ${PORTAINER_DATA_DIR}" +else + warn "跳过数据目录删除" +fi + +# ===== 7. 删除部署目录 ===== +echo "" +read -r -p "是否删除部署目录 ${SCRIPT_DIR}?(y/N): " del_deploy +if [[ "$del_deploy" =~ ^[Yy]$ ]]; then + cd /opt + rm -rf "$SCRIPT_DIR" + log "已删除部署目录" +else + warn "保留部署目录: ${SCRIPT_DIR}" +fi + +# ===== 完成 ===== +echo "" +log "Portainer 卸载完成" +echo "" +echo "保留的内容:" +echo " 备份目录: ${BACKUP_DIR}" +[ "$confirm_delete" != "DELETE" ] && echo " 数据目录: ${PORTAINER_DATA_DIR}" +[[ ! "$del_deploy" =~ ^[Yy]$ ]] && echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo "如需恢复,请参考 README.md 中的「恢复备份」章节。" diff --git a/siyuan/README.md b/siyuan/README.md index 563e3cb..2808624 100644 --- a/siyuan/README.md +++ b/siyuan/README.md @@ -42,6 +42,7 @@ siyuan/ ├── .env.example # 配置模板 ├── deploy.sh # 一键部署脚本 ├── backup.sh # 备份脚本 +├── uninstall.sh # 完全卸载脚本 ├── nginx/ │ └── siyuan.conf # Nginx 反向代理配置 └── README.md # 本文件 @@ -260,6 +261,65 @@ docker compose up -d # 启动 docker compose restart # 重启 ``` +## 完全卸载 + +如果需要从服务器上完全移除 SiYuan,使用卸载脚本: + +```bash +cd /opt/siyuan +bash uninstall.sh +``` + +脚本会**交互式确认**每个危险操作,按顺序执行: + +| 步骤 | 操作 | 确认方式 | +|------|------|----------| +| 0 | 卸载前备份(可选) | y/N | +| 1 | 停止并删除 SiYuan 容器 | 输入 YES | +| 2 | 删除 Docker 镜像 | 自动 | +| 3 | 删除 Nginx 站点配置并重载 | 自动 | +| 4 | 删除 Let's Encrypt SSL 证书 | 自动 | +| 5 | 清理 Certbot 定时任务(仅当无其他证书时) | 自动 | +| 6 | 删除数据目录 | 输入 DELETE | +| 7 | 删除部署目录 `/opt/siyuan` | y/N | + +**备份目录 `/var/backups/siyuan/` 始终保留**,不会被删除。 + +> 卸载后如需恢复,参考上方「恢复备份」章节。 + +
+手动卸载步骤(不使用脚本) + +```bash +cd /opt/siyuan + +# 1. 建议先备份 +bash backup.sh + +# 2. 停止并删除容器 +docker compose down -v + +# 3. 删除 Docker 镜像(可选) +docker image rm b3log/siyuan:latest + +# 4. 删除 Nginx 配置 +rm -f /etc/nginx/sites-enabled/siyuan /etc/nginx/sites-available/siyuan +nginx -t && systemctl reload nginx + +# 5. 删除 SSL 证书 +certbot delete --cert-name 你的域名 + +# 6. 删除数据目录(⚠ 不可恢复) +rm -rf /data/siyuan/workspace + +# 7. 删除部署目录(可选) +rm -rf /opt/siyuan + +# 备份目录保留在 /var/backups/siyuan/ +``` + +
+ ## 数据存储结构 SiYuan 的数据存储在工作空间目录下: diff --git a/siyuan/backup.sh b/siyuan/backup.sh index 8fb9349..0b9eb58 100755 --- a/siyuan/backup.sh +++ b/siyuan/backup.sh @@ -11,6 +11,7 @@ cd "$SCRIPT_DIR" # 加载配置 if [ -f .env ]; then + sed -i 's/\r$//' .env set -a; source .env; set +a else echo "[ERROR] .env 文件不存在,请先运行 deploy.sh" >&2 diff --git a/siyuan/deploy.sh b/siyuan/deploy.sh index a19a169..7d42875 100755 --- a/siyuan/deploy.sh +++ b/siyuan/deploy.sh @@ -48,6 +48,7 @@ init_env() { exit 0 fi + fix_crlf .env set -a; source .env; set +a local has_error=0 diff --git a/siyuan/nginx/siyuan.conf b/siyuan/nginx/siyuan.conf index fd2b8af..64ae715 100644 --- a/siyuan/nginx/siyuan.conf +++ b/siyuan/nginx/siyuan.conf @@ -13,8 +13,9 @@ server { } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl; + listen [::]:443 ssl; + http2 on; server_name __DOMAIN__; # SSL 证书 @@ -23,16 +24,15 @@ server { # SSL 参数 ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header Referrer-Policy strict-origin-when-cross-origin always; # 上传大小限制(资源文件、图片等上传) client_max_body_size 128M; diff --git a/siyuan/uninstall.sh b/siyuan/uninstall.sh new file mode 100755 index 0000000..c96a084 --- /dev/null +++ b/siyuan/uninstall.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================ +# SiYuan 卸载脚本 +# 停止容器 → 备份数据 → 清理容器/镜像/配置/数据 +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# ===== 检查 root ===== +if [ "$(id -u)" -ne 0 ]; then + error "请使用 root 用户运行: sudo bash uninstall.sh" + exit 1 +fi + +# ===== 加载配置 ===== +if [ -f .env ]; then + sed -i 's/\r$//' .env + set -a + source .env + set +a +fi + +SIYUAN_DATA_DIR="${SIYUAN_DATA_DIR:-/data/siyuan/workspace}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/siyuan}" +SIYUAN_DOMAIN="${SIYUAN_DOMAIN:-}" + +# ===== 确认操作 ===== +echo "" +echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}" +echo -e "${RED}║ ⚠ 即将卸载 SiYuan 及所有数据 ⚠ ║${NC}" +echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}" +echo "" +echo "将执行以下操作:" +echo " 1. 停止并删除 SiYuan 容器" +echo " 2. 删除 Docker 镜像" +echo " 3. 删除 Nginx 站点配置" +echo " 4. 删除 SSL 证书" +echo " 5. 删除 Certbot 自动续期定时任务" +echo "" +echo "涉及的数据目录:" +echo " SiYuan 数据: ${SIYUAN_DATA_DIR}" +echo " 备份目录: ${BACKUP_DIR}" +echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo -e "${YELLOW}备份目录将保留,不会被删除。${NC}" +echo "" +read -r -p "确定要继续卸载吗?输入 YES 确认: " confirm +if [ "$confirm" != "YES" ]; then + log "已取消卸载" + exit 0 +fi + +# ===== 卸载前备份 ===== +echo "" +read -r -p "是否在卸载前执行一次备份?(y/N): " do_backup +if [[ "$do_backup" =~ ^[Yy]$ ]]; then + if [ -f backup.sh ]; then + log "正在执行备份..." + bash backup.sh + log "备份完成" + else + warn "backup.sh 不存在,跳过备份" + fi +fi + +# ===== 1. 停止并删除容器 ===== +echo "" +log "正在停止并删除容器..." +if docker compose ps --quiet 2>/dev/null | grep -q .; then + docker compose down -v + log "容器已停止并删除" +else + log "没有运行中的容器" +fi + +# ===== 2. 删除 Docker 镜像 ===== +log "正在删除 Docker 镜像..." +SIYUAN_IMAGE="${SIYUAN_IMAGE:-b3log/siyuan:latest}" +docker image rm "$SIYUAN_IMAGE" 2>/dev/null && log "已删除镜像: $SIYUAN_IMAGE" || true + +# ===== 3. 删除 Nginx 配置 ===== +log "正在清理 Nginx 配置..." +rm -f /etc/nginx/sites-enabled/siyuan +rm -f /etc/nginx/sites-available/siyuan +if command -v nginx &>/dev/null && nginx -t 2>/dev/null; then + systemctl reload nginx 2>/dev/null || true + log "Nginx 已重载" +fi + +# ===== 4. 删除 SSL 证书 ===== +if [ -n "$SIYUAN_DOMAIN" ] && [ -d "/etc/letsencrypt/live/${SIYUAN_DOMAIN}" ]; then + log "正在删除 SSL 证书: ${SIYUAN_DOMAIN}..." + certbot delete --cert-name "${SIYUAN_DOMAIN}" --non-interactive 2>/dev/null || true + log "SSL 证书已删除" +fi + +# ===== 5. 清理 Certbot 定时任务 ===== +remaining_certs=$(certbot certificates 2>/dev/null | grep -c "Certificate Name" || true) +if [ "$remaining_certs" -eq 0 ]; then + crontab -l 2>/dev/null | grep -v "certbot renew" | crontab - 2>/dev/null || true + log "已移除 Certbot 自动续期定时任务(无剩余证书)" +else + log "保留 Certbot 定时任务(还有 ${remaining_certs} 个其他证书)" +fi + +# ===== 6. 删除数据目录 ===== +echo "" +echo -e "${RED}以下目录将被永久删除:${NC}" +echo " ${SIYUAN_DATA_DIR}" +echo "" +echo -e "${YELLOW}备份目录 ${BACKUP_DIR} 不会被删除。${NC}" +echo "" +read -r -p "确认删除数据目录?输入 DELETE 确认: " confirm_delete +if [ "$confirm_delete" = "DELETE" ]; then + rm -rf "$SIYUAN_DATA_DIR" + log "已删除: ${SIYUAN_DATA_DIR}" +else + warn "跳过数据目录删除" +fi + +# ===== 7. 删除部署目录 ===== +echo "" +read -r -p "是否删除部署目录 ${SCRIPT_DIR}?(y/N): " del_deploy +if [[ "$del_deploy" =~ ^[Yy]$ ]]; then + cd /opt + rm -rf "$SCRIPT_DIR" + log "已删除部署目录" +else + warn "保留部署目录: ${SCRIPT_DIR}" +fi + +# ===== 完成 ===== +echo "" +log "SiYuan 卸载完成" +echo "" +echo "保留的内容:" +echo " 备份目录: ${BACKUP_DIR}" +[ "$confirm_delete" != "DELETE" ] && echo " 数据目录: ${SIYUAN_DATA_DIR}" +[[ ! "$del_deploy" =~ ^[Yy]$ ]] && echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo "如需恢复,请参考 README.md 中的「恢复备份」章节。" diff --git a/vaultwarden/README.md b/vaultwarden/README.md index 66f1372..603fb90 100644 --- a/vaultwarden/README.md +++ b/vaultwarden/README.md @@ -36,6 +36,7 @@ vaultwarden/ ├── .env.example # 配置模板 ├── deploy.sh # 一键部署脚本 ├── backup.sh # 备份脚本 +├── uninstall.sh # 完全卸载脚本 ├── nginx/ │ └── vaultwarden.conf # Nginx 反向代理配置 └── README.md # 本文件 @@ -249,6 +250,65 @@ docker compose up -d # 启动 docker compose restart # 重启 ``` +## 完全卸载 + +如果需要从服务器上完全移除 Vaultwarden,使用卸载脚本: + +```bash +cd /opt/vaultwarden +bash uninstall.sh +``` + +脚本会**交互式确认**每个危险操作,按顺序执行: + +| 步骤 | 操作 | 确认方式 | +|------|------|----------| +| 0 | 卸载前备份(可选) | y/N | +| 1 | 停止并删除 Vaultwarden 容器 | 输入 YES | +| 2 | 删除 Docker 镜像 | 自动 | +| 3 | 删除 Nginx 站点配置并重载 | 自动 | +| 4 | 删除 Let's Encrypt SSL 证书 | 自动 | +| 5 | 清理 Certbot 定时任务(仅当无其他证书时) | 自动 | +| 6 | 删除数据目录 | 输入 DELETE | +| 7 | 删除部署目录 `/opt/vaultwarden` | y/N | + +**备份目录 `/var/backups/vaultwarden/` 始终保留**,不会被删除。 + +> 卸载后如需恢复,参考上方「恢复备份」章节。 + +
+手动卸载步骤(不使用脚本) + +```bash +cd /opt/vaultwarden + +# 1. 建议先备份 +bash backup.sh + +# 2. 停止并删除容器 +docker compose down -v + +# 3. 删除 Docker 镜像(可选) +docker image rm vaultwarden/server:latest + +# 4. 删除 Nginx 配置 +rm -f /etc/nginx/sites-enabled/vaultwarden /etc/nginx/sites-available/vaultwarden +nginx -t && systemctl reload nginx + +# 5. 删除 SSL 证书 +certbot delete --cert-name 你的域名 + +# 6. 删除数据目录(⚠ 不可恢复) +rm -rf /var/lib/vaultwarden + +# 7. 删除部署目录(可选) +rm -rf /opt/vaultwarden + +# 备份目录保留在 /var/backups/vaultwarden/ +``` + +
+ ## 安全加固建议 ### 1. 关闭注册 diff --git a/vaultwarden/backup.sh b/vaultwarden/backup.sh index df9bcc2..20fc5ac 100755 --- a/vaultwarden/backup.sh +++ b/vaultwarden/backup.sh @@ -11,6 +11,7 @@ cd "$SCRIPT_DIR" # 加载配置 if [ -f .env ]; then + sed -i 's/\r$//' .env set -a; source .env; set +a else echo "[ERROR] .env 文件不存在,请先运行 deploy.sh" >&2 diff --git a/vaultwarden/deploy.sh b/vaultwarden/deploy.sh index 28e90a0..0e6917b 100755 --- a/vaultwarden/deploy.sh +++ b/vaultwarden/deploy.sh @@ -61,6 +61,7 @@ init_env() { exit 0 fi + fix_crlf .env set -a; source .env; set +a local has_error=0 diff --git a/vaultwarden/nginx/vaultwarden.conf b/vaultwarden/nginx/vaultwarden.conf index 64962f3..5cbad02 100644 --- a/vaultwarden/nginx/vaultwarden.conf +++ b/vaultwarden/nginx/vaultwarden.conf @@ -21,8 +21,9 @@ server { } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl; + listen [::]:443 ssl; + http2 on; server_name __DOMAIN__; # SSL 证书 @@ -31,17 +32,15 @@ server { # SSL 参数 ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - add_header Referrer-Policy "same-origin" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + add_header Referrer-Policy strict-origin-when-cross-origin always; # 上传大小限制(附件上传) client_max_body_size 525M; diff --git a/vaultwarden/uninstall.sh b/vaultwarden/uninstall.sh new file mode 100755 index 0000000..98d332c --- /dev/null +++ b/vaultwarden/uninstall.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================ +# Vaultwarden 卸载脚本 +# 停止容器 → 备份数据 → 清理容器/镜像/配置/数据 +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# ===== 检查 root ===== +if [ "$(id -u)" -ne 0 ]; then + error "请使用 root 用户运行: sudo bash uninstall.sh" + exit 1 +fi + +# ===== 加载配置 ===== +if [ -f .env ]; then + sed -i 's/\r$//' .env + set -a + source .env + set +a +fi + +VAULTWARDEN_DATA_DIR="${VAULTWARDEN_DATA_DIR:-/var/lib/vaultwarden}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/vaultwarden}" +VAULTWARDEN_DOMAIN="${VAULTWARDEN_DOMAIN:-}" + +# ===== 确认操作 ===== +echo "" +echo -e "${RED}╔══════════════════════════════════════════════════════╗${NC}" +echo -e "${RED}║ ⚠ 即将卸载 Vaultwarden 及所有数据 ⚠ ║${NC}" +echo -e "${RED}╚══════════════════════════════════════════════════════╝${NC}" +echo "" +echo "将执行以下操作:" +echo " 1. 停止并删除 Vaultwarden 容器" +echo " 2. 删除 Docker 镜像" +echo " 3. 删除 Nginx 站点配置" +echo " 4. 删除 SSL 证书" +echo " 5. 删除 Certbot 自动续期定时任务" +echo "" +echo "涉及的数据目录:" +echo " Vaultwarden 数据: ${VAULTWARDEN_DATA_DIR}" +echo " 备份目录: ${BACKUP_DIR}" +echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo -e "${YELLOW}备份目录将保留,不会被删除。${NC}" +echo "" +read -r -p "确定要继续卸载吗?输入 YES 确认: " confirm +if [ "$confirm" != "YES" ]; then + log "已取消卸载" + exit 0 +fi + +# ===== 卸载前备份 ===== +echo "" +read -r -p "是否在卸载前执行一次备份?(y/N): " do_backup +if [[ "$do_backup" =~ ^[Yy]$ ]]; then + if [ -f backup.sh ]; then + log "正在执行备份..." + bash backup.sh + log "备份完成" + else + warn "backup.sh 不存在,跳过备份" + fi +fi + +# ===== 1. 停止并删除容器 ===== +echo "" +log "正在停止并删除容器..." +if docker compose ps --quiet 2>/dev/null | grep -q .; then + docker compose down -v + log "容器已停止并删除" +else + log "没有运行中的容器" +fi + +# ===== 2. 删除 Docker 镜像 ===== +log "正在删除 Docker 镜像..." +VAULTWARDEN_IMAGE="${VAULTWARDEN_IMAGE:-vaultwarden/server:latest}" +docker image rm "$VAULTWARDEN_IMAGE" 2>/dev/null && log "已删除镜像: $VAULTWARDEN_IMAGE" || true + +# ===== 3. 删除 Nginx 配置 ===== +log "正在清理 Nginx 配置..." +rm -f /etc/nginx/sites-enabled/vaultwarden +rm -f /etc/nginx/sites-available/vaultwarden +if command -v nginx &>/dev/null && nginx -t 2>/dev/null; then + systemctl reload nginx 2>/dev/null || true + log "Nginx 已重载" +fi + +# ===== 4. 删除 SSL 证书 ===== +if [ -n "$VAULTWARDEN_DOMAIN" ] && [ -d "/etc/letsencrypt/live/${VAULTWARDEN_DOMAIN}" ]; then + log "正在删除 SSL 证书: ${VAULTWARDEN_DOMAIN}..." + certbot delete --cert-name "${VAULTWARDEN_DOMAIN}" --non-interactive 2>/dev/null || true + log "SSL 证书已删除" +fi + +# ===== 5. 清理 Certbot 定时任务 ===== +remaining_certs=$(certbot certificates 2>/dev/null | grep -c "Certificate Name" || true) +if [ "$remaining_certs" -eq 0 ]; then + crontab -l 2>/dev/null | grep -v "certbot renew" | crontab - 2>/dev/null || true + log "已移除 Certbot 自动续期定时任务(无剩余证书)" +else + log "保留 Certbot 定时任务(还有 ${remaining_certs} 个其他证书)" +fi + +# ===== 6. 删除数据目录 ===== +echo "" +echo -e "${RED}以下目录将被永久删除:${NC}" +echo " ${VAULTWARDEN_DATA_DIR}" +echo "" +echo -e "${YELLOW}备份目录 ${BACKUP_DIR} 不会被删除。${NC}" +echo "" +read -r -p "确认删除数据目录?输入 DELETE 确认: " confirm_delete +if [ "$confirm_delete" = "DELETE" ]; then + rm -rf "$VAULTWARDEN_DATA_DIR" + log "已删除: ${VAULTWARDEN_DATA_DIR}" +else + warn "跳过数据目录删除" +fi + +# ===== 7. 删除部署目录 ===== +echo "" +read -r -p "是否删除部署目录 ${SCRIPT_DIR}?(y/N): " del_deploy +if [[ "$del_deploy" =~ ^[Yy]$ ]]; then + cd /opt + rm -rf "$SCRIPT_DIR" + log "已删除部署目录" +else + warn "保留部署目录: ${SCRIPT_DIR}" +fi + +# ===== 完成 ===== +echo "" +log "Vaultwarden 卸载完成" +echo "" +echo "保留的内容:" +echo " 备份目录: ${BACKUP_DIR}" +[ "$confirm_delete" != "DELETE" ] && echo " 数据目录: ${VAULTWARDEN_DATA_DIR}" +[[ ! "$del_deploy" =~ ^[Yy]$ ]] && echo " 部署目录: ${SCRIPT_DIR}" +echo "" +echo "如需恢复,请参考 README.md 中的「恢复备份」章节。"