#!/usr/bin/env bash # ============================================================================= # deploy.sh — Axil Accountants production deployment # Idempotent: safe for first-time setup and subsequent updates. # Usage: ./deploy.sh # ============================================================================= set -euo pipefail # ─── Constants ──────────────────────────────────────────────────────────────── PROJECT_DIR="/opt/03-business/Axil" COMPOSE_FILE="$PROJECT_DIR/docker-compose.prod.yml" ENV_FILE="$PROJECT_DIR/.env.production" DEPLOY_CONFIG="$PROJECT_DIR/.deploy-config" # Routing: Traefik (traefik-public network) — no nginx needed # ─── Colors ─────────────────────────────────────────────────────────────────── GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' log() { echo -e "${GREEN}[DEPLOY]${NC} $1"; } info() { echo -e "${CYAN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } header(){ echo -e "\n${BOLD}${CYAN}══════════════════════════════════════════════${NC}"; \ echo -e "${BOLD}${CYAN} $1${NC}"; \ echo -e "${BOLD}${CYAN}══════════════════════════════════════════════${NC}"; } # ─── Helpers ────────────────────────────────────────────────────────────────── gen_secret() { # Generate a random string of given length (default 64) # set +o pipefail to prevent SIGPIPE from tr being treated as an error local len="${1:-64}" local result set +o pipefail result=$(LC_ALL=C tr -dc 'A-Za-z0-9@#%^_=+' /dev/null; do if [ "$elapsed" -ge "$max_wait" ]; then error "Database did not become healthy within ${max_wait}s. Check logs: docker compose -f $COMPOSE_FILE logs db" fi sleep 2 elapsed=$((elapsed + 2)) done log "Database is healthy." } # ============================================================================= # Phase 0 — Prerequisites # ============================================================================= check_prerequisites() { header "Phase 0 — Prerequisites" # Must run as root or with sudo access for nginx/certbot if [ "$EUID" -ne 0 ]; then error "Please run as root or with sudo: sudo ./deploy.sh" fi command -v docker >/dev/null 2>&1 || error "Docker is not installed. Install it from https://docs.docker.com/engine/install/ubuntu/" docker compose version >/dev/null 2>&1 || error "Docker Compose plugin not found. Ensure Docker Engine >= 23." command -v git >/dev/null 2>&1 || error "Git is not installed. Run: apt-get install -y git" if ! command -v nginx >/dev/null 2>&1; then log "Nginx not found — installing..." apt-get update -qq && apt-get install -y nginx fi if ! command -v certbot >/dev/null 2>&1; then log "Certbot not found — installing..." apt-get update -qq && apt-get install -y certbot python3-certbot-nginx fi log "All prerequisites satisfied." } # ============================================================================= # Phase 1 — Code sync # ============================================================================= sync_code() { header "Phase 1 — Code sync" if [ ! -d "$PROJECT_DIR/.git" ]; then error "Project directory $PROJECT_DIR is not a git repository.\nClone the repo first:\n git clone $PROJECT_DIR" fi log "Pulling latest code..." git -C "$PROJECT_DIR" fetch --all git -C "$PROJECT_DIR" reset --hard origin/main log "Code is up to date ($(git -C "$PROJECT_DIR" rev-parse --short HEAD))." } # ============================================================================= # Phase 2 — Environment setup (first run only) # ============================================================================= setup_env() { header "Phase 2 — Environment setup" if [ -f "$ENV_FILE" ]; then log ".env.production already exists — skipping interactive setup." # Load DOMAIN and ADMIN_EMAIL for later phases if [ -f "$DEPLOY_CONFIG" ]; then # shellcheck disable=SC1090 source "$DEPLOY_CONFIG" fi return fi echo -e "\n${BOLD}First-time setup — please provide the following values.${NC}" echo -e "${YELLOW}Press Enter to accept defaults where shown.${NC}\n" # Domain prompt_with_default "Domain (without https://)" "axil.ai-impress.com" DOMAIN="$REPLY" # Admin email for Let's Encrypt prompt_with_default "Admin email (for SSL certificate alerts)" "" ADMIN_EMAIL="$REPLY" [ -z "$ADMIN_EMAIL" ] && error "Admin email is required for SSL certificate registration." # DB credentials prompt_with_default "Database user" "axil" DB_USER="$REPLY" prompt_with_default "Database name" "axil" DB_NAME="$REPLY" local suggested_pass suggested_pass="$(gen_secret 24)" echo -e "\n ${CYAN}Suggested DB password (auto-generated):${NC} ${suggested_pass}" prompt_with_default "Database password (Enter to use suggested)" "$suggested_pass" DB_PASSWORD="$REPLY" # Payload secret local suggested_secret suggested_secret="$(gen_secret 64)" echo -e "\n ${CYAN}Payload CMS secret (auto-generated 64-char):${NC}" echo -e " ${suggested_secret}" prompt_with_default "Payload secret (Enter to use auto-generated)" "$suggested_secret" PAYLOAD_SECRET="$REPLY" # Optional integrations echo -e "\n ${YELLOW}Optional — press Enter to skip any of these:${NC}" prompt_with_default "Resend API key (email service)" "" RESEND_API_KEY="$REPLY" prompt_with_default "UploadThing secret" "" UPLOADTHING_SECRET="$REPLY" prompt_with_default "UploadThing app ID" "" UPLOADTHING_APP_ID="$REPLY" # Write .env.production DATABASE_URI="postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}" NEXT_PUBLIC_SITE_URL="https://${DOMAIN}" cat > "$ENV_FILE" < "$DEPLOY_CONFIG" </dev/null 2>&1; do if [ "$elapsed" -ge "$max_wait" ]; then warn "App health check timed out — check logs: docker compose -f $COMPOSE_FILE logs app" return fi sleep 3 elapsed=$((elapsed + 3)) done log "App is running. Migrations were applied at startup via instrumentation.ts" } # ============================================================================= # Phase 6 — Traefik registration (automatic via Docker labels) # ============================================================================= setup_nginx() { header "Phase 6 — Traefik routing" # Load domain from deploy config if [ -f "$DEPLOY_CONFIG" ]; then # shellcheck disable=SC1090 source "$DEPLOY_CONFIG" fi log "Routing handled by Traefik via Docker labels." log "SSL certificate will be issued automatically via Cloudflare DNS resolver." info "Route: https://${DOMAIN:-axil.ai-impress.com} → axil-app-1:3000" } # ============================================================================= # Phase 7 — Final status # ============================================================================= print_status() { header "Deployment complete" # Load domain local domain="(unknown)" if [ -f "$DEPLOY_CONFIG" ]; then # shellcheck disable=SC1090 source "$DEPLOY_CONFIG" domain="$DOMAIN" fi echo log "Container status:" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" ps echo echo -e "${GREEN}${BOLD} Site: https://${domain}${NC}" echo -e "${GREEN}${BOLD} Admin: https://${domain}/admin${NC}" echo -e "${GREEN}${BOLD} Logs: docker compose -f $COMPOSE_FILE logs -f app${NC}" echo } # ============================================================================= # Main # ============================================================================= main() { echo -e "\n${BOLD}${GREEN}╔══════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${GREEN}║ Axil Accountants — Deploy Script ║${NC}" echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════╝${NC}" check_prerequisites sync_code setup_env build_containers start_containers run_migrations setup_nginx print_status } main