feat(val-blog): add 2026-04-30 dream journey post
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
Executable
+167
@@ -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 "$@"
|
||||
|
||||
@@ -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
|
||||
@@ -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 "建议立即登录并修改密码"
|
||||
Executable
+174
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user