189 lines
5.2 KiB
Bash
Executable File
189 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
# Ubuntu 服务器一键部署脚本。
|
|
# 用法:bash deploy/ubuntu-deploy.sh
|
|
|
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
ENV_FILE="${PROJECT_ROOT}/.env"
|
|
ENV_EXAMPLE="${PROJECT_ROOT}/.env.server.example"
|
|
BACKEND_DATA_DIR="${PROJECT_ROOT}/apps/backend/data"
|
|
BACKEND_CONTAINER_UID=1001
|
|
BACKEND_CONTAINER_GID=1001
|
|
HEALTH_URL=""
|
|
|
|
log() {
|
|
printf '\033[1;34m[deploy]\033[0m %s\n' "$*"
|
|
}
|
|
|
|
warn() {
|
|
printf '\033[1;33m[deploy]\033[0m %s\n' "$*"
|
|
}
|
|
|
|
fail() {
|
|
printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
read_env_value() {
|
|
local key="$1"
|
|
awk -F= -v key="${key}" '
|
|
$0 !~ /^[[:space:]]*#/ && $1 == key {
|
|
value = substr($0, index($0, "=") + 1)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
gsub(/^"|"$/, "", value)
|
|
gsub(/^'\''|'\''$/, "", value)
|
|
print value
|
|
exit
|
|
}
|
|
' "${ENV_FILE}"
|
|
}
|
|
|
|
ensure_docker() {
|
|
command -v docker >/dev/null 2>&1 || fail "未找到 docker,请先在 Ubuntu 上安装 Docker Engine。"
|
|
docker compose version >/dev/null 2>&1 || fail "未找到 docker compose 插件,请安装 Docker Compose plugin。"
|
|
command -v curl >/dev/null 2>&1 || fail "未找到 curl,请先安装:sudo apt-get update && sudo apt-get install -y curl"
|
|
}
|
|
|
|
ensure_env_file() {
|
|
if [[ -f "${ENV_FILE}" ]]; then
|
|
return
|
|
fi
|
|
|
|
cp "${ENV_EXAMPLE}" "${ENV_FILE}"
|
|
fail "已生成 .env,请先编辑真实域名、数据库密码、后台账号和密钥后再重新执行。"
|
|
}
|
|
|
|
validate_env_file() {
|
|
if grep -nE 'change-me|your-domain|example\.com' "${ENV_FILE}" >/tmp/order-site-env-placeholders.txt; then
|
|
cat /tmp/order-site-env-placeholders.txt >&2
|
|
fail ".env 仍包含示例占位值,请替换后再部署。"
|
|
fi
|
|
|
|
local site_addr
|
|
site_addr="$(read_env_value CADDY_SITE_ADDR)"
|
|
[[ -n "${site_addr}" ]] || fail ".env 缺少 CADDY_SITE_ADDR。"
|
|
HEALTH_URL="${site_addr%/}/health/ready"
|
|
|
|
validate_database_url_for_compose
|
|
}
|
|
|
|
validate_database_url_for_compose() {
|
|
local database_url
|
|
database_url="$(read_env_value DATABASE_URL)"
|
|
|
|
if [[ -z "${database_url}" ]]; then
|
|
return
|
|
fi
|
|
|
|
if [[ "${database_url}" == *"@localhost:"* \
|
|
|| "${database_url}" == *"@127.0.0.1:"* \
|
|
|| "${database_url}" == *"@[::1]:"* ]]; then
|
|
fail ".env 的 DATABASE_URL 当前指向本机地址。Docker 部署时后端容器内的 localhost 不是 postgres 容器,请改为 postgres 服务名或外部数据库地址。"
|
|
fi
|
|
}
|
|
|
|
ensure_data_dir() {
|
|
mkdir -p "${BACKEND_DATA_DIR}"
|
|
|
|
if [[ "$(id -u)" == "0" ]]; then
|
|
chown -R "${BACKEND_CONTAINER_UID}:${BACKEND_CONTAINER_GID}" "${BACKEND_DATA_DIR}"
|
|
return
|
|
fi
|
|
|
|
if command -v sudo >/dev/null 2>&1; then
|
|
sudo chown -R "${BACKEND_CONTAINER_UID}:${BACKEND_CONTAINER_GID}" "${BACKEND_DATA_DIR}"
|
|
return
|
|
fi
|
|
|
|
warn "未找到 sudo,跳过目录属主调整。若后端无法写入日志或配置,请执行:"
|
|
warn "sudo chown -R ${BACKEND_CONTAINER_UID}:${BACKEND_CONTAINER_GID} ${BACKEND_DATA_DIR}"
|
|
}
|
|
|
|
deploy_stack() {
|
|
log "拉取基础镜像"
|
|
docker compose pull --ignore-buildable
|
|
|
|
log "构建并启动服务"
|
|
if docker compose up -d --build; then
|
|
return
|
|
fi
|
|
|
|
print_diagnostics
|
|
fail "docker compose 启动失败,请根据上面的日志排查。"
|
|
}
|
|
|
|
wait_for_health() {
|
|
log "等待服务健康检查:${HEALTH_URL}"
|
|
|
|
for _ in $(seq 1 60); do
|
|
if curl -fsS "${HEALTH_URL}" >/dev/null 2>&1; then
|
|
log "服务已就绪"
|
|
return
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
print_diagnostics
|
|
fail "部署已启动但健康检查未通过,请根据日志排查。"
|
|
}
|
|
|
|
print_applied_migrations() {
|
|
log "已应用数据库迁移"
|
|
|
|
local migrations
|
|
if migrations="$(docker compose exec -T backend node --input-type=module -e '
|
|
const { query, closeDb } = await import("./dist/db/client.js");
|
|
try {
|
|
const result = await query("SELECT filename FROM schema_migrations ORDER BY filename");
|
|
console.log(result.rows.map((row) => row.filename).join("\n") || "(无迁移记录)");
|
|
} finally {
|
|
await closeDb();
|
|
}
|
|
' 2>&1)"; then
|
|
printf '%s\n' "${migrations}"
|
|
return
|
|
fi
|
|
|
|
warn "读取迁移记录失败:"
|
|
printf '%s\n' "${migrations}"
|
|
}
|
|
|
|
print_diagnostics() {
|
|
warn "当前容器状态:"
|
|
docker compose ps || true
|
|
|
|
warn "后端最近日志:"
|
|
local backend_logs
|
|
backend_logs="$(docker compose logs --tail=120 backend 2>&1 || true)"
|
|
printf '%s\n' "${backend_logs}"
|
|
|
|
if printf '%s\n' "${backend_logs}" | grep -q 'password authentication failed for user "postgres"'; then
|
|
warn "检测到 PostgreSQL 密码认证失败。通常是 postgres_data 已用旧密码初始化,而 .env 改成了新密码。"
|
|
warn "无生产数据时可执行:docker compose down -v && bash deploy/ubuntu-deploy.sh"
|
|
warn "需要保留数据时,请进入 postgres 容器用当前 .env 的 POSTGRES_PASSWORD 修改数据库用户密码。"
|
|
fi
|
|
|
|
warn "后端 ready 接口:"
|
|
docker compose exec -T backend sh -lc 'curl -fsS http://127.0.0.1:3000/health/ready || true' || true
|
|
|
|
print_applied_migrations || true
|
|
}
|
|
|
|
main() {
|
|
cd "${PROJECT_ROOT}"
|
|
|
|
ensure_docker
|
|
ensure_env_file
|
|
validate_env_file
|
|
ensure_data_dir
|
|
deploy_stack
|
|
wait_for_health
|
|
print_applied_migrations
|
|
|
|
log "部署完成"
|
|
docker compose ps
|
|
}
|
|
|
|
main "$@"
|