#!/usr/bin/env bash # 构建 api / player / admin 生产镜像并导出为 tar # 用法: # ./docs/docker/build-and-export-images.sh # ./docs/docker/build-and-export-images.sh --service admin # ./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="" SERVICE="all" NO_CACHE=1 EXPORT_ONLY=0 TAG="${IMAGE_TAG:-}" default_tag() { echo "latest"; } default_output() { local svc="$1" image_tag="$2" if [[ "$svc" == "all" ]]; then echo "thebet365-images-${image_tag}.tar" else echo "thebet365-${svc}-${image_tag}.tar" fi } validate_tag() { printf '%s' "$1" | grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' || { echo "错误: 镜像 tag 不合法: $1" >&2; exit 1; } } validate_service() { case "$1" in all|api|player|admin) ;; *) echo "错误: --service 无效: $1(可选 api / player / admin / all)" >&2; exit 1 ;; esac } usage() { cat <<'EOF' 用法: docs/docker/build-and-export-images.sh [选项] 选项: --service SVC api | player | admin | all(默认 all) --tag TAG 镜像 tag(默认 latest) --use-cache 构建时使用 Docker 缓存(默认 --no-cache) --export-only 跳过构建,仅导出已有指定 tag 镜像 --output PATH 导出文件路径 -h, --help 显示帮助 示例: ./docs/docker/build-and-export-images.sh --service admin ./docs/docker/build-and-export-images.sh --service admin --export-only EOF } while [[ $# -gt 0 ]]; do case "$1" in --tag) TAG="${2:?缺少 --tag 参数值}" shift 2 ;; --service) SERVICE="${2:?缺少 --service 参数值}" 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 validate_service "$SERVICE" TAG="${TAG:-$(default_tag)}" validate_tag "$TAG" OUTPUT="${OUTPUT:-$(default_output "$SERVICE" "$TAG")}" if [[ "$SERVICE" == "all" ]]; then BUILD_SERVICES=(api player admin) else BUILD_SERVICES=("$SERVICE") fi if [[ "$EXPORT_ONLY" -eq 0 ]]; then echo "==> 构建镜像: ${BUILD_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+=("${BUILD_SERVICES[@]}") IMAGE_TAG="$TAG" docker "${BUILD_ARGS[@]}" fi OUTPUT_PATH="$OUTPUT" if [[ "$OUTPUT" != /* ]] && [[ "$OUTPUT" != [A-Za-z]:* ]]; then OUTPUT_PATH="$ROOT/$OUTPUT" fi SAVE_ARGS=() for svc in "${BUILD_SERVICES[@]}"; do SAVE_ARGS+=("thebet365-${svc}:$TAG") done echo "==> 导出镜像 ($SERVICE) -> $OUTPUT_PATH" docker save "${SAVE_ARGS[@]}" -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 "service=$SERVICE" echo "built_at=$BUILT_AT" echo "git_commit=$GIT_COMMIT" echo "git_dirty=$GIT_DIRTY" echo "tar=$(basename "$OUTPUT_PATH")" echo "tar_sha256=$CHECKSUM" for svc in "${BUILD_SERVICES[@]}"; do if [[ "$SERVICE" == "all" ]]; then echo "${svc}_image=thebet365-${svc}:$TAG" echo "${svc}_image_id=$(docker image inspect "thebet365-${svc}:$TAG" --format '{{.Id}}')" else echo "image=thebet365-${svc}:$TAG" echo "image_id=$(docker image inspect "thebet365-${svc}:$TAG" --format '{{.Id}}')" fi done } > "$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" RECREATE="$SERVICE" if [[ "$SERVICE" == "all" ]]; then RECREATE="api player admin" fi cat <