deploy.sh
bash
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a
init: musehub initial commit
Human
171 days ago
| 1 | #!/usr/bin/env bash |
| 2 | # Zero-downtime blue-green deploy for MuseHub. |
| 3 | # |
| 4 | # Strategy: |
| 5 | # Two slots — blue (port 10003) and green (port 10004). |
| 6 | # The active slot serves traffic via nginx. The inactive slot is stopped. |
| 7 | # Deploy: |
| 8 | # 1. Pull the new image from ECR (old slot keeps serving). |
| 9 | # 2. Run migrations against the live DB (before swap — forward-compatible). |
| 10 | # 3. Start the inactive slot with the new image. |
| 11 | # 4. Health-check the new slot. |
| 12 | # 5. Flip nginx to the new slot (nginx -s reload — instant, zero downtime). |
| 13 | # 6. Stop the old slot. |
| 14 | # |
| 15 | # Called by deploy/push.sh via SSM — do not run directly in production. |
| 16 | # For manual use on the instance: |
| 17 | # ECR_IMAGE=992382692655.dkr.ecr.us-east-1.amazonaws.com/musehub/musehub \ |
| 18 | # IMAGE_TAG=<tag> bash deploy/deploy.sh |
| 19 | # |
| 20 | # First-time setup: |
| 21 | # bash deploy/deploy.sh --init |
| 22 | # (Initialises .active-slot and /etc/nginx/musehub-active-port if missing) |
| 23 | |
| 24 | set -euo pipefail |
| 25 | |
| 26 | APP_DIR="/opt/musehub" |
| 27 | SLOT_FILE="$APP_DIR/.active-slot" |
| 28 | NGINX_PORT_FILE="/etc/nginx/musehub-active-port" |
| 29 | ECR_REGISTRY="992382692655.dkr.ecr.us-east-1.amazonaws.com" |
| 30 | ECR_IMAGE="${ECR_IMAGE:-${ECR_REGISTRY}/musehub/musehub}" |
| 31 | IMAGE_TAG="${IMAGE_TAG:-latest}" |
| 32 | FULL_IMAGE="${ECR_IMAGE}:${IMAGE_TAG}" |
| 33 | REGION="us-east-1" |
| 34 | HEALTH_URL_BLUE="http://127.0.0.1:10003/healthz" |
| 35 | HEALTH_URL_GREEN="http://127.0.0.1:10004/healthz" |
| 36 | HEALTH_RETRIES=30 # × 2s = 60s max wait |
| 37 | |
| 38 | cd "$APP_DIR" |
| 39 | |
| 40 | # ── Helpers ─────────────────────────────────────────────────────────────────── |
| 41 | |
| 42 | log() { echo "[deploy] $*"; } |
| 43 | die() { echo "[deploy] ERROR: $*" >&2; exit 1; } |
| 44 | |
| 45 | health_check() { |
| 46 | local url="$1" |
| 47 | local slot="$2" |
| 48 | log "Health-checking $slot at $url ..." |
| 49 | for i in $(seq 1 "$HEALTH_RETRIES"); do |
| 50 | if curl -sf --max-time 3 "$url" > /dev/null 2>&1; then |
| 51 | log "$slot is healthy (attempt $i)" |
| 52 | return 0 |
| 53 | fi |
| 54 | sleep 2 |
| 55 | done |
| 56 | die "$slot failed health check after $((HEALTH_RETRIES * 2))s" |
| 57 | } |
| 58 | |
| 59 | nginx_point_to() { |
| 60 | local port="$1" |
| 61 | echo "server 127.0.0.1:${port};" | sudo tee "$NGINX_PORT_FILE" > /dev/null |
| 62 | sudo nginx -s reload |
| 63 | log "nginx now pointing to port $port" |
| 64 | } |
| 65 | |
| 66 | # ── Init mode ───────────────────────────────────────────────────────────────── |
| 67 | |
| 68 | if [ "${1:-}" = "--init" ]; then |
| 69 | log "Init: writing .active-slot=blue and nginx upstream file" |
| 70 | echo "blue" > "$SLOT_FILE" |
| 71 | echo "server 127.0.0.1:10003;" | sudo tee "$NGINX_PORT_FILE" > /dev/null |
| 72 | sudo nginx -s reload |
| 73 | log "Done. Run 'bash deploy/deploy.sh' (with ECR_IMAGE and IMAGE_TAG set) to deploy." |
| 74 | exit 0 |
| 75 | fi |
| 76 | |
| 77 | # ── Validate required env vars ──────────────────────────────────────────────── |
| 78 | |
| 79 | [ -n "${ECR_IMAGE:-}" ] || die "ECR_IMAGE is not set." |
| 80 | [ -n "${IMAGE_TAG:-}" ] || die "IMAGE_TAG is not set." |
| 81 | |
| 82 | # ── Read active slot ────────────────────────────────────────────────────────── |
| 83 | |
| 84 | if [ ! -f "$SLOT_FILE" ]; then |
| 85 | die ".active-slot not found. Run: bash deploy/deploy.sh --init" |
| 86 | fi |
| 87 | |
| 88 | ACTIVE_SLOT=$(cat "$SLOT_FILE") |
| 89 | if [ "$ACTIVE_SLOT" = "blue" ]; then |
| 90 | NEW_SLOT="green" |
| 91 | NEW_PORT=10004 |
| 92 | OLD_CONTAINER="musehub-blue" |
| 93 | NEW_CONTAINER="musehub-green" |
| 94 | HEALTH_URL="$HEALTH_URL_GREEN" |
| 95 | else |
| 96 | NEW_SLOT="blue" |
| 97 | NEW_PORT=10003 |
| 98 | OLD_CONTAINER="musehub-green" |
| 99 | NEW_CONTAINER="musehub-blue" |
| 100 | HEALTH_URL="$HEALTH_URL_BLUE" |
| 101 | fi |
| 102 | |
| 103 | log "Image: $FULL_IMAGE" |
| 104 | log "Active slot: $ACTIVE_SLOT → deploying to: $NEW_SLOT (port $NEW_PORT)" |
| 105 | |
| 106 | # ── Step 1: Login to ECR and pull new image ─────────────────────────────────── |
| 107 | |
| 108 | log "[1/6] Pulling image from ECR..." |
| 109 | aws ecr get-login-password --region "$REGION" | \ |
| 110 | sudo docker login --username AWS --password-stdin "$ECR_REGISTRY" |
| 111 | sudo docker pull "$FULL_IMAGE" |
| 112 | log "Pull complete." |
| 113 | |
| 114 | # ── Step 2: Run migrations against the live DB ──────────────────────────────── |
| 115 | |
| 116 | log "[2/6] Running migrations..." |
| 117 | DB_PASSWORD=$(grep ^DB_PASSWORD "$APP_DIR/.env" | cut -d= -f2) |
| 118 | sudo docker run --rm \ |
| 119 | --network musehub_musehub-internal \ |
| 120 | --env-file "$APP_DIR/.env" \ |
| 121 | -e "DATABASE_URL=postgresql+asyncpg://musehub:${DB_PASSWORD}@postgres:5432/musehub" \ |
| 122 | -e SKIP_MIGRATIONS=0 \ |
| 123 | "$FULL_IMAGE" alembic upgrade head |
| 124 | log "Migrations complete." |
| 125 | |
| 126 | # ── Step 3: Start the new slot ──────────────────────────────────────────────── |
| 127 | |
| 128 | log "[3/6] Starting $NEW_SLOT on port $NEW_PORT..." |
| 129 | |
| 130 | # Remove if a failed previous deploy left it around |
| 131 | sudo docker rm -f "$NEW_CONTAINER" 2>/dev/null || true |
| 132 | |
| 133 | sudo docker run -d \ |
| 134 | --name "$NEW_CONTAINER" \ |
| 135 | --network musehub_musehub-internal \ |
| 136 | --env-file "$APP_DIR/.env" \ |
| 137 | -e "DATABASE_URL=postgresql+asyncpg://musehub:${DB_PASSWORD}@postgres:5432/musehub" \ |
| 138 | -e SKIP_MIGRATIONS=1 \ |
| 139 | -v musehub_data:/data \ |
| 140 | -p "127.0.0.1:${NEW_PORT}:10003" \ |
| 141 | --restart unless-stopped \ |
| 142 | "$FULL_IMAGE" |
| 143 | |
| 144 | # ── Step 4: Health-check the new slot ──────────────────────────────────────── |
| 145 | |
| 146 | health_check "$HEALTH_URL" "$NEW_SLOT" |
| 147 | |
| 148 | # ── Step 5: Flip nginx to the new slot (instant, zero downtime) ─────────────── |
| 149 | |
| 150 | log "[5/6] Switching nginx to $NEW_SLOT (port $NEW_PORT)..." |
| 151 | nginx_point_to "$NEW_PORT" |
| 152 | echo "$NEW_SLOT" > "$SLOT_FILE" |
| 153 | |
| 154 | # ── Step 6: Stop the old slot ──────────────────────────────────────────────── |
| 155 | |
| 156 | log "[6/6] Stopping old slot ($ACTIVE_SLOT)..." |
| 157 | sudo docker rm -f "$OLD_CONTAINER" 2>/dev/null || true |
| 158 | |
| 159 | log "" |
| 160 | log "Deploy complete. Active slot: $NEW_SLOT (port $NEW_PORT)" |
| 161 | log "Image: $FULL_IMAGE" |
File History
1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a
init: musehub initial commit
Human
171 days ago