#!/usr/bin/env bash # # deploy.sh — Idempotent deployment script for PDF Accessibility Checker # # Usage: # cd /opt/pdf-accessibility && ./deploy.sh # # Architecture: # - Apache (host) serves frontend + api.php from /var/www/html/pdf-accessibility # - Docker Compose runs: PostgreSQL # - PDF processing via Google Cloud Run (synchronous HTTP call from api.php) # set -euo pipefail # ── Configuration ───────────────────────────────────────────────── REPO_DIR="$(cd "$(dirname "$0")" && pwd)" WEB_DIR="/var/www/html/pdf-accessibility" COMPOSE_FILE="docker-compose.prod.yml" ENV_FILE="${REPO_DIR}/.env" MIN_PHP_VERSION="8.0" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[DEPLOY]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERROR]${NC} $*"; } # ── Preflight Checks ───────────────────────────────────────────── log "Starting deployment from ${REPO_DIR}" # Check Docker if ! command -v docker &>/dev/null; then err "Docker is not installed. Install it first:" err " curl -fsSL https://get.docker.com | sh" err " sudo usermod -aG docker \$USER" exit 1 fi # Check Docker Compose (v2 plugin) if ! docker compose version &>/dev/null; then err "Docker Compose v2 is not available. Install it:" err " sudo apt-get install docker-compose-plugin" exit 1 fi # Check PHP if ! command -v php &>/dev/null; then warn "PHP is not installed. api.php requires PHP ${MIN_PHP_VERSION}+ with extensions:" warn " sudo apt-get install php8.2 php8.2-pgsql php8.2-curl php8.2-mbstring" else PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') log "PHP version: ${PHP_VER}" # Check required extensions MISSING_EXT="" php -m | grep -qi pgsql || MISSING_EXT="${MISSING_EXT} php-pgsql" php -m | grep -qi curl || MISSING_EXT="${MISSING_EXT} php-curl" php -m | grep -qi openssl || MISSING_EXT="${MISSING_EXT} php-openssl" if [ -n "${MISSING_EXT}" ]; then warn "Missing PHP extensions:${MISSING_EXT}" warn "Install with: sudo apt-get install${MISSING_EXT}" fi fi # ── Pull Latest Code ───────────────────────────────────────────── log "Pulling latest code..." cd "${REPO_DIR}" if [ -d .git ]; then git config core.fileMode false # Run git as the repo owner (not root) so SSH keys work REPO_OWNER=$(stat -c '%U' "${REPO_DIR}/.git") if [ "$(id -u)" = "0" ] && [ "${REPO_OWNER}" != "root" ]; then sudo -u "${REPO_OWNER}" git -C "${REPO_DIR}" fetch --all sudo -u "${REPO_OWNER}" git -C "${REPO_DIR}" reset --hard origin/$(git rev-parse --abbrev-ref HEAD) else git fetch --all git reset --hard origin/$(git rev-parse --abbrev-ref HEAD) fi log "Code updated to $(git log --oneline -1)" else warn "Not a git repo — using existing files" fi # ── Environment File ───────────────────────────────────────────── if [ ! -f "${ENV_FILE}" ]; then log "Creating .env from .env.example (first run)..." cp "${REPO_DIR}/.env.example" "${ENV_FILE}" # Override Docker hostnames with localhost for host-side PHP sed -i 's/^DB_HOST=postgres/DB_HOST=127.0.0.1/' "${ENV_FILE}" sed -i 's/^DEV_MODE=true/DEV_MODE=false/' "${ENV_FILE}" warn "Review and update ${ENV_FILE} with production values:" warn " - DB_PASSWORD (change from default!)" warn " - ANTHROPIC_API_KEY" warn " - GOOGLE_API_KEY" warn " - CLOUD_RUN_URL" warn " - GCP_SA_KEY_PATH (copy pdf-api-invoker-key.json to server)" warn " - AZURE_* settings" else log "Using existing .env file" fi # ── Build Docker Containers ────────────────────────────────────── log "Building Docker containers (using cache)..." docker compose -f "${COMPOSE_FILE}" build log "Starting/restarting Docker services..." docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans # Wait for PostgreSQL to be ready log "Waiting for PostgreSQL to be healthy..." RETRIES=30 until docker compose -f "${COMPOSE_FILE}" exec -T postgres pg_isready -U pdf_checker &>/dev/null || [ $RETRIES -eq 0 ]; do sleep 1 RETRIES=$((RETRIES - 1)) done if [ $RETRIES -eq 0 ]; then err "PostgreSQL failed to start. Check logs:" err " docker compose -f ${COMPOSE_FILE} logs postgres" exit 1 fi log "PostgreSQL is ready" # Database init.sql runs automatically on first compose up via # /docker-entrypoint-initdb.d/init.sql — no migration tool needed. # For future migrations, add numbered SQL files to db/ and apply: if [ -d "${REPO_DIR}/db/migrations" ]; then for migration in "${REPO_DIR}"/db/migrations/*.sql; do [ -f "$migration" ] || continue MIGRATION_NAME=$(basename "$migration") log "Applying migration: ${MIGRATION_NAME}" docker compose -f "${COMPOSE_FILE}" exec -T postgres \ psql -U pdf_checker -d pdf_checker -f "/dev/stdin" < "$migration" 2>/dev/null || \ warn "Migration ${MIGRATION_NAME} may have already been applied" done fi # ── Deploy Frontend Files ───────────────────────────────────────── log "Deploying frontend to ${WEB_DIR}..." # Create web directory if it doesn't exist sudo mkdir -p "${WEB_DIR}" # Clean old frontend files (but preserve uploads, results, .env, logs) log "Cleaning old frontend files..." sudo rm -f "${WEB_DIR}/index.html" "${WEB_DIR}/history.html" sudo rm -rf "${WEB_DIR}/css" "${WEB_DIR}/js" sudo rm -f "${WEB_DIR}/api.php" "${WEB_DIR}/auth.php" # Copy frontend files sudo cp "${REPO_DIR}/index.html" "${WEB_DIR}/" sudo cp "${REPO_DIR}/history.html" "${WEB_DIR}/" sudo cp -r "${REPO_DIR}/css" "${WEB_DIR}/" sudo cp -r "${REPO_DIR}/js" "${WEB_DIR}/" # Copy PHP backend files sudo cp "${REPO_DIR}/api.php" "${WEB_DIR}/" sudo cp "${REPO_DIR}/auth.php" "${WEB_DIR}/" # Copy Python scripts (needed if api.php fallback exec() is used) sudo cp "${REPO_DIR}/enterprise_pdf_checker.py" "${WEB_DIR}/" sudo cp "${REPO_DIR}/pdf_remediation.py" "${WEB_DIR}/" sudo cp "${REPO_DIR}/logger_config.py" "${WEB_DIR}/" sudo cp "${REPO_DIR}/retry_helper.py" "${WEB_DIR}/" # Copy .env for PHP (if not already there) if [ ! -f "${WEB_DIR}/.env" ]; then sudo cp "${ENV_FILE}" "${WEB_DIR}/.env" log "Copied .env to web directory" else # Update .env in web dir from repo .env sudo cp "${ENV_FILE}" "${WEB_DIR}/.env" fi # Create runtime directories sudo mkdir -p "${WEB_DIR}/uploads" "${WEB_DIR}/results" "${WEB_DIR}/logs" "${WEB_DIR}/rate_limits" # Set ownership for Apache sudo chown -R www-data:www-data "${WEB_DIR}" sudo chmod -R 755 "${WEB_DIR}" sudo chmod -R 775 "${WEB_DIR}/uploads" "${WEB_DIR}/results" "${WEB_DIR}/logs" "${WEB_DIR}/rate_limits" # ── Verify ──────────────────────────────────────────────────────── log "" log "=============================================" log " Deployment complete!" log "=============================================" log "" log "Services status:" docker compose -f "${COMPOSE_FILE}" ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" log "" log "Frontend: ${WEB_DIR}" log "Docker: PostgreSQL (127.0.0.1:1221)" log "Cloud Run: ${CLOUD_RUN_URL:-$(grep '^CLOUD_RUN_URL=' "${ENV_FILE}" 2>/dev/null | cut -d= -f2 || echo 'not set')}" log "" # Quick health check if docker compose -f "${COMPOSE_FILE}" exec -T postgres pg_isready -U pdf_checker &>/dev/null; then log "PostgreSQL: OK" fi log "" log "Reloading Apache..." sudo systemctl reload apache2 && log "Apache reloaded" || warn "Apache reload failed — run: sudo systemctl reload apache2" log "" log "Next steps (if first deploy):" log " 1. Ensure pdf-api-invoker-key.json is at the GCP_SA_KEY_PATH location" log " 2. Review ${WEB_DIR}/.env (especially CLOUD_RUN_URL and API keys)" log ""