#!/usr/bin/env bash set -Eeuo pipefail TAG="${1:?Usage: deploy.sh TAG [--build-only]}" MODE="${2:-}" [[ "$TAG" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$ ]] || { echo 'Invalid release tag' >&2; exit 1; } [[ -z "$MODE" || "$MODE" == '--build-only' ]] || { echo 'Unknown mode' >&2; exit 1; } BASE_DIR=/www/wwwroot/sw2.fuhua91.com RELEASE="$BASE_DIR/releases/$TAG" BACKEND_IMAGE="sw2/backend:$TAG" FRONTEND_IMAGE="sw2/frontend:$TAG" NETWORK=sw2-net NAMES=(sw2-backend sw2-frontend) command -v docker >/dev/null command -v curl >/dev/null test -f "$RELEASE/backend/Dockerfile" test -f "$RELEASE/frontend/Dockerfile" docker build --build-arg "VERSION=$TAG" -t "$BACKEND_IMAGE" "$RELEASE/backend" docker build --build-arg "VERSION=$TAG" -t "$FRONTEND_IMAGE" "$RELEASE/frontend" docker run --rm "$BACKEND_IMAGE" /app/kratos-admin -h >/dev/null docker run --rm --add-host sw2-backend:127.0.0.1 "$FRONTEND_IMAGE" nginx -t if [[ "$MODE" == '--build-only' ]]; then exit 0; fi test -s "$BASE_DIR/config/config.yaml" test -f "$BASE_DIR/runtime.env" install -d -o 1000 -g 1000 -m 0750 "$BASE_DIR/config" "$BASE_DIR/log" "$BASE_DIR/uploads" chown 1000:1000 "$BASE_DIR/config/config.yaml" chmod 0600 "$BASE_DIR/config/config.yaml" "$BASE_DIR/runtime.env" if ! docker network inspect "$NETWORK" >/dev/null 2>&1; then docker network create --label com.sw2.application=sw2 "$NETWORK" fi managed() { [[ "$(docker inspect --format '{{index .Config.Labels "com.sw2.application"}}' "$1")" == sw2 ]] } exists() { docker inspect "$1" >/dev/null 2>&1; } for name in "${NAMES[@]}"; do for candidate in "$name" "$name-previous"; do if exists "$candidate"; then managed "$candidate" || { echo "Refusing to replace unmanaged container: $candidate" >&2; exit 1; } fi done done moved=() created=() rollback() { result=$? trap - ERR set +eu for name in "${created[@]}"; do docker rm -f "$name" >/dev/null 2>&1; done for name in "${moved[@]}"; do if exists "$name-previous"; then docker rename "$name-previous" "$name"; fi docker start "$name" done echo 'Deployment failed; previous containers restored where present.' >&2 exit "$result" } trap rollback ERR for name in "${NAMES[@]}"; do if exists "$name-previous"; then docker rm "$name-previous" >/dev/null; fi if exists "$name"; then moved+=("$name") docker stop -t 30 "$name" >/dev/null docker rename "$name" "$name-previous" fi done created+=(sw2-backend) docker run -d --name sw2-backend --label com.sw2.application=sw2 \ --restart unless-stopped --network "$NETWORK" \ --log-opt max-size=20m --log-opt max-file=3 \ -p 127.0.0.1:9602:8000 --env-file "$BASE_DIR/runtime.env" \ -v "$BASE_DIR/config:/data/conf" -v "$BASE_DIR/log:/app/logs" -v "$BASE_DIR/uploads:/app/uploads" \ "$BACKEND_IMAGE" ready=false for attempt in $(seq 1 45); do if curl -fsS --max-time 3 http://127.0.0.1:9602/health >/dev/null 2>&1; then ready=true; break; fi sleep 2 done [[ "$ready" == true ]] || { echo 'Backend health check failed' >&2; false; } created+=(sw2-frontend) docker run -d --name sw2-frontend --label com.sw2.application=sw2 \ --restart unless-stopped --network "$NETWORK" \ --log-opt max-size=20m --log-opt max-file=3 \ -p 127.0.0.1:9601:80 "$FRONTEND_IMAGE" ready=false for attempt in $(seq 1 15); do if curl -fsS --max-time 3 http://127.0.0.1:9601/api/health >/dev/null 2>&1; then ready=true; break; fi sleep 2 done [[ "$ready" == true ]] || { echo 'Frontend API proxy health check failed' >&2; false; } trap - ERR printf '%s\n' "$TAG" > "$BASE_DIR/current-release" install -m 0750 "$RELEASE/deploy.sh" "$BASE_DIR/deploy.sh" docker ps --filter label=com.sw2.application=sw2 --format '{{.Names}} {{.Image}} {{.Status}} {{.Ports}}'