diff --git a/.dockerignore b/.dockerignore index c02c856..d7524e6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,10 @@ # 后端构建上下文忽略项:只保留 pom.xml 与 src target/ data/ +mariadb-data/ +mariadb_data/ +*.pid +*.log .idea/ .vscode/ .git/ diff --git a/.env.example b/.env.example index 082cc5a..a2447a7 100644 --- a/.env.example +++ b/.env.example @@ -9,7 +9,7 @@ # ===================================================================== # ---------- 数据库(MariaDB) ---------- -# 首次创建数据卷时生效;修改后如需重建,先执行 docker compose down -v +# 首次初始化时生效;修改后如需重建,先执行 docker compose down -v 并删除 DATA_DIR 目录 DB_NAME=lab DB_USER=lab DB_PASSWORD=lab123456 @@ -28,5 +28,6 @@ WEB_PORT=8088 # 后端直连端口(调试用,前端经 Nginx 代理访问后端) BACKEND_PORT=8080 -# 数据目录 -DATA_DIR=./mariadb_data +# ---------- 数据持久化目录(宿主机) ---------- +# MariaDB 数据文件保存位置,升级/迁移时可直接拷贝该目录 +DATA_DIR=./mariadb-data diff --git a/.gitignore b/.gitignore index 29678e6..3a19d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ data/ .env !.env.example -mariadb-data +mariadb-data/ +mariadb_data/ diff --git a/README.md b/README.md index f5ff3da..788037a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ docker compose up -d --build # 构建并启动 mariadb + backend + frontend # 后端:http://localhost:8080 docker compose ps # 查看健康状态 docker compose down # 停止容器,保留数据库数据卷 -docker compose down -v # 停止并清空数据库(下次启动重建演示数据) +docker compose down -v # 停止容器并删除数据目录(下次启动重建演示数据) ``` - 编排:`mariadb:11.4`(数据持久化到命名卷)+ 后端多阶段构建镜像(JRE 21,非 root 运行)+ Nginx 前端镜像 @@ -77,10 +77,24 @@ Error: "lab-backend" is not a valid container, cannot be used as a dependency .. 而系统未配置 `unqualified-search-registries`。**先显式构建镜像即可解决**: ```bash -./scripts/podman-deploy.sh up # 一键:构建镜像 -> 启动 -> 健康检查 -> 打印访问地址 -./scripts/podman-deploy.sh down # 停止(保留数据库数据卷) -./scripts/podman-deploy.sh down-v # 停止并清空数据库 -./scripts/podman-deploy.sh logs # 查看日志 +./scripts/podman-deploy.sh up # 一键:构建镜像 -> 按序启动 -> 健康检查 -> 打印访问地址 +./scripts/podman-deploy.sh status # 查看容器状态 +./scripts/podman-deploy.sh down # 停止(保留数据目录) +./scripts/podman-deploy.sh down-v # 停止并删除数据目录(重建演示数据) +./scripts/podman-deploy.sh logs # 跟踪后端日志 +./scripts/podman-deploy.sh diagnose # 部署失败时输出诊断信息(镜像/容器/日志) +``` + +脚本做了三件关键的事,规避 podman-compose 的兼容性问题: + +1. `podman build` 显式构建 `lab-backend:latest` / `lab-frontend:latest`,避免短名拉取; +2. `up -d --no-deps` 按「数据库 → 后端 → 前端」逐个启动并等待就绪,绕过 compose 依赖查找; +3. `down` 直接按容器名清理,避免对不存在容器的报错噪音。 + +若已有历史失败状态,先清理再重来: + +```bash +./scripts/podman-deploy.sh down-v && ./scripts/podman-deploy.sh up ``` 手动等价步骤: @@ -101,7 +115,9 @@ Podman 注意事项: `~/.config/containers/registries.conf` 配置 `unqualified-search-registries = ["docker.io"]` - **旧版 podman-compose**:若提示不支持 `depends_on.condition`,请升级到 1.0.6+, 或先 `podman-compose up -d mariadb`,等数据库健康后再 `podman-compose up -d backend frontend` -- **SELinux(Fedora/RHEL)**:本项目只使用命名卷,无需额外 `:Z` 标记;如自定义 bind mount 请自行加 `:Z` +- **数据目录**:数据库数据以 bind mount 方式保存在 `DATA_DIR`(默认 `./mariadb-data`,已在 + `.gitignore` 中忽略),`down-v` 会删除该目录;目录中含历史 binlog 等文件时属正常现象 +- **SELinux(Fedora/RHEL)**:挂载已带 `:Z` 自动打标;若自定义其他 bind mount 请同样加 `:Z` ### 5. 演示账号 diff --git a/docker-compose.yml b/docker-compose.yml index ba62a5a..7c0ff6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,8 +34,9 @@ services: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci volumes: - # 数据库数据持久化;首次启动由后端自动建表并写入演示数据 - - ${DATA_DIR:-./mariadb-data}:/var/lib/mysql + # 数据库数据持久化到宿主机目录(便于备份/检查),首次启动自动建表并写入演示数据 + # :Z 供 Linux SELinux 环境自动打标,Docker Desktop / macOS 会忽略该后缀 + - ${DATA_DIR:-./mariadb-data}:/var/lib/mysql:Z # 需要本地客户端连库调试时可打开端口映射 # ports: # - "3306:3306" diff --git a/scripts/podman-deploy.sh b/scripts/podman-deploy.sh index 8c81848..cda9d5b 100755 --- a/scripts/podman-deploy.sh +++ b/scripts/podman-deploy.sh @@ -3,17 +3,22 @@ # 使用 Podman 部署实验室预约平台 # # 用法: -# ./scripts/podman-deploy.sh up # 构建镜像并启动(默认) +# ./scripts/podman-deploy.sh up # 构建镜像并按 数据库->后端->前端 顺序启动(默认) +# ./scripts/podman-deploy.sh build # 只构建镜像 # ./scripts/podman-deploy.sh down # 停止并删除容器(保留数据库数据卷) -# ./scripts/podman-deploy.sh down-v # 停止并删除容器与数据卷(重建演示数据) +# ./scripts/podman-deploy.sh down-v # 停止并删除数据目录(重建演示数据) # ./scripts/podman-deploy.sh status # 查看容器状态 # ./scripts/podman-deploy.sh logs # 跟随查看日志 +# ./scripts/podman-deploy.sh diagnose # 输出镜像/容器/日志等诊断信息 # -# 说明:Podman 的 compose 实现(podman-compose)在容器名/镜像名解析上与 -# Docker Compose 存在差异,常见报错为: +# 背景:Podman 的 compose 实现(podman-compose)与 Docker Compose 在 +# 「镜像名解析、depends_on 依赖查找」上存在差异,直接 up 常见报错: # Error: short-name "lab-reservation_backend" did not resolve to an alias ... -# 因此本脚本先显式构建 lab-backend:latest / lab-frontend:latest, -# 再交给 compose 启动,确保 compose 直接使用本地镜像而不会尝试拉取。 +# Error: "lab-backend" is not a valid container, cannot be used as a dependency +# 本脚本的处理方式: +# 1) 先用 podman build 显式构建 lab-backend / lab-frontend 镜像,避免短名拉取; +# 2) 再用 `up -d --no-deps` 逐个启动服务并等待就绪,绕过 compose 的依赖查找; +# 3) down 时直接按容器名清理,避免 podman-compose 对不存在容器的报错噪音。 # ===================================================================== set -euo pipefail @@ -21,83 +26,220 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" cd "${PROJECT_DIR}" +# ---------- 服务/容器名 ---------- +MARIADB_CONTAINER="lab-mariadb" +BACKEND_CONTAINER="lab-backend" +FRONTEND_CONTAINER="lab-frontend" +NETWORK_NAME="lab-reservation_lab-net" + +# ---------- 检查 podman ---------- +if ! command -v podman >/dev/null 2>&1; then + echo "错误:未找到 podman,请先安装:https://podman.io/docs/installation" >&2 + exit 1 +fi + # ---------- 选择可用的 compose 命令 ---------- if podman compose version >/dev/null 2>&1; then COMPOSE=(podman compose) elif command -v podman-compose >/dev/null 2>&1; then COMPOSE=(podman-compose) else - echo "错误:未找到 podman compose / podman-compose,请先安装其一" >&2 + echo "错误:未找到 podman compose / podman-compose,请安装 podman-compose:" >&2 + echo " pipx install podman-compose 或 pip3 install --user podman-compose" >&2 exit 1 fi -# ---------- 读取 .env 中的端口配置(不 source,避免值中含空格被当作命令执行) ---------- +# ---------- 读取 .env 端口配置(不 source,避免值中含空格被当作命令执行) ---------- read_env() { - [ -f .env ] && grep -E "^$1=" .env 2>/dev/null | tail -n 1 | cut -d= -f2- | tr -d '"' || true + if [ -f .env ]; then + grep -E "^$1=" .env 2>/dev/null | tail -n 1 | cut -d= -f2- | tr -d '"' || true + fi } -WEB_PORT="${WEB_PORT:-$(read_env WEB_PORT)}" -BACKEND_PORT="${BACKEND_PORT:-$(read_env BACKEND_PORT)}" +WEB_PORT="$(read_env WEB_PORT)" +BACKEND_PORT="$(read_env BACKEND_PORT)" +DATA_DIR_ENV="$(read_env DATA_DIR)" WEB_PORT="${WEB_PORT:-8088}" BACKEND_PORT="${BACKEND_PORT:-8080}" +DATA_DIR="${DATA_DIR:-${DATA_DIR_ENV:-./mariadb-data}}" +DATA_DIR="${DATA_DIR%/}" + +# ---------- 通用检查函数(inspect 在 podman / docker 下语义一致) ---------- +image_exists() { + podman image inspect "$1" >/dev/null 2>&1 +} +container_exists() { + podman inspect "$1" >/dev/null 2>&1 +} # ---------- 构建镜像 ---------- build_images() { - echo "==> 构建后端镜像 lab-backend:latest(首次构建需拉取 Maven 依赖)" + echo "==> 构建后端镜像 lab-backend:latest(首次构建需拉取 Maven 依赖,可能较慢)" podman build -t lab-backend:latest . echo "==> 构建前端镜像 lab-frontend:latest" podman build -t lab-frontend:latest ./frontend + if ! image_exists lab-backend:latest || ! image_exists lab-frontend:latest; then + echo "错误:镜像构建失败,请查看上方构建日志" >&2 + exit 1 + fi + echo "==> 镜像构建完成" } -# ---------- 等待 HTTP 服务就绪 ---------- -wait_ready() { - local name="$1" url="$2" tries=60 code +# ---------- 启动单个服务(--no-deps 绕过 compose 依赖查找,失败则回退) ---------- +start_service() { + local service="$1" + if ! "${COMPOSE[@]}" up -d --no-deps "${service}" 2>/dev/null; then + "${COMPOSE[@]}" up -d "${service}" + fi +} + +# ---------- 等待 MySQL/MariaDB 就绪 ---------- +wait_mariadb() { + local tries=60 + echo " 等待 MariaDB 就绪..." for _ in $(seq 1 "${tries}"); do - code="$(curl -s -o /dev/null -w '%{http_code}' "${url}" || true)" - if [ -n "${code}" ] && [ "${code}" != "000" ]; then - echo " ${name} 就绪:${url}(HTTP ${code})" + if podman exec "${MARIADB_CONTAINER}" healthcheck.sh --connect --innodb_initialized >/dev/null 2>&1; then + echo " MariaDB 已就绪" return 0 fi sleep 2 done - echo " 警告:${name} 未在预期时间内就绪,请执行 '$0 logs' 查看日志" >&2 + echo " 警告:MariaDB 未在预期时间内就绪" >&2 return 1 } +# ---------- 等待 HTTP 服务响应 ---------- +wait_http() { + local name="$1" url="$2" tries="${3:-60}" code + echo " 等待 ${name} 响应..." + for _ in $(seq 1 "${tries}"); do + code="$(curl -s -o /dev/null -w '%{http_code}' "${url}" || true)" + if [ -n "${code}" ] && [ "${code}" != "000" ]; then + echo " ${name} 已就绪:${url}(HTTP ${code})" + return 0 + fi + sleep 2 + done + echo " 警告:${name} 未在预期时间内响应:${url}" >&2 + return 1 +} + +# ---------- 打印部署结果 ---------- +print_result() { + echo + echo "部署完成:" + echo " 前端地址:http://localhost:${WEB_PORT}" + echo " 后端地址:http://localhost:${BACKEND_PORT}" + echo " 演示账号:admin/admin123、teacher1/123456、student1/123456" + echo + podman ps --filter "name=lab-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +} + +# ---------- 诊断信息 ---------- +diagnose() { + echo "== podman 版本 ==" + podman version --format '{{.Client.Version}} (server {{.Server.Version}})' 2>/dev/null || podman --version + echo + echo "== compose 命令 ==" + echo "${COMPOSE[*]}" + echo + echo "== 相关镜像 ==" + podman images --filter "reference=lab-*" --format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Created}}" + echo + echo "== 相关容器 ==" + podman ps -a --filter "name=lab-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + echo + for container in "${MARIADB_CONTAINER}" "${BACKEND_CONTAINER}" "${FRONTEND_CONTAINER}"; do + if container_exists "${container}"; then + echo "== ${container} 最近日志 ==" + podman logs --tail 30 "${container}" 2>&1 || true + echo + fi + done +} + +# ---------- 停止并清理 ---------- +down() { + # 静默调用 compose down(不存在容器时的报错噪音直接忽略),再按容器名兜底清理 + "${COMPOSE[@]}" down >/dev/null 2>&1 || true + for container in "${FRONTEND_CONTAINER}" "${BACKEND_CONTAINER}" "${MARIADB_CONTAINER}"; do + podman rm -f "${container}" >/dev/null 2>&1 || true + done + podman network rm "${NETWORK_NAME}" >/dev/null 2>&1 || true + echo "已停止并删除容器(数据库数据卷保留)" +} + +down_volumes() { + down + + # 兼容历史配置:清理名为 *mariadb-data 的命名卷 + local volumes + volumes="$(podman volume ls --format '{{.Name}}' 2>/dev/null | grep -E 'mariadb-data$' || true)" + if [ -n "${volumes}" ]; then + echo "${volumes}" | while read -r volume; do + podman volume rm -f "${volume}" >/dev/null 2>&1 || true + done + fi + + # 当前配置:数据库数据持久化在宿主机目录(DATA_DIR,默认 ./mariadb-data) + local absolute_dir + absolute_dir="$(cd "$(dirname "${DATA_DIR}")" 2>/dev/null && pwd)/$(basename "${DATA_DIR}")" || absolute_dir="" + case "${absolute_dir}" in + "${PROJECT_DIR}"/*) + if [ -d "${absolute_dir}" ]; then + rm -rf "${absolute_dir}" + echo "已删除数据库数据目录 ${absolute_dir},下次启动将重建演示数据" + fi + ;; + "") + echo "提示:数据目录 ${DATA_DIR} 无法解析,请手动检查" + ;; + *) + echo "提示:数据目录 ${DATA_DIR} 不在项目目录内,出于安全考虑未自动删除,请手动处理" + ;; + esac +} + ACTION="${1:-up}" case "${ACTION}" in up) build_images - echo "==> 启动容器" - "${COMPOSE[@]}" up -d - echo "==> 等待服务就绪" - # 后端返回 401 也代表进程已起来 - wait_ready "后端" "http://localhost:${BACKEND_PORT}/api/labs" || true - wait_ready "前端" "http://localhost:${WEB_PORT}/" || true - echo - echo "部署完成:" - echo " 前端地址:http://localhost:${WEB_PORT}" - echo " 后端地址:http://localhost:${BACKEND_PORT}" - echo " 演示账号:admin/admin123、teacher1/123456、student1/123456" - echo - "${COMPOSE[@]}" ps || podman ps --filter "name=lab-" + echo "==> 1/3 启动数据库" + start_service mariadb + wait_mariadb || true + echo "==> 2/3 启动后端" + start_service backend + if ! wait_http "后端" "http://localhost:${BACKEND_PORT}/api/labs" 60; then + diagnose + exit 1 + fi + echo "==> 3/3 启动前端" + start_service frontend + if ! wait_http "前端" "http://localhost:${WEB_PORT}/" 60; then + diagnose + exit 1 + fi + print_result ;; build) build_images ;; down) - "${COMPOSE[@]}" down + down ;; down-v) - "${COMPOSE[@]}" down -v + down_volumes ;; status) - "${COMPOSE[@]}" ps || podman ps --filter "name=lab-" + podman ps -a --filter "name=lab-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ;; logs) - "${COMPOSE[@]}" logs -f --tail=100 + podman logs -f --tail=100 "${BACKEND_CONTAINER}" + ;; + diagnose) + diagnose ;; *) - echo "用法:$0 [up|build|down|down-v|status|logs]" >&2 + echo "用法:$0 [up|build|down|down-v|status|logs|diagnose]" >&2 exit 1 ;; esac