feat(val-blog): add 2026-04-30 dream journey post

This commit is contained in:
Chen Gu
2026-08-13 17:00:22 +08:00
committed by Chen Gu
parent 691f96e8a6
commit 81978704fc
941 changed files with 195468 additions and 755 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Deploy App (Gitea Actions)
on:
push:
branches: [ main, master ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup SSH key
shell: bash
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SERVER_HOST: ${{ secrets.SERVER_HOST }}
SERVER_PORT: ${{ secrets.SERVER_PORT }}
SERVER_USER: ${{ secrets.SERVER_USER }}
run: |
set -euo pipefail
mkdir -p ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "$SERVER_PORT" -H "$SERVER_HOST" >> ~/.ssh/known_hosts
- name: Rsync project to server
shell: bash
env:
SERVER_HOST: ${{ secrets.SERVER_HOST }}
SERVER_PORT: ${{ secrets.SERVER_PORT }}
SERVER_USER: ${{ secrets.SERVER_USER }}
run: |
set -euo pipefail
rsync -az --delete \
--exclude '.git' \
-e "ssh -p $SERVER_PORT -i ~/.ssh/id_ed25519" \
./ "$SERVER_USER@$SERVER_HOST:~/gch3n-infra/"
- name: Remote deploy
shell: bash
env:
SERVER_HOST: ${{ secrets.SERVER_HOST }}
SERVER_PORT: ${{ secrets.SERVER_PORT }}
SERVER_USER: ${{ secrets.SERVER_USER }}
run: |
set -euo pipefail
ssh -p "$SERVER_PORT" -i ~/.ssh/id_ed25519 "$SERVER_USER@$SERVER_HOST" << 'EOF'
set -euo pipefail
cd ~/gch3n-infra
chmod +x deploy.sh
./deploy.sh
curl -fsS http://127.0.0.1:3001/health || curl -fsS http://127.0.0.1:3002/health
EOF
+46
View File
@@ -0,0 +1,46 @@
# gch3n.online - Caddy 配置文件
# 自动 HTTPS,无需手动申请证书
# Git 服务器
git.gch3n.online {
reverse_proxy localhost:8080
# 日志
log {
output file /var/log/caddy/git.gch3n.online.log
}
}
# 应用部署(蓝绿)
app.gch3n.online {
# 负载均衡到 blue/green
reverse_proxy localhost:3001 localhost:3002 {
lb_policy round_robin
health_uri /health
health_interval 10s
health_timeout 5s
}
log {
output file /var/log/caddy/app.gch3n.online.log
}
}
# 主域名 - 静态展示页
gch3n.online {
# 静态文件目录
root * /var/www/gch3n
# 尝试文件
try_files {path} {path}/ /index.html
# 文件服务
file_server
# 压缩
encode gzip
log {
output file /var/log/caddy/gch3n.online.log
}
}
+105
View File
@@ -0,0 +1,105 @@
# Gitea 初始化指南
## 访问地址
- **Web:** http://49.235.172.252:8080
- **SSH:** `git@49.235.172.252:2222`
## 安装向导配置
打开 http://49.235.172.252:8080 后,填写以下配置:
### 数据库设置
- **数据库类型:** SQLite3(保持默认)
### 通用设置
- **站点标题:** gch3n Git
- **仓库根目录:** `/data/git/gitea-repositories`
- **Git 域名:** `49.235.172.252`
- **SSH 服务端口:** `2222`
- **Gitea 基础 URL:** `http://49.235.172.252:8080/`
- **日志路径:** `/data/gitea/log`
### 可选设置
- **启用发送邮件:** 暂时不勾选(后续可配置)
- **服务器和其他服务设置:** 保持默认
### 管理员账号设置
- **管理员用户名:** (你自己定)
- **密码:** (强密码)
- **电子邮箱:** (你的邮箱)
## 安装后配置
### 1. 创建第一个仓库
1. 点击右上角 + → 新建仓库
2. 仓库名称: `my-app`
3. 初始化仓库: 勾选 "添加 README"
### 2. 本地 Git 配置
```bash
# 配置 Git 用户信息(如果没配置过)
git config --global user.name "Your Name"
git config --global user.email "your@email.com"
# 生成 SSH 密钥(如果没有)
ssh-keygen -t ed25519 -C "your@email.com"
# 复制公钥到 Gitea
cat ~/.ssh/id_ed25519.pub
# 在 Gitea 网页: 用户设置 → SSH/GPG 密钥 → 添加密钥
# 克隆仓库
git clone ssh://git@49.235.172.252:2222/用户名/my-app.git
cd my-app
# 或者关联现有项目
git remote add origin ssh://git@49.235.172.252:2222/用户名/my-app.git
git push -u origin main
```
### 3. 启用 CI/CD (Gitea Actions)
1. 在 Gitea 管理后台 → 配置 → 启用 Actions
2. 在仓库设置 → Actions → 启用
3.`.gitea/workflows/deploy.yml` 复制到你的仓库 `.gitea/workflows/` 目录
### 4. 添加部署密钥到仓库 Secrets
在仓库 → 设置 → Secrets:
- `SSH_PRIVATE_KEY`: (之前生成的私钥内容)
- `SERVER_HOST`: `49.235.172.252`
- `SERVER_PORT`: `11022`
- `SERVER_USER`: `nplx`
## 使用蓝绿部署
```bash
# 在服务器上执行
ssh nplx@49.235.172.252 -p 11022
cd ~/gch3n-infra
./deploy.sh blue # 部署到 blue 环境
./deploy.sh green # 部署到 green 环境(自动切换流量)
```
## 下一步:配置 HTTPS
当前是 HTTP,需要配置 SSL 证书:
1. 清理占用 80 端口的服务
2. 申请 Let's Encrypt 证书
3. 配置 Nginx 反向代理 + HTTPS
## 文件位置
- Gitea 数据: `~/gch3n-infra/data/gitea/`
- Git 仓库: `~/gch3n-infra/data/gitea/git/`
- CI/CD 配置: `~/gch3n-infra/.gitea/workflows/deploy.yml`
- 部署脚本: `~/gch3n-infra/deploy.sh`
---
**遇到问题?** 查看 Gitea 日志:
```bash
docker logs gitea -f
```
+29
View File
@@ -0,0 +1,29 @@
# gch3n.online DevOps 基础设施
## 组件
- Gitea - 私有 Git 服务器
- Nginx - 反向代理 + HTTPS
- Docker Compose - 容器编排
- GitHub Actions / Gitea Actions - CI/CD
## 域名规划
- git.gch3n.online - Gitea Git 服务器
- app.gch3n.online - 应用部署(蓝绿部署)
## 部署架构
```
Developer -> Git Push -> Gitea Webhook -> CI/CD Pipeline
|
v
Build & Test
|
v
Blue/Green Deployment
/ \
app-blue:3001 app-green:3002
\ /
Nginx (Upstream)
|
v
app.gch3n.online
```
+39
View File
@@ -0,0 +1,39 @@
# 多阶段构建
FROM node:20-alpine AS builder
WORKDIR /app
# 复制依赖文件
COPY package*.json ./
RUN npm ci --only=production
# 生产阶段
FROM node:20-alpine
WORKDIR /app
# 安装必要工具(健康检查用)
RUN apk add --no-cache wget
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S app -u 1001
# 复制依赖
COPY --from=builder --chown=app:nodejs /app/node_modules ./node_modules
# 复制应用代码
COPY --chown=app:nodejs . .
# 切换到非 root 用户
USER app
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:${PORT:-3000}/health || exit 1
# 启动应用
CMD ["node", "server.js"]
+13
View File
@@ -0,0 +1,13 @@
{
"name": "gch3n-app",
"version": "1.0.0",
"description": "gch3n.online 示例应用",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js"
},
"keywords": [],
"author": "",
"license": "MIT"
}
+70
View File
@@ -0,0 +1,70 @@
const http = require('http');
const PORT = process.env.PORT || 3000;
const COLOR = process.env.DEPLOY_COLOR || 'unknown';
const VERSION = process.env.npm_package_version || '1.0.0';
const server = http.createServer((req, res) => {
const url = req.url;
// 健康检查端点
if (url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
color: COLOR,
version: VERSION,
timestamp: new Date().toISOString(),
uptime: process.uptime()
}));
return;
}
// 主页
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>gch3n.online</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
background: ${COLOR === 'blue' ? '#e3f2fd' : '#e8f5e9'};
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
background: ${COLOR === 'blue' ? '#1976d2' : '#388e3c'};
color: white;
}
h1 { color: #333; }
.info { color: #666; margin-top: 20px; }
</style>
</head>
<body>
<h1>🚀 gch3n.online</h1>
<span class="badge">${COLOR}</span>
<p>蓝绿部署示例应用</p>
<div class="info">
<p>版本: ${VERSION}</p>
<p>端口: ${PORT}</p>
<p>时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
</body>
</html>
`);
});
server.listen(PORT, () => {
console.log(`🚀 服务器运行在端口 ${PORT}`);
console.log(`🎨 部署颜色: ${COLOR}`);
console.log(`📦 版本: ${VERSION}`);
});
+167
View File
@@ -0,0 +1,167 @@
#!/bin/bash
set -e
# 蓝绿部署脚本(Caddy 版本)
# 用法: ./deploy.sh [blue|green]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 日志函数
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 自动发现 Docker 网络名(兼容 docker compose 前缀)
detect_network() {
local net
net=$(docker network ls --format "{{.Name}}" | grep -E '^(gch3n-infra_)?gch3n-network$' | head -1 || true)
if [ -z "$net" ]; then
# 回退到默认 bridge,避免脚本直接失败
net="bridge"
log_warn "未发现 gch3n-network,回退使用 bridge 网络"
fi
echo "$net"
}
# 获取当前活跃的环境
get_active_color() {
local active=$(docker ps --filter "name=app-" --format "{{.Names}}" | grep -E "app-(blue|green)" | head -1)
if [ -z "$active" ]; then
echo "none"
else
echo "$active" | sed 's/app-//'
fi
}
# 健康检查(主机侧)
check_health() {
local color=$1
local port=$2
local max_attempts=30
local attempt=0
log_info "检查 $color 健康状态 (127.0.0.1:$port)..."
while [ $attempt -lt $max_attempts ]; do
if curl -fsS "http://127.0.0.1:$port/health" >/dev/null 2>&1; then
log_info "$color 健康检查通过 ✓"
return 0
fi
attempt=$((attempt + 1))
log_warn "健康检查尝试 $attempt/$max_attempts 失败,等待 2 秒..."
sleep 2
done
log_error "$color 健康检查失败 ✗"
return 1
}
# 部署到指定环境
deploy_to() {
local target_color=$1
local target_port=$2
local other_color=$3
local network_name=$4
log_info "开始部署到 $target_color 环境..."
# 1. 构建新镜像
log_info "构建 Docker 镜像..."
docker build -t "gch3n/app:$target_color" -t "gch3n/app:latest" ./app
# 2. 停止目标环境(如果存在)
if docker ps -q --filter "name=app-$target_color" | grep -q .; then
log_warn "停止现有的 $target_color 容器..."
docker stop "app-$target_color" || true
docker rm "app-$target_color" || true
fi
# 3. 启动新容器
log_info "启动 $target_color 容器(network=$network_name..."
docker run -d \
--name "app-$target_color" \
--network "$network_name" \
-p "127.0.0.1:$target_port:$target_port" \
-e "PORT=$target_port" \
-e "DEPLOY_COLOR=$target_color" \
--health-cmd="wget --quiet --tries=1 --spider http://localhost:$target_port/health || exit 1" \
--health-interval=10s \
--health-timeout=5s \
--health-retries=3 \
--health-start-period=10s \
--restart=unless-stopped \
"gch3n/app:$target_color"
# 4. 健康检查
if ! check_health "$target_color" "$target_port"; then
log_error "$target_color 部署失败,执行回滚..."
docker stop "app-$target_color" || true
docker rm "app-$target_color" || true
if [ "$other_color" != "none" ]; then
log_warn "保持 $other_color 环境运行"
fi
exit 1
fi
# 5. 停止旧环境(Caddy 会自动只转发健康后端)
if [ "$other_color" != "none" ] && [ "$other_color" != "$target_color" ]; then
log_info "停止旧环境 $other_color..."
docker stop "app-$other_color" || true
docker rm "app-$other_color" || true
fi
log_info "部署完成!当前活跃环境: $target_color"
log_info "Caddy 会自动反代到健康实例,访问: https://app.gch3n.online"
}
# 主逻辑
main() {
log_info "========================================"
log_info " gch3n.online 蓝绿部署脚本 (Caddy)"
log_info "========================================"
local network_name
network_name=$(detect_network)
# 获取当前活跃环境
local active=$(get_active_color)
log_info "当前活跃环境: $active"
# 确定目标环境
local target=$1
if [ -z "$target" ]; then
# 自动选择非活跃环境
if [ "$active" = "blue" ]; then
target="green"
else
target="blue"
fi
log_info "自动选择目标环境: $target"
fi
# 验证参数
if [ "$target" != "blue" ] && [ "$target" != "green" ]; then
log_error "无效的环境: $target,只能是 blue 或 green"
exit 1
fi
# 确定端口
local target_port=3001
[ "$target" = "green" ] && target_port=3002
# 执行部署
deploy_to "$target" "$target_port" "$active" "$network_name"
}
# 执行
main "$@"
+103
View File
@@ -0,0 +1,103 @@
version: '3.8'
services:
# ========== Gitea Git 服务器 ==========
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.gch3n.online
- GITEA__server__ROOT_URL=https://git.gch3n.online/
- GITEA__server__SSH_DOMAIN=git.gch3n.online
- GITEA__server__SSH_PORT=2222
- GITEA__server__SSH_LISTEN_PORT=2222
- GITEA__service__DISABLE_REGISTRATION=false
- GITEA__service__REQUIRE_SIGNIN_VIEW=false
- GITEA__webhook__ALLOWED_HOST_LIST=external,loopback
ports:
- "127.0.0.1:3000:3000"
- "2222:2222"
volumes:
- ./data/gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
networks:
- gch3n-network
# ========== Nginx 反向代理 ==========
nginx:
image: nginx:alpine
container_name: nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./data/certbot/conf:/etc/letsencrypt:ro
- ./data/certbot/www:/var/www/certbot:ro
depends_on:
- gitea
networks:
- gch3n-network
# ========== Certbot SSL 证书 ==========
certbot:
image: certbot/certbot
container_name: certbot
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- gch3n-network
# ========== 应用蓝绿部署 ==========
app-blue:
image: gch3n/app:latest
container_name: app-blue
restart: always
expose:
- "3001"
environment:
- PORT=3001
- DEPLOY_COLOR=blue
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
networks:
- gch3n-network
deploy:
replicas: 0 # 初始状态,由部署脚本控制
app-green:
image: gch3n/app:latest
container_name: app-green
restart: always
expose:
- "3002"
environment:
- PORT=3002
- DEPLOY_COLOR=green
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3002/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
networks:
- gch3n-network
deploy:
replicas: 0 # 初始状态,由部署脚本控制
networks:
gch3n-network:
driver: bridge
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# Gitea 初始化脚本 - 在服务器上运行
echo "=========================================="
echo " Gitea 一键初始化脚本"
echo "=========================================="
# 配置
ADMIN_USER="gch3n"
ADMIN_EMAIL="admin@gch3n.online"
ADMIN_PASS="Gch3n@2026!"
SERVER_IP="49.235.172.252"
# 获取 CSRF token
echo "[1/4] 获取安装令牌..."
CSRF_TOKEN=$(curl -s -c /tmp/gitea_cookies.txt http://localhost:8080/install | grep -o 'name="_csrf" value="[^"]*"' | sed 's/.*value="\([^"]*\)".*/\1/')
if [ -z "$CSRF_TOKEN" ]; then
echo "错误: 无法获取 CSRF token"
exit 1
fi
echo "[2/4] 提交安装表单..."
curl -s -b /tmp/gitea_cookies.txt \
-X POST http://localhost:8080/install \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "_csrf=$CSRF_TOKEN" \
-d "db_type=SQLite3" \
-d "db_path=/data/gitea/gitea.db" \
-d "app_name=gch3n+Git" \
-d "repo_root_path=/data/git/gitea-repositories" \
-d "lfs_root_path=/data/gitea/lfs" \
-d "run_user=git" \
-d "domain=$SERVER_IP" \
-d "ssh_port=2222" \
-d "http_port=3000" \
-d "app_url=http://$SERVER_IP:8080/" \
-d "log_root_path=/data/gitea/log" \
-d "enable_federated_avatar=on" \
-d "default_allow_create_organization=on" \
-d "default_enable_timetracking=on" \
-d "no_reply_address=noreply.localhost" \
-d "admin_name=$ADMIN_USER" \
-d "admin_passwd=$ADMIN_PASS" \
-d "admin_confirm_passwd=$ADMIN_PASS" \
-d "admin_email=$ADMIN_EMAIL" \
-L > /tmp/gitea_install.log 2>&1
# 检查安装结果
if [ -f ~/gch3n-infra/data/gitea/gitea.db ]; then
echo "[3/4] ✓ 数据库创建成功"
else
echo "[3/4] ✗ 数据库创建失败"
cat /tmp/gitea_install.log
exit 1
fi
# 测试登录
echo "[4/4] 测试登录..."
LOGIN_RESULT=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST http://localhost:8080/user/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "user_name=$ADMIN_USER" \
-d "password=$ADMIN_PASS")
if [ "$LOGIN_RESULT" = "302" ]; then
echo "✓ 登录成功"
else
echo "✗ 登录失败 (HTTP $LOGIN_RESULT)"
fi
echo ""
echo "=========================================="
echo " Gitea 初始化完成!"
echo "=========================================="
echo ""
echo "访问地址: http://$SERVER_IP:8080"
echo "管理员: $ADMIN_USER"
echo "密码: $ADMIN_PASS"
echo ""
echo "建议立即登录并修改密码"
+174
View File
@@ -0,0 +1,174 @@
#!/bin/bash
set -e
# 服务器初始化脚本
# 在腾讯云服务器上执行此脚本以完成基础设施部署
echo "========================================"
echo " gch3n.online 服务器初始化脚本"
echo "========================================"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
# 检查 root 权限
if [ "$EUID" -ne 0 ]; then
log_error "请使用 root 权限运行此脚本"
exit 1
fi
# ========== 1. 系统更新 ==========
log_step "1/8 更新系统软件包..."
apt-get update && apt-get upgrade -y
# ========== 2. 安装必要工具 ==========
log_step "2/8 安装必要工具..."
apt-get install -y \
curl \
wget \
git \
vim \
htop \
net-tools \
certbot \
python3-certbot-nginx \
ufw \
fail2ban
# ========== 3. 安装 Docker ==========
log_step "3/8 安装 Docker..."
if ! command -v docker &> /dev/null; then
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
log_info "Docker 安装完成"
else
log_warn "Docker 已安装,跳过"
fi
# 将当前用户添加到 docker 组
usermod -aG docker nplx 2>/dev/null || true
# ========== 4. 安装 Docker Compose ==========
log_step "4/8 安装 Docker Compose..."
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
curl -L "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
log_info "Docker Compose 安装完成"
else
log_warn "Docker Compose 已安装,跳过"
fi
# ========== 5. 配置防火墙 ==========
log_step "5/8 配置防火墙..."
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw allow 11022/tcp # 自定义 SSH 端口
ufw allow 2222/tcp # Gitea SSH
ufw --force enable
log_info "防火墙配置完成"
# ========== 6. 配置 Fail2ban ==========
log_step "6/8 配置 Fail2ban..."
systemctl enable fail2ban
systemctl start fail2ban
log_info "Fail2ban 配置完成"
# ========== 7. 创建项目目录 ==========
log_step "7/8 创建项目目录..."
PROJECT_DIR="/home/nplx/gch3n-infra"
mkdir -p "$PROJECT_DIR"
mkdir -p "$PROJECT_DIR/data/gitea"
mkdir -p "$PROJECT_DIR/data/certbot/conf"
mkdir -p "$PROJECT_DIR/data/certbot/www"
mkdir -p "$PROJECT_DIR/data/letsencrypt"
# 设置权限
chown -R nplx:nplx "$PROJECT_DIR"
log_info "项目目录创建完成: $PROJECT_DIR"
# ========== 8. 申请 SSL 证书 ==========
log_step "8/8 申请 SSL 证书..."
log_warn "请确保域名 gch3n.online 和 git.gch3n.online 已解析到本服务器 IP: $(curl -s ifconfig.me)"
log_warn "按回车继续申请证书,或按 Ctrl+C 取消..."
read
# 创建临时 nginx 配置用于证书验证
cat > /tmp/nginx-cert.conf << 'EOF'
server {
listen 80;
server_name gch3n.online git.gch3n.online app.gch3n.online;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 200 "Certbot validation server";
}
}
EOF
# 启动临时 nginx
docker run -d --name temp-nginx \
-p 80:80 \
-v /tmp/nginx-cert.conf:/etc/nginx/conf.d/default.conf:ro \
-v "$PROJECT_DIR/data/certbot/www:/var/www/certbot" \
nginx:alpine
# 等待 nginx 启动
sleep 3
# 申请证书
certbot certonly --webroot \
-w /var/www/certbot \
-d gch3n.online \
-d git.gch3n.online \
-d app.gch3n.online \
--agree-tos \
--non-interactive \
--email admin@gch3n.online \
|| {
log_error "证书申请失败,请检查域名解析"
docker stop temp-nginx && docker rm temp-nginx
exit 1
}
# 停止临时 nginx
docker stop temp-nginx && docker rm temp-nginx
# 复制证书到项目目录
cp -r /etc/letsencrypt/live "$PROJECT_DIR/data/certbot/conf/"
chown -R nplx:nplx "$PROJECT_DIR/data/certbot"
log_info "SSL 证书申请完成"
echo ""
echo "========================================"
echo " 服务器初始化完成!"
echo "========================================"
echo ""
echo "下一步:"
echo "1. 将项目文件复制到 $PROJECT_DIR"
echo "2. 切换到 nplx 用户: su - nplx"
echo "3. 进入项目目录: cd ~/gch3n-infra"
echo "4. 启动服务: docker-compose up -d"
echo ""
echo "访问地址:"
echo "- Git 服务器: https://git.gch3n.online"
echo "- 应用: https://app.gch3n.online"
echo ""
@@ -0,0 +1,70 @@
# 应用蓝绿部署
server {
listen 80;
server_name app.gch3n.online;
# Certbot 验证
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name app.gch3n.online;
ssl_certificate /etc/letsencrypt/live/gch3n.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gch3n.online/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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# 安全头
add_header Strict-Transport-Security "max-age=63072000" always;
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 Referrer-Policy "strict-origin-when-cross-origin" always;
# 健康检查端点
location /health {
proxy_pass http://app_backend/health;
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;
access_log off;
}
# 应用代理
location / {
proxy_pass http://app_backend;
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 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 错误处理
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2;
}
}
@@ -0,0 +1,54 @@
# Gitea Git 服务器
server {
listen 80;
server_name git.gch3n.online;
# Certbot 验证
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name git.gch3n.online;
ssl_certificate /etc/letsencrypt/live/gch3n.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gch3n.online/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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# 安全头
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Gitea 代理
location / {
proxy_pass http://gitea:3000;
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 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
+52
View File
@@ -0,0 +1,52 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'upstream=$upstream_addr upstream_time=$upstream_response_time';
access_log /var/log/nginx/access.log main;
# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# 上游服务器(蓝绿部署)
upstream app_backend {
least_conn;
server app-blue:3001 max_fails=3 fail_timeout=30s;
server app-green:3002 max_fails=3 fail_timeout=30s;
# 健康检查
check interval=5000 rise=2 fall=3 timeout=3000 type=http;
check_http_send "GET /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
# 包含站点配置
include /etc/nginx/conf.d/*.conf;
}
+126
View File
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>gch3n.online</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
.container {
text-align: center;
padding: 40px;
max-width: 600px;
}
h1 {
font-size: 3.5rem;
margin-bottom: 20px;
font-weight: 700;
letter-spacing: -1px;
}
.tagline {
font-size: 1.3rem;
opacity: 0.9;
margin-bottom: 40px;
font-weight: 300;
}
.links {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.link-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 24px 32px;
text-decoration: none;
color: #fff;
transition: all 0.3s ease;
min-width: 140px;
}
.link-card:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.link-card h3 {
font-size: 1.1rem;
margin-bottom: 8px;
}
.link-card p {
font-size: 0.85rem;
opacity: 0.8;
}
.footer {
margin-top: 60px;
font-size: 0.9rem;
opacity: 0.6;
}
@media (max-width: 480px) {
h1 {
font-size: 2.5rem;
}
.links {
flex-direction: column;
}
.link-card {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<h1>gch3n.online</h1>
<p class="tagline">Personal DevOps Playground</p>
<div class="links">
<a href="https://git.gch3n.online" class="link-card">
<h3>🗂️ Git</h3>
<p>代码仓库</p>
</a>
<a href="https://app.gch3n.online" class="link-card">
<h3>🚀 App</h3>
<p>应用部署</p>
</a>
</div>
<div class="footer">
<p>Powered by Caddy + Docker + Gitea</p>
<p id="year"></p>
</div>
</div>
<script>
document.getElementById('year').textContent = '© ' + new Date().getFullYear();
</script>
</body>
</html>