#!/usr/bin/env bash # 构建 api / player / admin 生产镜像并导出为 tar # 用法: # ./docs/docker/build-and-export-images.sh # ./docs/docker/build-and-export-images.sh --tag v1.2.3 # ./build-and-export-images.sh --use-cache # ./build-and-export-images.sh --export-only set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$ROOT" COMPOSE_FILE="docker-compose.prod.yml" ENV_FILE=".env.docker" OUTPUT="" SERVICES=(api player admin) NO_CACHE=1 EXPORT_ONLY=0 TAG="${IMAGE_TAG:-}" default_tag() { if command -v git >/dev/null 2>&1 && git rev-parse --short HEAD >/dev/null 2>&1; then git rev-parse --short HEAD else date +%Y%m%d-%H%M%S fi } validate_tag() { printf '%s' "$1" | grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' || { echo "错误: 镜像 tag 不合法: $1" >&2; exit 1; } } usage() { cat <<'EOF' 用法: docs/docker/build-and-export-images.sh [选项] 选项: --tag TAG 镜像 tag(默认当前 git 短提交号;非 git 目录则用时间戳) --use-cache 构建时使用 Docker 缓存(默认 --no-cache) --export-only 跳过构建,仅导出已有指定 tag 镜像 --output PATH 导出文件路径(默认项目根目录 thebet365-images-.tar) -h, --help 显示帮助 EOF } while [[ $# -gt 0 ]]; do case "$1" in --tag) TAG="${2:?缺少 --tag 参数值}" shift 2 ;; --use-cache) NO_CACHE=0; shift ;; --export-only) EXPORT_ONLY=1; shift ;; --output) OUTPUT="${2:?缺少 --output 参数值}" shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "未知参数: $1" >&2; usage; exit 1 ;; esac done if [[ ! -f "$COMPOSE_FILE" ]]; then echo "错误: 未找到 ${COMPOSE_FILE}(目录: ${ROOT})" >&2 exit 1 fi if [[ ! -f "$ENV_FILE" ]]; then if [[ -f ".env.docker.example" ]]; then echo "警告: 未找到 ${ENV_FILE},将使用 .env.docker.example(生产部署请复制为 .env.docker 并修改密钥)" ENV_FILE=".env.docker.example" else echo "错误: 未找到 ${ENV_FILE} 或 .env.docker.example" >&2 exit 1 fi fi TAG="${TAG:-$(default_tag)}" validate_tag "$TAG" OUTPUT="${OUTPUT:-thebet365-images-$TAG.tar}" if [[ "$EXPORT_ONLY" -eq 0 ]]; then echo "==> 构建镜像: ${SERVICES[*]} (tag: $TAG)" BUILD_ARGS=(compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build) if [[ "$NO_CACHE" -eq 1 ]]; then BUILD_ARGS+=(--no-cache) fi BUILD_ARGS+=("${SERVICES[@]}") IMAGE_TAG="$TAG" docker "${BUILD_ARGS[@]}" fi OUTPUT_PATH="$OUTPUT" if [[ "$OUTPUT" != /* ]] && [[ "$OUTPUT" != [A-Za-z]:* ]]; then OUTPUT_PATH="$ROOT/$OUTPUT" fi echo "==> 导出镜像 -> $OUTPUT_PATH" docker save \ "thebet365-api:$TAG" \ "thebet365-player:$TAG" \ "thebet365-admin:$TAG" \ -o "$OUTPUT_PATH" MANIFEST_PATH="${OUTPUT_PATH%.tar}.manifest.txt" BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" GIT_COMMIT="unknown" GIT_DIRTY="unknown" if command -v git >/dev/null 2>&1 && git rev-parse HEAD >/dev/null 2>&1; then GIT_COMMIT="$(git rev-parse HEAD)" if git diff --quiet && git diff --cached --quiet; then GIT_DIRTY="false" else GIT_DIRTY="true" fi fi CHECKSUM="unavailable" if command -v sha256sum >/dev/null 2>&1; then CHECKSUM="$(sha256sum "$OUTPUT_PATH" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then CHECKSUM="$(shasum -a 256 "$OUTPUT_PATH" | awk '{print $1}')" fi { echo "tag=$TAG" echo "built_at=$BUILT_AT" echo "git_commit=$GIT_COMMIT" echo "git_dirty=$GIT_DIRTY" echo "tar=$(basename "$OUTPUT_PATH")" echo "tar_sha256=$CHECKSUM" echo "api_image=thebet365-api:$TAG" echo "api_image_id=$(docker image inspect "thebet365-api:$TAG" --format '{{.Id}}')" echo "player_image=thebet365-player:$TAG" echo "player_image_id=$(docker image inspect "thebet365-player:$TAG" --format '{{.Id}}')" echo "admin_image=thebet365-admin:$TAG" echo "admin_image_id=$(docker image inspect "thebet365-admin:$TAG" --format '{{.Id}}')" } > "$MANIFEST_PATH" if command -v du >/dev/null 2>&1; then SIZE="$(du -h "$OUTPUT_PATH" | awk '{print $1}')" echo "完成: $OUTPUT_PATH ($SIZE)" else echo "完成: $OUTPUT_PATH" fi echo "Manifest: $MANIFEST_PATH" cat <