Add Caddy production setup with systemd services

This commit is contained in:
jingrow 2025-11-15 02:32:47 +08:00
parent a8c77e113d
commit a3ceb858e0
3 changed files with 422 additions and 7 deletions

View File

@ -1,7 +1,17 @@
# Caddy 配置文件 - 前端静态文件 + 后端 API 反向代理
# 最小化配置,符合 Caddy 2.x 最佳实践
#
# 使用说明:
# 1. 生产环境:将 :8080 替换为你的域名(如 example.com
# 2. 确保域名 DNS 已解析到服务器 IP
# 3. 确保防火墙开放 80 443 端口
# 4. Caddy 会自动从 Let's Encrypt 获取和续期 SSL 证书
# 5. 可选:取消注释 email 行,用于证书到期通知
{
# 自动 HTTPS 配置Let's Encrypt
# email your-email@example.com # 可选:用于证书到期通知
# acme_ca https://acme-v02.api.letsencrypt.org/directory # 默认使用 Let's Encrypt
log {
output stdout
format console
@ -15,21 +25,21 @@
path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.ico *.woff *.woff2 *.ttf *.eot
}
header @static Cache-Control "public, max-age=31536000, immutable"
# HTML 不缓存
@html {
file
path *.html
}
header @html Cache-Control "no-cache"
# 安全头
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
# API 反向代理
handle /api/* {
reverse_proxy localhost:9001 {
@ -38,7 +48,7 @@
header_up X-Forwarded-Proto {scheme}
}
}
# 后端 API 反向代理
handle /jingrow/* {
reverse_proxy localhost:9001 {
@ -47,7 +57,7 @@
header_up X-Forwarded-Proto {scheme}
}
}
# 前端静态文件 + SPA 路由支持
handle {
root * apps/jingrow/frontend/dist
@ -56,4 +66,3 @@
file_server
}
}

BIN
caddy

Binary file not shown.

406
setup_production.sh Executable file
View File

@ -0,0 +1,406 @@
#!/bin/bash
# 一键生成生产环境服务文件脚本(静默安装)
# 包括Caddy 安装、后端服务 systemd 文件、Caddy 服务 systemd 文件
#
# 用法:
# ./setup_production.sh # 静默安装所有内容(默认)
# ./setup_production.sh --no-install # 只生成服务文件,不安装到系统
set -e
# 获取脚本所在目录(项目根目录)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" || exit 1
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 打印函数
info() { echo -e "${BLUE}${NC} $1"; }
success() { echo -e "${GREEN}${NC} $1"; }
warn() { echo -e "${YELLOW}${NC} $1"; }
error() { echo -e "${RED}${NC} $1"; }
# 配置变量
CADDY_VERSION="2.10.2"
CADDY_ARCH="linux_amd64"
CADDY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_${CADDY_ARCH}.tar.gz"
CADDY_BINARY="caddy"
CADDY_INSTALL_DIR="/usr/local/bin"
CADDY_CONFIG_DIR="/etc/caddy"
CADDY_DATA_DIR="/var/lib/caddy"
PROJECT_ROOT="$SCRIPT_DIR"
JINGROW_APP_DIR="$PROJECT_ROOT/apps/jingrow"
SERVICE_USER="${SERVICE_USER:-$(whoami)}"
SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn)}"
TMP_DIR="$PROJECT_ROOT/tmp"
# 检测 uv 路径
detect_uv_path() {
if command -v uv &> /dev/null; then
UV_PATH=$(command -v uv)
info "检测到 uv 路径: $UV_PATH"
else
# 尝试常见路径
if [ -f "$HOME/.local/bin/uv" ]; then
UV_PATH="$HOME/.local/bin/uv"
elif [ -f "/usr/local/bin/uv" ]; then
UV_PATH="/usr/local/bin/uv"
else
error "未找到 uv请先安装"
exit 1
fi
warn "使用 uv 路径: $UV_PATH"
fi
}
# 检测系统架构
detect_arch() {
local arch=$(uname -m)
case "$arch" in
x86_64|amd64)
CADDY_ARCH="linux_amd64"
;;
aarch64|arm64)
CADDY_ARCH="linux_arm64"
;;
armv7l|armv6l)
CADDY_ARCH="linux_armv7"
;;
*)
error "不支持的架构: $arch"
exit 1
;;
esac
CADDY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_${CADDY_ARCH}.tar.gz"
info "检测到架构: $arch,使用 $CADDY_ARCH"
}
# 创建 Caddyfile 符号链接
setup_caddyfile_link() {
# 创建必要的目录
if [ "$EUID" -eq 0 ]; then
mkdir -p "$CADDY_CONFIG_DIR" "$CADDY_DATA_DIR"
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$CADDY_CONFIG_DIR" "$CADDY_DATA_DIR" 2>/dev/null || true
else
sudo mkdir -p "$CADDY_CONFIG_DIR" "$CADDY_DATA_DIR"
sudo chown -R "$SERVICE_USER:$SERVICE_GROUP" "$CADDY_CONFIG_DIR" "$CADDY_DATA_DIR" 2>/dev/null || true
fi
# 创建 Caddyfile 符号链接
if [ -f "$PROJECT_ROOT/Caddyfile" ]; then
info "创建 Caddyfile 符号链接..."
# 如果已存在文件或链接,先删除
if [ "$EUID" -eq 0 ]; then
[ -e "$CADDY_CONFIG_DIR/Caddyfile" ] && rm -f "$CADDY_CONFIG_DIR/Caddyfile"
ln -s "$PROJECT_ROOT/Caddyfile" "$CADDY_CONFIG_DIR/Caddyfile"
else
[ -e "$CADDY_CONFIG_DIR/Caddyfile" ] && sudo rm -f "$CADDY_CONFIG_DIR/Caddyfile"
sudo ln -s "$PROJECT_ROOT/Caddyfile" "$CADDY_CONFIG_DIR/Caddyfile"
fi
success "Caddyfile 符号链接已创建: $CADDY_CONFIG_DIR/Caddyfile -> $PROJECT_ROOT/Caddyfile"
else
warn "未找到 Caddyfile请手动创建符号链接: ln -s $PROJECT_ROOT/Caddyfile $CADDY_CONFIG_DIR/Caddyfile"
fi
}
# 安装 Caddy
install_caddy() {
info "开始安装 Caddy ${CADDY_VERSION}..."
# 检查是否已安装
if command -v caddy &> /dev/null; then
local installed_version=$(caddy version 2>/dev/null | head -n1 | grep -oP 'v\d+\.\d+\.\d+' || echo "unknown")
info "Caddy 已安装,版本: $installed_version,跳过二进制安装"
else
# 创建临时目录
local tmp_dir=$(mktemp -d)
trap "rm -rf $tmp_dir" EXIT
info "下载 Caddy..."
local caddy_tar="$tmp_dir/caddy.tar.gz"
if ! curl -L -o "$caddy_tar" "$CADDY_URL"; then
error "下载 Caddy 失败"
exit 1
fi
info "解压 Caddy..."
cd "$tmp_dir"
tar -xzf "$caddy_tar"
# 检查二进制文件是否存在
if [ ! -f "$CADDY_BINARY" ]; then
error "未找到 Caddy 二进制文件"
exit 1
fi
# 安装到系统目录(需要 root 权限)
info "安装 Caddy 到 $CADDY_INSTALL_DIR..."
if [ "$EUID" -eq 0 ]; then
cp "$CADDY_BINARY" "$CADDY_INSTALL_DIR/caddy"
chmod +x "$CADDY_INSTALL_DIR/caddy"
else
warn "需要 root 权限安装 Caddy尝试使用 sudo..."
sudo cp "$CADDY_BINARY" "$CADDY_INSTALL_DIR/caddy"
sudo chmod +x "$CADDY_INSTALL_DIR/caddy"
fi
fi
# 无论是否已安装,都设置 Caddyfile 符号链接
setup_caddyfile_link
success "Caddy 安装完成"
# 验证安装
if command -v caddy &> /dev/null; then
local version=$(caddy version 2>/dev/null | head -n1 || echo "unknown")
info "Caddy 版本: $version"
fi
}
# 生成后端服务 systemd 文件
generate_backend_service() {
info "生成后端服务 systemd 文件..."
# 确保 tmp 目录存在
mkdir -p "$TMP_DIR"
local service_file="$TMP_DIR/jingrow-backend.service"
local uv_dir=$(dirname "$UV_PATH")
local env_file_line=""
# 如果 .env 文件存在,使用 EnvironmentFile 引用
if [ -f "$PROJECT_ROOT/.env" ]; then
env_file_line="EnvironmentFile=${PROJECT_ROOT}/.env"
fi
cat > "$service_file" <<EOF
[Unit]
Description=Jingrow Backend API Service
After=network.target redis.service
Requires=redis.service
[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_GROUP}
WorkingDirectory=${JINGROW_APP_DIR}
Environment="PATH=${uv_dir}:/usr/local/bin:/usr/bin:/bin"
Environment="HOME=${HOME}"
${env_file_line}
ExecStart=${UV_PATH} run python -m jingrow.app
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=jingrow-backend
# 安全设置
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
success "已生成: $service_file"
}
# 生成 Worker 服务 systemd 文件
generate_worker_service() {
info "生成 Worker 服务 systemd 文件..."
# 确保 tmp 目录存在
mkdir -p "$TMP_DIR"
local service_file="$TMP_DIR/jingrow-worker.service"
local uv_dir=$(dirname "$UV_PATH")
local env_file_line=""
# 如果 .env 文件存在,使用 EnvironmentFile 引用
if [ -f "$PROJECT_ROOT/.env" ]; then
env_file_line="EnvironmentFile=${PROJECT_ROOT}/.env"
fi
cat > "$service_file" <<EOF
[Unit]
Description=Jingrow Worker Service (Dramatiq)
After=network.target redis.service
Requires=redis.service
[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_GROUP}
WorkingDirectory=${JINGROW_APP_DIR}
Environment="PATH=${uv_dir}:/usr/local/bin:/usr/bin:/bin"
Environment="HOME=${HOME}"
${env_file_line}
ExecStart=${UV_PATH} run python -m jingrow.dramatiq
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=jingrow-worker
# 安全设置
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
success "已生成: $service_file"
}
# 生成 Caddy 服务 systemd 文件
generate_caddy_service() {
info "生成 Caddy 服务 systemd 文件..."
# 确保 tmp 目录存在
mkdir -p "$TMP_DIR"
local service_file="$TMP_DIR/caddy.service"
cat > "$service_file" <<EOF
[Unit]
Description=Caddy - Modern HTTP/2 web server
Documentation=https://caddyserver.com/docs/
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
User=${SERVICE_USER}
Group=${SERVICE_GROUP}
WorkingDirectory=${PROJECT_ROOT}
ExecStart=${CADDY_INSTALL_DIR}/caddy run --environ --config ${CADDY_CONFIG_DIR}/Caddyfile
ExecReload=${CADDY_INSTALL_DIR}/caddy reload --config ${CADDY_CONFIG_DIR}/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
EOF
success "已生成: $service_file"
}
# 安装 systemd 服务文件
install_services() {
# 检查是否有 --no-install 参数
local skip_install=false
for arg in "$@"; do
if [ "$arg" = "--no-install" ]; then
skip_install=true
break
fi
done
if [ "$skip_install" = true ]; then
info "跳过服务安装(--no-install服务文件已生成在 $TMP_DIR"
return 0
fi
info "安装 systemd 服务文件..."
local services=("jingrow-backend.service" "jingrow-worker.service" "caddy.service")
for service in "${services[@]}"; do
local service_file="$TMP_DIR/$service"
local service_name=$(basename "$service")
if [ -f "$service_file" ]; then
info "安装 $service_name..."
if [ "$EUID" -eq 0 ]; then
cp "$service_file" "/etc/systemd/system/$service_name"
chmod 644 "/etc/systemd/system/$service_name"
else
sudo cp "$service_file" "/etc/systemd/system/$service_name"
sudo chmod 644 "/etc/systemd/system/$service_name"
fi
success "$service_name 已安装"
else
warn "$service_name 不存在,跳过"
fi
done
# 重新加载 systemd
info "重新加载 systemd..."
if [ "$EUID" -eq 0 ]; then
systemctl daemon-reload
else
sudo systemctl daemon-reload
fi
success "服务文件安装完成"
info "使用以下命令管理服务:"
echo " sudo systemctl start jingrow-backend"
echo " sudo systemctl start jingrow-worker"
echo " sudo systemctl start caddy"
echo " sudo systemctl enable jingrow-backend jingrow-worker caddy # 开机自启"
}
# 主函数
main() {
info "开始生成生产环境服务文件..."
# 检测架构
detect_arch
# 检查必要工具
if ! command -v curl &> /dev/null; then
error "需要 curl请先安装"
exit 1
fi
if ! command -v tar &> /dev/null; then
error "需要 tar请先安装"
exit 1
fi
# 检测 uv 路径
detect_uv_path
# 检查项目目录
if [ ! -d "$JINGROW_APP_DIR" ]; then
error "未找到后端目录: $JINGROW_APP_DIR"
exit 1
fi
# 安装 Caddy
install_caddy
# 生成服务文件
generate_backend_service
generate_worker_service
generate_caddy_service
# 安装服务(静默安装,除非指定 --no-install
install_services "$@"
success "完成!"
info "生成的文件(位于 $TMP_DIR:"
echo " - $TMP_DIR/jingrow-backend.service"
echo " - $TMP_DIR/jingrow-worker.service"
echo " - $TMP_DIR/caddy.service"
echo ""
info "注意事项:"
echo " 1. 确保已构建前端: npm run build:frontend"
echo " 2. 确保后端依赖已安装: npm run install:backend"
echo " 3. 确保 Redis 已安装并运行"
echo " 4. 检查并配置 .env 文件"
echo " 5. 修改服务文件中的用户和组(如需要)"
}
main "$@"