Files
gatehouse-api/deploy/deploy.sh
T
2026-06-20 11:06:27 +07:00

135 lines
5.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Rolling deploy: drains each api container from nginx before replacing it,
# so users see zero downtime. Job workers are restarted last without draining.
#
# Usage:
# ./deploy.sh <image-tag> e.g. ./deploy.sh a1b2c3d
#
# Overridable via env vars (defaults below are gatehouse-api):
# IMAGE_NAME docker image name
# SVC1/SVC2 compose service names
# SVC1_PORT host port for SVC1
# SVC2_PORT host port for SVC2
# HEALTH_PATH HTTP path for health check
# NGINX_CONF path to nginx site config
# COMPOSE_DIR directory with docker-compose.yml and .env
#
# The runner user needs passwordless sudo for nginx:
# echo "runner ALL=(ALL) NOPASSWD: /usr/sbin/nginx, /bin/systemctl reload nginx" \
# | sudo tee /etc/sudoers.d/runner-nginx
set -euo pipefail
TAG="${1:?Usage: deploy.sh <image-tag> (e.g. deploy.sh a1b2c3d)}"
IMAGE_NAME="${IMAGE_NAME:-gatehouse-api}"
NGINX_CONF="${NGINX_CONF:-/etc/nginx/conf.d/gatehouse-api.conf}"
COMPOSE_DIR="${COMPOSE_DIR:-/opt/gatehouse-api}"
SVC1="${SVC1:-api1}"
SVC2="${SVC2:-api2}"
SVC1_PORT="${SVC1_PORT:-5000}"
SVC2_PORT="${SVC2_PORT:-5001}"
HEALTH_PATH="${HEALTH_PATH:-/api/health}"
HEALTH_RETRIES=18 # 18 × 5 s = 90 s max
HEALTH_INTERVAL=5
# ── helpers ───────────────────────────────────────────────────────────────────
log() { echo "[$(date '+%H:%M:%S')] $*"; }
die() { log "ERROR: $*" >&2; exit 1; }
step() { log ""; log "── $* ──"; }
health_check() {
local port=$1 label=$2 attempt=0
log "Waiting for ${label} on :${port}${HEALTH_PATH} ..."
while (( attempt < HEALTH_RETRIES )); do
if curl -sf "http://127.0.0.1:${port}${HEALTH_PATH}" -o /dev/null; then
log "${label} healthy"
return 0
fi
attempt=$(( attempt + 1 ))
log " [${attempt}/${HEALTH_RETRIES}] not ready, retrying in ${HEALTH_INTERVAL}s"
sleep "${HEALTH_INTERVAL}"
done
log "ERROR: ${label} failed health check after $((HEALTH_RETRIES * HEALTH_INTERVAL))s"
return 1
}
get_service_tag() {
docker compose ps -q "$1" 2>/dev/null \
| xargs -r docker inspect --format '{{.Config.Image}}' 2>/dev/null \
| cut -d: -f2
}
rollback() {
local service=$1 port=$2 old_tag=$3
if [[ -z "${old_tag}" ]]; then
nginx_restore "${port}"
die "Deploy aborted — ${service} failed health check, no previous tag to roll back to"
fi
log "Rolling back ${service} to ${old_tag}..."
IMAGE_TAG="${old_tag}" docker compose up -d --no-deps --force-recreate "${service}"
nginx_restore "${port}"
die "Deploy aborted — ${service} rolled back to ${old_tag}"
}
nginx_drain() {
local port=$1
sudo sed -i "s|server 127.0.0.1:${port};|server 127.0.0.1:${port} down;|" "$NGINX_CONF"
sudo nginx -t 2>&1 | tail -2
sudo nginx -s reload
log "nginx: drained :${port}"
}
nginx_restore() {
local port=$1
sudo sed -i "s|server 127.0.0.1:${port} down;|server 127.0.0.1:${port};|" "$NGINX_CONF"
sudo nginx -t 2>&1 | tail -2
sudo nginx -s reload
log "nginx: restored :${port}"
}
# ── pre-flight ────────────────────────────────────────────────────────────────
cd "${COMPOSE_DIR}"
pwd; ls -la
log "Deploying ${IMAGE_NAME}:${TAG}"
docker image inspect "${IMAGE_NAME}:${TAG}" > /dev/null 2>&1 \
|| die "Image ${IMAGE_NAME}:${TAG} not found locally — build it first."
# ── roll SVC1 ─────────────────────────────────────────────────────────────────
step "${SVC1}${TAG} (traffic: ${SVC2} only)"
old_svc1=$(get_service_tag "${SVC1}")
nginx_drain "${SVC1_PORT}"
log "Waiting 15s for in-flight requests to drain..."
sleep 15
IMAGE_TAG="${TAG}" docker compose up -d --no-deps --force-recreate "${SVC1}"
health_check "${SVC1_PORT}" "${SVC1}" || rollback "${SVC1}" "${SVC1_PORT}" "${old_svc1}"
nginx_restore "${SVC1_PORT}"
# ── roll SVC2 ─────────────────────────────────────────────────────────────────
step "${SVC2}${TAG} (traffic: ${SVC1} only)"
old_svc2=$(get_service_tag "${SVC2}")
nginx_drain "${SVC2_PORT}"
log "Waiting 15s for in-flight requests to drain..."
sleep 15
IMAGE_TAG="${TAG}" docker compose up -d --no-deps --force-recreate "${SVC2}"
health_check "${SVC2_PORT}" "${SVC2}" || rollback "${SVC2}" "${SVC2_PORT}" "${old_svc2}"
nginx_restore "${SVC2_PORT}"
# ── job workers ───────────────────────────────────────────────────────────────
step "job workers → ${TAG}"
IMAGE_TAG="${TAG}" docker compose up -d --no-deps --force-recreate zerotier-reconciler mfa-compliance
# ── done ──────────────────────────────────────────────────────────────────────
log ""
log "Deploy complete ✓ ${IMAGE_NAME}:${TAG}"