#!/usr/bin/env bash set -euo pipefail # ───────────────────────────────────────────────────────────── # DrinkTracker — Production Install Script # ───────────────────────────────────────────────────────────── # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # No Color COMPOSE_FILE="docker-compose.prod.yml" ENV_FILE=".env.production" # ─── Helpers ───────────────────────────────────────────────── info() { echo -e "${BLUE}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERR]${NC} $*"; } fatal() { error "$*"; exit 1; } prompt_value() { local var_name="$1" local prompt_text="$2" local default_value="${3:-}" local value if [[ -n "$default_value" ]]; then echo -en "${CYAN}$prompt_text${NC} [${default_value}]: " read -r value value="${value:-$default_value}" else echo -en "${CYAN}$prompt_text${NC}: " read -r value fi eval "$var_name=\"$value\"" } prompt_secret() { local var_name="$1" local prompt_text="$2" local auto_value="$3" echo -en "${CYAN}$prompt_text${NC} [auto-generate]: " read -r value if [[ -z "$value" ]]; then value="$auto_value" echo -e " Generated: ${BOLD}${value:0:12}...${NC}" fi eval "$var_name=\"$value\"" } check_port() { local port="$1" if ss -tlnp 2>/dev/null | grep -q ":${port} " || \ netstat -tlnp 2>/dev/null | grep -q ":${port} "; then return 1 fi return 0 } # ─── Banner ────────────────────────────────────────────────── echo "" echo -e "${BOLD}${CYAN}" echo " ╔══════════════════════════════════════╗" echo " ║ DrinkTracker Install ║" echo " ║ Production Deployment via Docker ║" echo " ╚══════════════════════════════════════╝" echo -e "${NC}" echo "" # ─── Step 1: Prerequisites ────────────────────────────────── info "Checking prerequisites..." # Docker if command -v docker &>/dev/null; then DOCKER_VER=$(docker --version 2>/dev/null | head -1) success "Docker found: $DOCKER_VER" else fatal "Docker is not installed. Install it from https://docs.docker.com/engine/install/" fi # Docker Compose v2 if docker compose version &>/dev/null; then COMPOSE_VER=$(docker compose version 2>/dev/null | head -1) success "Docker Compose found: $COMPOSE_VER" else fatal "Docker Compose v2 not found. Install it: https://docs.docker.com/compose/install/" fi # OpenSSL if command -v openssl &>/dev/null; then success "OpenSSL found" else fatal "OpenSSL is not installed. Install it: apt install openssl" fi # Compose file exists if [[ ! -f "$COMPOSE_FILE" ]]; then fatal "$COMPOSE_FILE not found. Run this script from the drinktracker project root." fi # Dockerfile exists if [[ ! -f "Dockerfile" ]]; then fatal "Dockerfile not found. Run this script from the drinktracker project root." fi # Check ports PORTS_OK=true for port in 3000; do if ! check_port "$port"; then warn "Port $port is already in use" PORTS_OK=false fi done if [[ "$PORTS_OK" == "false" ]]; then echo "" warn "Some ports are in use. The install may fail if those services conflict." echo -en "${CYAN}Continue anyway? [y/N]:${NC} " read -r confirm [[ "$confirm" =~ ^[Yy] ]] || exit 1 fi echo "" success "All prerequisites met!" echo "" # ─── Step 2: Check for existing config ────────────────────── if [[ -f "$ENV_FILE" ]]; then echo "" warn "Existing $ENV_FILE found." echo -en "${CYAN}Overwrite with new config? [y/N]:${NC} " read -r overwrite if [[ ! "$overwrite" =~ ^[Yy] ]]; then info "Keeping existing $ENV_FILE. Skipping to build step..." SKIP_CONFIG=true else SKIP_CONFIG=false fi else SKIP_CONFIG=false fi # ─── Step 3: Interactive Configuration ────────────────────── if [[ "$SKIP_CONFIG" == "false" ]]; then echo -e "${BOLD}Configure your deployment:${NC}" echo "" # App URL prompt_value APP_URL "App URL (your domain or IP)" "http://localhost:3000" echo "" echo -e "${BOLD}Database:${NC}" prompt_value PG_USER "PostgreSQL username" "drinktracker" prompt_secret PG_PASSWORD "PostgreSQL password" "$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)" prompt_value PG_DB "PostgreSQL database" "drinktracker" echo "" echo -e "${BOLD}Object Storage (MinIO):${NC}" prompt_secret MINIO_AK "MinIO access key" "$(openssl rand -base64 16 | tr -d '/+=' | head -c 20)" prompt_secret MINIO_SK "MinIO secret key" "$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)" echo "" echo -e "${BOLD}Security (press Enter to auto-generate):${NC}" prompt_secret AUTH_SECRET "NextAuth secret" "$(openssl rand -base64 32)" prompt_secret ENC_KEY "Encryption key" "$(openssl rand -hex 32)" echo "" echo -e "${BOLD}OAuth Providers (optional — press Enter to skip):${NC}" prompt_value GOOGLE_CID "Google Client ID" "" prompt_value GOOGLE_CS "Google Client Secret" "" prompt_value GITHUB_CID "GitHub Client ID" "" prompt_value GITHUB_CS "GitHub Client Secret" "" # Build DATABASE_URL DATABASE_URL="postgresql://${PG_USER}:${PG_PASSWORD}@db:5432/${PG_DB}" # ─── Step 4: Write .env.production ─────────────────────── echo "" info "Writing $ENV_FILE..." cat > "$ENV_FILE" </dev/null | \ grep -o '"db"[^}]*"healthy"' 2>/dev/null && echo "yes" || echo "no") # Check app APP_RUNNING=$(docker compose -f "$COMPOSE_FILE" ps --status running --format json 2>/dev/null | \ grep -o '"app"' 2>/dev/null && echo "yes" || echo "no") if [[ "$DB_HEALTHY" == *"yes"* ]] && [[ "$APP_RUNNING" == *"yes"* ]]; then break fi echo -ne "\r Waiting... ${ELAPSED}s / ${MAX_WAIT}s" sleep $INTERVAL ELAPSED=$((ELAPSED + INTERVAL)) done echo "" # Verify services are running SERVICES_UP=true for service in db minio app; do STATUS=$(docker compose -f "$COMPOSE_FILE" ps "$service" --format "{{.Status}}" 2>/dev/null || echo "not found") if echo "$STATUS" | grep -qi "up\|running\|healthy"; then success "$service: $STATUS" else error "$service: $STATUS" SERVICES_UP=false fi done # Check migrate completed MIGRATE_STATUS=$(docker compose -f "$COMPOSE_FILE" ps migrate --format "{{.Status}}" 2>/dev/null || echo "not found") if echo "$MIGRATE_STATUS" | grep -qi "exited (0)\|completed"; then success "migrate: completed successfully" else warn "migrate: $MIGRATE_STATUS (check logs: docker compose -f $COMPOSE_FILE logs migrate)" fi if [[ "$SERVICES_UP" == "false" ]]; then echo "" error "Some services failed to start. Check logs:" echo " docker compose -f $COMPOSE_FILE logs" exit 1 fi # Quick HTTP check echo "" info "Checking app connectivity..." sleep 3 APP_URL_CHECK="${APP_URL:-http://localhost:3000}" if command -v curl &>/dev/null; then HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$APP_URL_CHECK" --max-time 10 2>/dev/null || echo "000") if [[ "$HTTP_CODE" =~ ^(200|302|307)$ ]]; then success "App responding (HTTP $HTTP_CODE)" else warn "App returned HTTP $HTTP_CODE — it may still be starting up" fi else info "curl not available, skipping HTTP check" fi # ─── Step 7: Summary ──────────────────────────────────────── echo "" echo -e "${BOLD}${GREEN}" echo " ╔══════════════════════════════════════╗" echo " ║ Installation Complete! ║" echo " ╚══════════════════════════════════════╝" echo -e "${NC}" echo "" echo -e " ${BOLD}App URL:${NC} ${APP_URL_CHECK}" echo -e " ${BOLD}Config file:${NC} $(pwd)/$ENV_FILE" echo "" echo -e " ${BOLD}Useful commands:${NC}" echo " View logs: docker compose -f $COMPOSE_FILE logs -f" echo " Stop: docker compose -f $COMPOSE_FILE down" echo " Restart: docker compose -f $COMPOSE_FILE restart" echo " Rebuild: docker compose -f $COMPOSE_FILE up -d --build" echo "" echo -e " ${BOLD}${YELLOW}Next steps:${NC}" echo " 1. Open ${APP_URL_CHECK} and create your account" echo " 2. Add your Claude or OpenAI API key in Settings" echo " 3. Set up a reverse proxy (nginx/Caddy) for HTTPS" if [[ -z "${GOOGLE_CID:-}" ]] && [[ -z "${GITHUB_CID:-}" ]]; then echo " 4. (Optional) Add OAuth providers in $ENV_FILE" fi echo ""