484 lines
16 KiB
Bash
Executable File
484 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# ============================================
|
||
# Gitea 全新服务器一键部署脚本
|
||
# 适用于 Ubuntu 20.04+ / Debian 11+
|
||
# 包含:系统更新 → Docker → Nginx → Certbot → Gitea + MySQL
|
||
# ============================================
|
||
|
||
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}"; }
|
||
|
||
# ===== 检查 root 权限 =====
|
||
check_root() {
|
||
if [ "$(id -u)" -ne 0 ]; then
|
||
error "请使用 root 用户运行此脚本: sudo bash deploy.sh"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ===== 1. 系统初始化 =====
|
||
init_system() {
|
||
step "1/8 系统初始化"
|
||
|
||
# 检测包管理器
|
||
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 "不支持的系统,需要 apt/dnf/yum 包管理器"
|
||
exit 1
|
||
fi
|
||
|
||
log "检测到包管理器: $PKG_MGR"
|
||
log "正在更新系统软件包..."
|
||
|
||
case "$PKG_MGR" in
|
||
apt)
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -qq
|
||
apt-get upgrade -y -qq
|
||
apt-get install -y -qq curl wget git gnupg2 ca-certificates \
|
||
lsb-release software-properties-common openssl cron
|
||
;;
|
||
dnf|yum)
|
||
$PKG_MGR update -y -q
|
||
$PKG_MGR install -y -q curl wget git gnupg2 ca-certificates openssl cronie
|
||
;;
|
||
esac
|
||
|
||
# 设置时区
|
||
if [ ! -f /etc/timezone ] || [ "$(cat /etc/timezone 2>/dev/null)" != "Asia/Shanghai" ]; then
|
||
timedatectl set-timezone Asia/Shanghai 2>/dev/null || true
|
||
log "时区已设置为 Asia/Shanghai"
|
||
fi
|
||
|
||
log "系统初始化完成"
|
||
}
|
||
|
||
# ===== 配置 Docker 镜像加速 =====
|
||
configure_docker_mirrors() {
|
||
local mirrors="${DOCKER_REGISTRY_MIRRORS:-}"
|
||
if [ -z "$mirrors" ]; then
|
||
log "未配置 DOCKER_REGISTRY_MIRRORS,跳过镜像加速"
|
||
return
|
||
fi
|
||
|
||
mkdir -p /etc/docker
|
||
|
||
# 将逗号分隔的镜像列表转为 JSON 数组
|
||
local json_array
|
||
json_array=$(echo "$mirrors" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | awk '{printf "\"%s\",", $0}' | sed 's/,$//')
|
||
|
||
local need_restart=0
|
||
|
||
if [ -f /etc/docker/daemon.json ]; then
|
||
# 已有配置文件,检查是否需要更新
|
||
if ! grep -q "registry-mirrors" /etc/docker/daemon.json; then
|
||
need_restart=1
|
||
fi
|
||
else
|
||
need_restart=1
|
||
fi
|
||
|
||
if [ "$need_restart" -eq 1 ]; then
|
||
# 生成或覆盖 daemon.json(保留 log 配置)
|
||
cat > /etc/docker/daemon.json <<EOF
|
||
{
|
||
"registry-mirrors": [${json_array}],
|
||
"log-driver": "json-file",
|
||
"log-opts": {
|
||
"max-size": "10m",
|
||
"max-file": "3"
|
||
}
|
||
}
|
||
EOF
|
||
log "Docker 镜像加速已配置:"
|
||
echo "$mirrors" | tr ',' '\n' | sed 's/^/ → /'
|
||
# 重启 Docker 使配置生效
|
||
systemctl restart docker
|
||
log "Docker 已重启以应用镜像加速"
|
||
else
|
||
log "Docker 镜像加速已存在,跳过"
|
||
fi
|
||
}
|
||
|
||
# ===== 2. 安装 Docker =====
|
||
install_docker() {
|
||
step "2/8 安装 Docker"
|
||
|
||
if command -v docker &> /dev/null; then
|
||
log "Docker 已安装: $(docker --version)"
|
||
else
|
||
log "正在安装 Docker (使用阿里云镜像)..."
|
||
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" \
|
||
> /etc/apt/sources.list.d/docker.list
|
||
apt-get update -qq
|
||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||
log "Docker 安装完成: $(docker --version)"
|
||
fi
|
||
|
||
# 确保 Docker 服务运行
|
||
systemctl enable --now docker
|
||
log "Docker 服务已启动"
|
||
|
||
# 检查 Docker Compose V2
|
||
if ! docker compose version &> /dev/null; then
|
||
error "Docker Compose V2 不可用"
|
||
error "请手动安装: apt install docker-compose-plugin"
|
||
exit 1
|
||
fi
|
||
log "Docker Compose 已就绪: $(docker compose version --short)"
|
||
}
|
||
|
||
# ===== 3. 安装 Nginx =====
|
||
install_nginx() {
|
||
step "3/8 安装 Nginx"
|
||
|
||
if command -v nginx &> /dev/null; then
|
||
log "Nginx 已安装: $(nginx -v 2>&1)"
|
||
else
|
||
log "正在安装 Nginx..."
|
||
case "$PKG_MGR" in
|
||
apt)
|
||
apt-get install -y -qq nginx
|
||
;;
|
||
dnf|yum)
|
||
$PKG_MGR install -y -q nginx
|
||
;;
|
||
esac
|
||
log "Nginx 安装完成"
|
||
fi
|
||
|
||
systemctl enable --now nginx
|
||
|
||
# 确保 Nginx 配置目录结构存在
|
||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /var/www/certbot
|
||
|
||
# 确保 nginx.conf 包含 sites-enabled
|
||
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
|
||
fi
|
||
|
||
log "Nginx 配置就绪"
|
||
}
|
||
|
||
# ===== 4. 初始化 .env =====
|
||
init_env() {
|
||
step "4/8 初始化配置"
|
||
|
||
if [ ! -f .env ]; then
|
||
if [ ! -f .env.example ]; then
|
||
error "缺少 .env.example 模板文件"
|
||
exit 1
|
||
fi
|
||
cp .env.example .env
|
||
|
||
# 自动生成随机密码
|
||
DB_PASS=$(openssl rand -base64 24 | tr -d '/+=\n' | head -c 32)
|
||
DB_ROOT_PASS=$(openssl rand -base64 24 | tr -d '/+=\n' | head -c 32)
|
||
|
||
sed -i "s/请替换为强密码/${DB_PASS}/" .env
|
||
sed -i "s/请替换为ROOT强密码/${DB_ROOT_PASS}/" .env
|
||
|
||
log "已生成 .env 文件,数据库密码已随机生成"
|
||
echo ""
|
||
warn "┌─────────────────────────────────────────────┐"
|
||
warn "│ 请编辑 .env 文件,至少修改以下配置: │"
|
||
warn "│ │"
|
||
warn "│ GITEA_DOMAIN=你的域名 │"
|
||
warn "│ CERTBOT_EMAIL=你的邮箱 │"
|
||
warn "│ │"
|
||
warn "│ 编辑命令: vi $SCRIPT_DIR/.env │"
|
||
warn "│ 编辑完成后重新运行: bash deploy.sh │"
|
||
warn "└─────────────────────────────────────────────┘"
|
||
exit 0
|
||
fi
|
||
|
||
# 加载并验证配置
|
||
set -a
|
||
source .env
|
||
set +a
|
||
|
||
local has_error=0
|
||
|
||
if [[ -z "${GITEA_DOMAIN:-}" ]] || [[ "${GITEA_DOMAIN}" == "git.example.com" ]]; then
|
||
error "请在 .env 中将 GITEA_DOMAIN 修改为你的实际域名"
|
||
has_error=1
|
||
fi
|
||
if [[ -z "${CERTBOT_EMAIL:-}" ]] || [[ "${CERTBOT_EMAIL}" == "admin@example.com" ]]; then
|
||
error "请在 .env 中将 CERTBOT_EMAIL 修改为你的实际邮箱"
|
||
has_error=1
|
||
fi
|
||
if [[ "${DB_PASSWORD:-}" == *"请替换"* ]] || [[ -z "${DB_PASSWORD:-}" ]]; then
|
||
error "DB_PASSWORD 未正确设置"
|
||
has_error=1
|
||
fi
|
||
|
||
if [ "$has_error" -eq 1 ]; then
|
||
error "请修改 .env 后重新运行: vi $SCRIPT_DIR/.env"
|
||
exit 1
|
||
fi
|
||
|
||
log "配置检查通过: 域名=${GITEA_DOMAIN}"
|
||
}
|
||
|
||
# ===== 5. 创建数据目录 =====
|
||
create_dirs() {
|
||
step "5/8 创建数据目录"
|
||
|
||
local gitea_data="${GITEA_DATA_DIR:-/var/lib/gitea}"
|
||
local mysql_data="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}"
|
||
local backup_dir="${BACKUP_DIR:-/var/backups/gitea}"
|
||
|
||
mkdir -p "$gitea_data" "$mysql_data" "$backup_dir"
|
||
|
||
# Gitea 容器以 UID=1000 运行,确保数据目录归属正确
|
||
chown -R 1000:1000 "$gitea_data"
|
||
# MySQL 容器以 mysql(999) 运行
|
||
chown -R 999:999 "$mysql_data"
|
||
|
||
log "数据目录已就绪:"
|
||
log " Gitea 数据: $gitea_data"
|
||
log " MySQL 数据: $mysql_data"
|
||
log " 备份目录: $backup_dir"
|
||
}
|
||
|
||
# ===== 6. 配置防火墙 =====
|
||
setup_firewall() {
|
||
step "6/8 配置防火墙"
|
||
|
||
local ssh_port="${SSH_PORT:-2222}"
|
||
|
||
if command -v ufw &> /dev/null; then
|
||
ufw --force enable 2>/dev/null || true
|
||
ufw allow ssh comment "SSH" 2>/dev/null || true
|
||
ufw allow 80/tcp comment "HTTP" 2>/dev/null || true
|
||
ufw allow 443/tcp comment "HTTPS" 2>/dev/null || true
|
||
ufw allow "$ssh_port"/tcp comment "Gitea SSH" 2>/dev/null || true
|
||
ufw reload 2>/dev/null || true
|
||
log "UFW 防火墙规则已添加 (22, 80, 443, $ssh_port)"
|
||
elif command -v firewall-cmd &> /dev/null; then
|
||
systemctl enable --now firewalld 2>/dev/null || true
|
||
firewall-cmd --permanent --add-service=ssh 2>/dev/null || true
|
||
firewall-cmd --permanent --add-service=http 2>/dev/null || true
|
||
firewall-cmd --permanent --add-service=https 2>/dev/null || true
|
||
firewall-cmd --permanent --add-port="$ssh_port"/tcp 2>/dev/null || true
|
||
firewall-cmd --reload 2>/dev/null || true
|
||
log "Firewalld 规则已添加 (22, 80, 443, $ssh_port)"
|
||
else
|
||
warn "未检测到防火墙工具,请手动开放端口: 22, 80, 443, $ssh_port"
|
||
fi
|
||
}
|
||
|
||
# ===== 7. 配置 Nginx + SSL =====
|
||
setup_ssl() {
|
||
step "7/8 配置 HTTPS (Nginx + Let's Encrypt)"
|
||
|
||
local domain="${GITEA_DOMAIN}"
|
||
local email="${CERTBOT_EMAIL}"
|
||
|
||
# 安装 certbot
|
||
if ! command -v certbot &> /dev/null; then
|
||
log "正在安装 Certbot..."
|
||
case "$PKG_MGR" in
|
||
apt)
|
||
apt-get install -y -qq certbot python3-certbot-nginx
|
||
;;
|
||
dnf)
|
||
dnf install -y -q certbot python3-certbot-nginx
|
||
;;
|
||
yum)
|
||
yum install -y -q certbot python3-certbot-nginx
|
||
;;
|
||
esac
|
||
fi
|
||
log "Certbot 已就绪"
|
||
|
||
# 部署临时 Nginx 配置(仅 HTTP,用于证书验证)
|
||
cat > /etc/nginx/sites-available/gitea <<NGINX_TEMP
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name ${domain};
|
||
|
||
location /.well-known/acme-challenge/ {
|
||
root /var/www/certbot;
|
||
}
|
||
|
||
location / {
|
||
return 200 'Gitea is being configured...';
|
||
add_header Content-Type text/plain;
|
||
}
|
||
}
|
||
NGINX_TEMP
|
||
|
||
ln -sf /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/gitea
|
||
# 移除默认站点避免冲突
|
||
rm -f /etc/nginx/sites-enabled/default
|
||
nginx -t && systemctl reload nginx
|
||
|
||
# 申请 SSL 证书
|
||
if [ ! -d "/etc/letsencrypt/live/${domain}" ]; then
|
||
log "正在申请 SSL 证书: ${domain} ..."
|
||
if ! certbot certonly --webroot \
|
||
-w /var/www/certbot \
|
||
-d "${domain}" \
|
||
--email "${email}" \
|
||
--agree-tos \
|
||
--non-interactive \
|
||
--no-eff-email; then
|
||
error "SSL 证书申请失败!"
|
||
error "请确认:"
|
||
error " 1. 域名 ${domain} 已解析到本服务器 IP"
|
||
error " 2. 服务器 80 端口可从外网访问"
|
||
error "解决后重新运行 deploy.sh"
|
||
exit 1
|
||
fi
|
||
log "SSL 证书申请成功"
|
||
else
|
||
log "SSL 证书已存在,跳过申请"
|
||
fi
|
||
|
||
# 部署正式 Nginx 配置
|
||
cp "$SCRIPT_DIR/nginx/gitea.conf" /etc/nginx/sites-available/gitea
|
||
sed -i "s/__GITEA_DOMAIN__/${domain}/g" /etc/nginx/sites-available/gitea
|
||
|
||
nginx -t
|
||
systemctl reload nginx
|
||
log "Nginx HTTPS 反向代理配置完成"
|
||
|
||
# 设置证书自动续期
|
||
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
|
||
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
|
||
log "已配置 SSL 证书自动续期 (每天 03:00 检查)"
|
||
fi
|
||
}
|
||
|
||
# ===== 8. 启动服务 =====
|
||
start_services() {
|
||
step "8/8 启动 Gitea 服务"
|
||
|
||
log "正在拉取镜像(首次可能较慢)..."
|
||
docker compose pull
|
||
|
||
log "正在启动容器..."
|
||
docker compose up -d
|
||
|
||
log "等待 MySQL 就绪..."
|
||
local max_wait=60
|
||
for i in $(seq 1 $max_wait); do
|
||
if docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD}" --silent &> /dev/null; then
|
||
log "MySQL 已就绪"
|
||
break
|
||
fi
|
||
if [ "$i" -eq "$max_wait" ]; then
|
||
warn "MySQL 启动超时,请检查日志: docker compose logs db"
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
log "等待 Gitea 就绪..."
|
||
for i in $(seq 1 30); do
|
||
if curl -sf http://127.0.0.1:3000/api/v1/version &> /dev/null; then
|
||
log "Gitea 启动成功!"
|
||
break
|
||
fi
|
||
if [ "$i" -eq 30 ]; then
|
||
warn "Gitea 可能仍在初始化,请稍后检查"
|
||
fi
|
||
sleep 2
|
||
done
|
||
}
|
||
|
||
# ===== 输出完成信息 =====
|
||
show_info() {
|
||
set -a
|
||
source .env
|
||
set +a
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ Gitea 部署完成! ║${NC}"
|
||
echo -e "${GREEN}╠══════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} Web 访问: ${CYAN}https://${GITEA_DOMAIN}${NC}"
|
||
echo -e "${GREEN}║${NC} SSH 克隆: ${CYAN}ssh://git@${GITEA_DOMAIN}:${SSH_PORT:-2222}/用户名/仓库.git${NC}"
|
||
echo -e "${GREEN}║${NC} HTTPS 克隆: ${CYAN}https://${GITEA_DOMAIN}/用户名/仓库.git${NC}"
|
||
echo -e "${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} 已启用功能:"
|
||
echo -e "${GREEN}║${NC} ✓ HTTPS (Let's Encrypt 自动续期)"
|
||
echo -e "${GREEN}║${NC} ✓ Git LFS 大文件存储"
|
||
echo -e "${GREEN}║${NC} ✓ SSH 密钥认证"
|
||
echo -e "${GREEN}║${NC} ✓ GPG 签名验证"
|
||
echo -e "${GREEN}║${NC} ✓ MySQL 8.4 LTS 数据库"
|
||
echo -e "${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} 首次访问说明:"
|
||
echo -e "${GREEN}║${NC} 1. 浏览器打开 https://${GITEA_DOMAIN}"
|
||
echo -e "${GREEN}║${NC} 2. 进入安装向导(数据库配置已自动填写)"
|
||
echo -e "${GREEN}║${NC} 3. 创建管理员账户"
|
||
echo -e "${GREEN}║${NC} 4. 完成安装"
|
||
echo -e "${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} 常用命令:"
|
||
echo -e "${GREEN}║${NC} 查看日志: cd ${INSTALL_DIR:-/opt/gitea} && docker compose logs -f"
|
||
echo -e "${GREEN}║${NC} 重启服务: cd ${INSTALL_DIR:-/opt/gitea} && docker compose restart"
|
||
echo -e "${GREEN}║${NC} 备份数据: bash ${INSTALL_DIR:-/opt/gitea}/backup.sh"
|
||
echo -e "${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} 服务器目录:"
|
||
echo -e "${GREEN}║${NC} 部署文件: ${INSTALL_DIR:-/opt/gitea}"
|
||
echo -e "${GREEN}║${NC} Gitea 数据: ${GITEA_DATA_DIR:-/var/lib/gitea}"
|
||
echo -e "${GREEN}║${NC} MySQL 数据: ${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}"
|
||
echo -e "${GREEN}║${NC} 备份目录: ${BACKUP_DIR:-/var/backups/gitea}"
|
||
echo -e "${GREEN}║${NC} SSL 证书: /etc/letsencrypt/live/${GITEA_DOMAIN}"
|
||
echo -e "${GREEN}║${NC} Nginx 配置: /etc/nginx/sites-available/gitea"
|
||
echo -e "${GREEN}║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
warn "重要:创建管理员后请编辑 .env 将 DISABLE_REGISTRATION 改为 true 并重启"
|
||
}
|
||
|
||
# ===== 主流程 =====
|
||
main() {
|
||
echo -e "${CYAN}"
|
||
echo " ____ _ _"
|
||
echo " / ___|(_) |_ ___ __ _"
|
||
echo "| | _ | | __/ _ \\/ _\` |"
|
||
echo "| |_| || | || __/ (_| |"
|
||
echo " \\____|_|\\__\\___|\\__,_| Deploy Script"
|
||
echo -e "${NC}"
|
||
echo ""
|
||
|
||
check_root
|
||
init_system
|
||
install_docker
|
||
install_nginx
|
||
init_env
|
||
configure_docker_mirrors
|
||
create_dirs
|
||
setup_firewall
|
||
setup_ssl
|
||
start_services
|
||
show_info
|
||
|
||
log "===== 全部部署完成 ====="
|
||
}
|
||
|
||
main "$@"
|