Rewrite deploy.sh following ppt-tool pattern
Numbered steps matching server conventions: prerequisites install, git pull with SSH auto-switch, .env validation, docker compose build, postgres + health-check waits, idempotent Apache Include management, UFW firewall. Apache step replaces old inline block with a canonical Include pointing to deploy/apache.conf. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
548a9d8ef5
commit
63818bc6e2
1 changed files with 188 additions and 85 deletions
273
deploy.sh
273
deploy.sh
|
|
@ -1,125 +1,228 @@
|
|||
#!/usr/bin/env bash
|
||||
# deploy.sh — idempotent deploy script for hp-prod-tracker
|
||||
# Usage: bash deploy.sh [--skip-pull]
|
||||
#
|
||||
# Works for both initial deployment and updates.
|
||||
# Run from /opt/hp-prod-tracker as any user with docker + git access.
|
||||
|
||||
# Idempotent — safe to run multiple times (initial deploy or update)
|
||||
# Run as normal user; uses sudo internally for apt/apache/ufw
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Config ──────────────────────────────────────────────────────────────────
|
||||
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPOSE_FILE="$APP_DIR/docker-compose.yml"
|
||||
HEALTH_URL="http://localhost:3001/api/health"
|
||||
HEALTH_RETRIES=30
|
||||
HEALTH_INTERVAL=3 # seconds between retries
|
||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; NC='\033[0m'
|
||||
info() { echo -e "${GREEN}[deploy]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||
error() { echo -e "${RED}[error]${NC} $*" >&2; }
|
||||
|
||||
# ─── Colours ─────────────────────────────────────────────────────────────────
|
||||
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||
log() { echo -e "${GREEN}[deploy]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[deploy]${NC} $*"; }
|
||||
fail() { echo -e "${RED}[deploy]${NC} $*" >&2; exit 1; }
|
||||
|
||||
# ─── Args ─────────────────────────────────────────────────────────────────────
|
||||
# ── Args ─────────────────────────────────────────────────────────────────────
|
||||
SKIP_PULL=false
|
||||
for arg in "$@"; do
|
||||
[[ "$arg" == "--skip-pull" ]] && SKIP_PULL=true
|
||||
done
|
||||
for arg in "$@"; do [[ "$arg" == "--skip-pull" ]] && SKIP_PULL=true; done
|
||||
|
||||
cd "$APP_DIR"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# ─── Pre-flight checks ───────────────────────────────────────────────────────
|
||||
log "Running pre-flight checks..."
|
||||
APACHE_CONF="/etc/apache2/sites-available/optical-dev.oliver.solutions.conf"
|
||||
INCLUDE_LINE=" Include $SCRIPT_DIR/deploy/apache.conf"
|
||||
APP_PORT=3001
|
||||
|
||||
command -v docker >/dev/null 2>&1 || fail "docker is not installed"
|
||||
docker compose version >/dev/null 2>&1 || fail "docker compose plugin is not installed"
|
||||
command -v git >/dev/null 2>&1 || fail "git is not installed"
|
||||
# ── Sudo check ───────────────────────────────────────────────────────────────
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
warn "This script needs sudo for apt/apache/ufw — you may be prompted for your password."
|
||||
fi
|
||||
|
||||
[[ -f "$APP_DIR/.env" ]] || fail ".env file not found at $APP_DIR/.env — copy .env.example and fill in the values"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 1: Prerequisites
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 1: Checking prerequisites..."
|
||||
|
||||
# Warn if critical vars are empty
|
||||
for var in AUTH_SECRET AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET AUTH_MICROSOFT_ENTRA_ID_TENANT_ID AUTH_URL; do
|
||||
val=$(grep -E "^${var}=" "$APP_DIR/.env" | cut -d= -f2- | tr -d '"' || true)
|
||||
[[ -z "$val" ]] && warn "WARNING: $var is not set in .env"
|
||||
done
|
||||
install_if_missing() {
|
||||
local cmd=$1 pkg=${2:-$1}
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
info " Installing $pkg..."
|
||||
sudo apt-get install -y "$pkg" -qq
|
||||
else
|
||||
info " $cmd: OK"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Pull latest code ────────────────────────────────────────────────────────
|
||||
if [[ "$SKIP_PULL" == false ]]; then
|
||||
log "Pulling latest code..."
|
||||
# Ensure remote uses SSH (avoids HTTPS credential prompts)
|
||||
sudo apt-get update -qq
|
||||
|
||||
install_if_missing docker docker.io
|
||||
install_if_missing git git
|
||||
install_if_missing curl curl
|
||||
install_if_missing ufw ufw
|
||||
|
||||
if ! docker compose version &>/dev/null 2>&1; then
|
||||
info " Installing docker-compose-plugin..."
|
||||
sudo apt-get install -y docker-compose-plugin -qq
|
||||
fi
|
||||
|
||||
if ! command -v apache2 &>/dev/null; then
|
||||
info " Installing apache2..."
|
||||
sudo apt-get install -y apache2 -qq
|
||||
fi
|
||||
|
||||
info " Enabling Apache modules..."
|
||||
sudo a2enmod proxy proxy_http proxy_wstunnel headers rewrite -q
|
||||
|
||||
# Ensure current user can run docker without sudo
|
||||
if ! groups | grep -q docker; then
|
||||
sudo usermod -aG docker "$USER"
|
||||
warn "Added $USER to docker group — you may need to re-login for this to take effect"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 2: Pull latest code
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 2: Pulling latest code..."
|
||||
|
||||
if [[ "$SKIP_PULL" == true ]]; then
|
||||
warn " Skipping git pull (--skip-pull)"
|
||||
else
|
||||
# Switch to SSH remote if still on HTTPS (avoids credential prompts)
|
||||
current_remote=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ "$current_remote" == https://* ]]; then
|
||||
ssh_remote=$(echo "$current_remote" \
|
||||
| sed 's|https://bitbucket.org/|git@bitbucket.org:|' \
|
||||
| sed 's|\.git$||')
|
||||
git remote set-url origin "${ssh_remote}.git"
|
||||
log "Switched remote to SSH: $(git remote get-url origin)"
|
||||
info " Switched remote to SSH: $(git remote get-url origin)"
|
||||
fi
|
||||
|
||||
git fetch origin
|
||||
LOCAL=$(git rev-parse HEAD)
|
||||
REMOTE=$(git rev-parse @{u} 2>/dev/null || echo "")
|
||||
if [[ "$LOCAL" == "$REMOTE" ]]; then
|
||||
log "Already up to date ($(git rev-parse --short HEAD))"
|
||||
info " Already up to date ($(git rev-parse --short HEAD))"
|
||||
else
|
||||
git pull --ff-only || fail "git pull failed — resolve conflicts manually then re-run"
|
||||
log "Updated to $(git rev-parse --short HEAD)"
|
||||
git pull --ff-only || { error "git pull failed — resolve conflicts manually then re-run"; exit 1; }
|
||||
info " Updated to $(git rev-parse --short HEAD) — $(git log -1 --pretty=%s)"
|
||||
fi
|
||||
else
|
||||
warn "Skipping git pull (--skip-pull)"
|
||||
fi
|
||||
|
||||
# ─── Apache config ───────────────────────────────────────────────────────────
|
||||
APACHE_CONF="/etc/apache2/sites-available/optical-dev.oliver.solutions.conf"
|
||||
INCLUDE_LINE="Include $APP_DIR/deploy/apache.conf"
|
||||
INCLUDE_MARKER="hp-prod-tracker"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 3: Environment file
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 3: Checking .env..."
|
||||
|
||||
if [[ -f "$APACHE_CONF" ]]; then
|
||||
if grep -q "$INCLUDE_MARKER" "$APACHE_CONF"; then
|
||||
log "Apache: existing hp-prod-tracker config found — replacing with canonical snippet..."
|
||||
# Remove old inline block (lines between the marker comment and next comment block)
|
||||
sudo sed -i '/# .*HP-PROD-TRACKER\|hp-prod-tracker.*3001/,/^[[:space:]]*$/{/ProxyPass.*hp-prod-tracker\|ProxyPassReverse.*hp-prod-tracker/d}' "$APACHE_CONF" 2>/dev/null || true
|
||||
# Add include if not already present
|
||||
if ! grep -qF "$INCLUDE_LINE" "$APACHE_CONF"; then
|
||||
sudo sed -i "s|</VirtualHost>| $INCLUDE_LINE\n</VirtualHost>|" "$APACHE_CONF"
|
||||
log "Apache: added Include directive"
|
||||
else
|
||||
log "Apache: Include already present — skipping"
|
||||
fi
|
||||
if [[ ! -f .env ]]; then
|
||||
if [[ -f .env.example ]]; then
|
||||
cp .env.example .env
|
||||
warn " .env was missing — copied from .env.example"
|
||||
warn " Edit .env with real secrets before continuing (Ctrl-C to abort)"
|
||||
read -rp " Press Enter to continue or Ctrl-C to abort..."
|
||||
else
|
||||
# Fresh server — just add the include before </VirtualHost>
|
||||
sudo sed -i "s|</VirtualHost>| $INCLUDE_LINE\n</VirtualHost>|" "$APACHE_CONF"
|
||||
log "Apache: added hp-prod-tracker config include"
|
||||
error " .env and .env.example both missing — cannot continue"
|
||||
exit 1
|
||||
fi
|
||||
sudo apache2ctl configtest 2>&1 | grep -v "^$" || fail "Apache config test failed — check $APACHE_CONF"
|
||||
sudo systemctl reload apache2
|
||||
log "Apache: reloaded"
|
||||
else
|
||||
warn "Apache config not found at $APACHE_CONF — skipping (add manually if needed)"
|
||||
fi
|
||||
|
||||
# ─── Deploy ──────────────────────────────────────────────────────────────────
|
||||
log "Stopping existing containers..."
|
||||
docker compose -f "$COMPOSE_FILE" down --remove-orphans
|
||||
# Warn on unset critical vars
|
||||
for var in AUTH_SECRET AUTH_MICROSOFT_ENTRA_ID_ID AUTH_MICROSOFT_ENTRA_ID_SECRET \
|
||||
AUTH_MICROSOFT_ENTRA_ID_TENANT_ID AUTH_URL DB_PASSWORD; do
|
||||
val=$(grep -E "^${var}=" .env | cut -d= -f2- | tr -d '"' || true)
|
||||
[[ -z "$val" ]] && warn " WARNING: $var is not set in .env"
|
||||
done
|
||||
|
||||
log "Building and starting containers (this may take a few minutes)..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||
info " .env OK"
|
||||
|
||||
# ─── Health check ────────────────────────────────────────────────────────────
|
||||
log "Waiting for app to be healthy..."
|
||||
attempt=0
|
||||
until curl -sf "$HEALTH_URL" >/dev/null 2>&1; do
|
||||
attempt=$((attempt + 1))
|
||||
if [[ $attempt -ge $HEALTH_RETRIES ]]; then
|
||||
fail "App did not become healthy after $((HEALTH_RETRIES * HEALTH_INTERVAL))s — check logs:\n docker compose logs app --tail 50"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 4: Build and start Docker containers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 4: Building and starting containers..."
|
||||
|
||||
docker compose down --remove-orphans
|
||||
docker compose up -d --build
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 5: Wait for Postgres
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 5: Waiting for Postgres..."
|
||||
|
||||
MAX_WAIT=30
|
||||
for i in $(seq 1 $MAX_WAIT); do
|
||||
if docker compose exec -T db pg_isready -U postgres &>/dev/null; then
|
||||
info " Postgres ready (${i}s)"
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq $MAX_WAIT ]]; then
|
||||
error " Postgres did not become ready within ${MAX_WAIT}s"
|
||||
docker compose logs db --tail 20
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 6: Wait for app health check
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 6: Waiting for app to be healthy (Prisma migrations run on startup)..."
|
||||
|
||||
HEALTH_URL="http://localhost:${APP_PORT}/api/health"
|
||||
for i in $(seq 1 40); do
|
||||
if curl -sf "$HEALTH_URL" &>/dev/null; then
|
||||
info " App healthy (${i}s)"
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq 40 ]]; then
|
||||
error " App did not become healthy within 120s"
|
||||
error " Check logs: docker compose logs app --tail 50"
|
||||
docker compose logs app --tail 30
|
||||
exit 1
|
||||
fi
|
||||
echo -n "."
|
||||
sleep $HEALTH_INTERVAL
|
||||
sleep 3
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ─── Done ────────────────────────────────────────────────────────────────────
|
||||
log "Deploy complete!"
|
||||
log " Commit : $(git rev-parse --short HEAD) — $(git log -1 --pretty=%s)"
|
||||
log " App : https://optical-dev.oliver.solutions/hp-prod-tracker"
|
||||
log " Logs : docker compose logs -f app"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 7: Apache configuration
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 7: Configuring Apache..."
|
||||
|
||||
if [[ ! -f "$APACHE_CONF" ]]; then
|
||||
warn " Apache config not found at $APACHE_CONF — skipping"
|
||||
warn " Add the following line inside <VirtualHost> manually:"
|
||||
warn " $INCLUDE_LINE"
|
||||
else
|
||||
if grep -qF "Include $SCRIPT_DIR/deploy/apache.conf" "$APACHE_CONF"; then
|
||||
info " Include already present — skipping"
|
||||
else
|
||||
# Remove old inline block (3 lines added manually before this script existed)
|
||||
sudo sed -i '/# .*HP-PROD-TRACKER\|HP-PROD-TRACKER.*3001/d' "$APACHE_CONF"
|
||||
sudo sed -i '/ProxyPass.*hp-prod-tracker.*3001/d' "$APACHE_CONF"
|
||||
sudo sed -i '/ProxyPassReverse.*hp-prod-tracker/d' "$APACHE_CONF"
|
||||
|
||||
# Add Include before closing </VirtualHost>
|
||||
sudo sed -i "s|</VirtualHost>|$INCLUDE_LINE\n</VirtualHost>|" "$APACHE_CONF"
|
||||
info " Added Include directive for deploy/apache.conf"
|
||||
fi
|
||||
|
||||
if sudo apache2ctl configtest 2>&1 | grep -q "Syntax OK"; then
|
||||
sudo systemctl reload apache2
|
||||
info " Apache reloaded OK"
|
||||
else
|
||||
error " Apache config test failed:"
|
||||
sudo apache2ctl configtest
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STEP 8: UFW Firewall
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
info "Step 8: Configuring UFW firewall..."
|
||||
|
||||
sudo ufw default deny incoming 2>/dev/null || true
|
||||
sudo ufw default allow outgoing 2>/dev/null || true
|
||||
sudo ufw allow 22/tcp
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw --force enable
|
||||
info " UFW enabled (22/tcp, 80/tcp allowed)"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Done
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
docker compose ps
|
||||
echo ""
|
||||
info "Deploy complete!"
|
||||
info " Commit : $(git rev-parse --short HEAD) — $(git log -1 --pretty=%s)"
|
||||
info " App : https://optical-dev.oliver.solutions/hp-prod-tracker"
|
||||
info " Logs : docker compose logs -f app"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue