#!/bin/bash set -euo pipefail # ============================================================ # Memento - Docker Deploy Script (macOS / Linux) # ============================================================ # Usage: # ./scripts/deploy-docker.sh # Full setup # ./scripts/deploy-docker.sh --env-only # Generate .env.docker only # ./scripts/deploy-docker.sh --build # Build + deploy (no env setup) # ./scripts/deploy-docker.sh --stop # Stop all containers # ./scripts/deploy-docker.sh --logs # Show logs # ============================================================ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" ENV_FILE="$PROJECT_DIR/.env.docker" # 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' info() { echo -e "${BLUE}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } step() { echo -e "\n${CYAN}${BOLD}==>${NC} ${BOLD}$*${NC}"; } # ----------------------------------------------------------- # Check dependencies # ----------------------------------------------------------- check_deps() { step "Checking dependencies..." if ! command -v docker &>/dev/null; then error "Docker is not installed. macOS: https://docs.docker.com/desktop/install/mac-install/ Linux: https://docs.docker.com/engine/install/" fi if ! docker info &>/dev/null 2>&1; then error "Docker daemon is not running. Start Docker first." fi if docker compose version &>/dev/null 2>&1; then COMPOSE_CMD="docker compose" elif command -v docker-compose &>/dev/null; then COMPOSE_CMD="docker-compose" else error "Docker Compose is not installed. Install: https://docs.docker.com/compose/install/" fi if ! command -v openssl &>/dev/null; then warn "openssl not found. Will use /dev/urandom for secrets." fi ok "All dependencies met" } # ----------------------------------------------------------- # Helper: generate random secret # ----------------------------------------------------------- gen_secret() { if command -v openssl &>/dev/null; then openssl rand -base64 32 2>/dev/null else head -c 32 /dev/urandom | base64 fi } gen_password() { if command -v openssl &>/dev/null; then openssl rand -hex 16 2>/dev/null else head -c 16 /dev/urandom | hexdump -v -e '/1 "%02x"' fi } # ----------------------------------------------------------- # Ask a question with default # ----------------------------------------------------------- ask() { local prompt="$1" local default="${2:-}" local var="$3" local result if [ -n "$default" ]; then echo -ne " ${CYAN}?${NC} ${prompt} [${default}]: " else echo -ne " ${CYAN}?${NC} ${prompt}: " fi read -r result result="${result:-$default}" eval "$var=\"\$result\"" } ask_required() { local prompt="$1" local var="$2" local result while true; do echo -ne " ${CYAN}?${NC} ${prompt} (required): " read -r result if [ -n "$result" ]; then eval "$var=\"\$result\"" return fi echo -e " ${RED}This field is required.${NC}" done } ask_email() { local prompt="$1" local var="$2" local result while true; do echo -ne " ${CYAN}?${NC} ${prompt} (required): " read -r result if [ -n "$result" ] && echo "$result" | grep -qE '^[^@]+@[^@]+\.[^@]+$'; then eval "$var=\"\$result\"" return fi echo -e " ${RED}Please enter a valid email address.${NC}" done } # ----------------------------------------------------------- # Generate .env.docker interactively # ----------------------------------------------------------- generate_env() { step "Configuring Memento for Docker deployment" if [ -f "$ENV_FILE" ]; then warn ".env.docker already exists." echo -ne " ${CYAN}?${NC} Overwrite? [y/N]: " read -r confirm [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { info "Keeping existing .env.docker"; return 0; } fi echo "" echo -e "${BOLD} This wizard will guide you through the configuration.${NC}" echo -e "${BOLD} Press Enter to accept defaults in [brackets].${NC}" echo "" # ---- Core ---- step "Core configuration" local url="http://localhost:3000" ask "App URL (NEXTAUTH_URL)" "$url" url local secret secret=$(gen_secret) info "Auto-generated NEXTAUTH_SECRET" local admin_email ask_email "Admin email (first user with this email becomes ADMIN)" admin_email local allow_reg="true" ask "Allow public registration" "$allow_reg" allow_reg # Normalize case "$allow_reg" in [Yy]|[Yy]es|true|1) allow_reg="true" ;; *) allow_reg="false" ;; esac # ---- PostgreSQL ---- step "PostgreSQL configuration" local pg_port="5433" local pg_db="memento" local pg_user="memento" local pg_pass pg_pass=$(gen_password) ask "PostgreSQL exposed port" "$pg_port" pg_port ask "PostgreSQL database name" "$pg_db" pg_db ask "PostgreSQL username" "$pg_user" pg_user info "Auto-generated secure PostgreSQL password" # ---- AI Provider ---- step "AI Provider configuration" echo " Choose your AI provider:" echo " 1) OpenAI" echo " 2) Ollama (local, requires Ollama container)" echo " 3) OpenRouter" echo " 4) Custom OpenAI-compatible (Groq, Together, Mistral, etc.)" echo " 5) Skip AI configuration" echo "" local ai_choice="5" ask "Choice" "$ai_choice" ai_choice local ai_tags_provider="" ai_tags_model="" local ai_embed_provider="" ai_embed_model="" local ai_chat_provider="" ai_chat_model="" local openai_key="" custom_key="" custom_url="" ollama_url="" case "$ai_choice" in 1) ai_tags_provider="openai"; ai_tags_model="gpt-4o-mini" ai_embed_provider="openai"; ai_embed_model="text-embedding-3-small" ai_chat_provider="openai"; ai_chat_model="gpt-4o-mini" ask_required "OpenAI API Key" openai_key ;; 2) ai_tags_provider="ollama"; ai_tags_model="granite4:latest" ai_embed_provider="ollama"; ai_embed_model="embeddinggemma:latest" ai_chat_provider="ollama"; ai_chat_model="granite4:latest" ollama_url="http://ollama:11434" ask "Ollama base URL" "$ollama_url" ollama_url ;; 3) ai_tags_provider="custom"; ai_tags_model="google/gemma-3-27b-it" ai_embed_provider="custom"; ai_embed_model="text-embedding-3-small" ai_chat_provider="custom"; ai_chat_model="google/gemma-3-27b-it" custom_url="https://openrouter.ai/api/v1" ask_required "OpenRouter API Key" custom_key ask "OpenRouter base URL" "$custom_url" custom_url ;; 4) ai_tags_provider="custom" ai_embed_provider="custom" ai_chat_provider="custom" ask_required "Custom provider API Key" custom_key ask_required "Custom provider base URL" custom_url ask "Model for tags" "gpt-4o-mini" ai_tags_model ask "Model for embeddings" "text-embedding-3-small" ai_embed_model ask "Model for chat" "gpt-4o-mini" ai_chat_model ;; 5) info "Skipping AI configuration. You can configure it later in the admin panel." ;; *) error "Invalid choice" ;; esac # ---- MCP Server ---- step "MCP Server configuration" echo " The MCP server allows external tools (Claude, Cline, N8N) to interact with Memento." echo "" local mcp_enable="yes" ask "Enable MCP server?" "$mcp_enable" mcp_enable case "$mcp_enable" in [Nn]|[Nn]o|false|0) mcp_enable="false" ;; *) mcp_enable="true" ;; esac local mcp_port="3001" local mcp_server_mode="sse" local mcp_server_url="" local mcp_api_key="" if [ "$mcp_enable" = "true" ]; then ask "MCP server port" "$mcp_port" mcp_port ask "MCP server mode (sse or stdio)" "$mcp_server_mode" mcp_server_mode mcp_server_url="${url%:*}:${mcp_port}" local mcp_auth="yes" ask "Require MCP authentication?" "$mcp_auth" mcp_auth case "$mcp_auth" in [Nn]|[Nn]o|false|0) ;; *) mcp_api_key=$(gen_password) info "Auto-generated MCP API key" ;; esac fi # ---- Email ---- step "Email configuration (optional, needed for password reset)" echo " Choose an email provider:" echo " 1) Resend" echo " 2) SMTP" echo " 3) Skip" echo "" local email_choice="3" ask "Choice" "$email_choice" email_choice local resend_key="" smtp_host="" smtp_port="" smtp_user="" smtp_pass="" smtp_from="" case "$email_choice" in 1) ask_required "Resend API Key" resend_key ;; 2) ask_required "SMTP Host" smtp_host ask "SMTP Port" "587" smtp_port ask_required "SMTP Username" smtp_user ask_required "SMTP Password" smtp_pass ask_required "SMTP From email" smtp_from ;; esac # ---- Ollama container ---- local enable_ollama="no" if [ "$ai_choice" = "2" ]; then enable_ollama="yes" else step "Ollama container (optional)" ask "Also start Ollama container?" "$enable_ollama" enable_ollama case "$enable_ollama" in [Yy]|[Yy]es|true|1) enable_ollama="yes" ;; *) enable_ollama="no" ;; esac fi # ---- Web Search (optional) ---- step "Web Search configuration (optional)" echo " Choose a web search provider:" echo " 1) SearXNG (self-hosted)" echo " 2) Brave Search" echo " 3) Skip" echo "" local search_choice="3" ask "Choice" "$search_choice" search_choice local searxng_url="" brave_key="" case "$search_choice" in 1) ask_required "SearXNG URL" searxng_url ;; 2) ask_required "Brave Search API Key" brave_key ;; esac # ---- Write .env.docker ---- step "Writing .env.docker" cat > "$ENV_FILE" << EOF # ============================================================================= # Memento - Docker Environment (auto-generated by deploy-docker.sh) # ============================================================================= # Generated on $(date '+%Y-%m-%d %H:%M:%S') # ============================================================================= # Core NEXTAUTH_URL="${url}" NEXTAUTH_SECRET="${secret}" ADMIN_EMAIL="${admin_email}" ALLOW_REGISTRATION="${allow_reg}" # PostgreSQL POSTGRES_PORT=${pg_port} POSTGRES_DB=${pg_db} POSTGRES_USER=${pg_user} POSTGRES_PASSWORD="${pg_pass}" EOF # AI config if [ "$ai_choice" != "5" ]; then cat >> "$ENV_FILE" << EOF # AI - Tags AI_PROVIDER_TAGS=${ai_tags_provider} AI_MODEL_TAGS="${ai_tags_model}" # AI - Embeddings AI_PROVIDER_EMBEDDING=${ai_embed_provider} AI_MODEL_EMBEDDING="${ai_embed_model}" # AI - Chat AI_PROVIDER_CHAT=${ai_chat_provider} AI_MODEL_CHAT="${ai_chat_model}" EOF if [ -n "$openai_key" ]; then echo "OPENAI_API_KEY=\"${openai_key}\"" >> "$ENV_FILE" fi if [ -n "$custom_key" ]; then echo "CUSTOM_OPENAI_API_KEY=\"${custom_key}\"" >> "$ENV_FILE" echo "CUSTOM_OPENAI_BASE_URL=\"${custom_url}\"" >> "$ENV_FILE" fi if [ -n "$ollama_url" ]; then echo "OLLAMA_BASE_URL=\"${ollama_url}\"" >> "$ENV_FILE" fi fi # MCP config if [ "$mcp_enable" = "true" ]; then cat >> "$ENV_FILE" << EOF # MCP Server MCP_MODE="${mcp_server_mode}" MCP_PORT="${mcp_port}" MCP_SERVER_MODE="${mcp_server_mode}" MCP_SERVER_URL="${mcp_server_url}" EOF if [ -n "$mcp_api_key" ]; then echo "MCP_API_KEY=\"${mcp_api_key}\"" >> "$ENV_FILE" fi else cat >> "$ENV_FILE" << EOF # MCP Server (disabled) MCP_SERVER_MODE="disabled" EOF fi # Email config if [ -n "$resend_key" ]; then cat >> "$ENV_FILE" << EOF # Email - Resend RESEND_API_KEY="${resend_key}" EOF fi if [ -n "$smtp_host" ]; then cat >> "$ENV_FILE" << EOF # Email - SMTP SMTP_HOST="${smtp_host}" SMTP_PORT="${smtp_port}" SMTP_USER="${smtp_user}" SMTP_PASS="${smtp_pass}" SMTP_FROM="${smtp_from}" EOF fi # Web Search if [ -n "$searxng_url" ]; then cat >> "$ENV_FILE" << EOF # Web Search - SearXNG WEB_SEARCH_PROVIDER="searxng" SEARXNG_URL="${searxng_url}" EOF fi if [ -n "$brave_key" ]; then cat >> "$ENV_FILE" << EOF # Web Search - Brave WEB_SEARCH_PROVIDER="brave" BRAVE_SEARCH_API_KEY="${brave_key}" EOF fi ok ".env.docker created at $ENV_FILE" echo "" echo -e " ${BOLD}Configuration summary:${NC}" echo " URL: $url" echo " Admin email: $admin_email" echo " Registration: $allow_reg" echo " PostgreSQL user: $pg_user / db: $pg_db" echo " AI provider: $([ "$ai_choice" = "5" ] && echo "skipped" || echo "$ai_tags_provider")" echo " MCP server: $([ "$mcp_enable" = "true" ] && echo "enabled ($mcp_server_mode)" || echo "disabled")" echo " Email: $([ "$email_choice" = "3" ] && echo "skipped" || echo "configured")" echo " Ollama container: $enable_ollama" echo " (sensitive values are hidden)" } # ----------------------------------------------------------- # Build and deploy # ----------------------------------------------------------- deploy() { [ -f "$ENV_FILE" ] || error ".env.docker not found. Run: $0 --env-only" cd "$PROJECT_DIR" # Determine Ollama profile local ollama_profile="" if grep -q 'OLLAMA_BASE_URL="http://ollama' "$ENV_FILE" 2>/dev/null || \ grep -q 'AI_PROVIDER_TAGS=ollama' "$ENV_FILE" 2>/dev/null; then ollama_profile="--profile ollama" fi step "Building Docker containers..." $COMPOSE_CMD build --parallel 2>&1 step "Starting containers..." $COMPOSE_CMD up -d $ollama_profile 2>&1 step "Waiting for services to be healthy..." local retries=0 local max_retries=45 while [ $retries -lt $max_retries ]; do local unhealthy unhealthy=$($COMPOSE_CMD ps --format '{{.Status}}' 2>/dev/null | grep -c -v "healthy\|Up" || true) if [ "$unhealthy" -eq 0 ]; then break fi retries=$((retries + 1)) sleep 2 printf "." done echo "" if [ $retries -ge $max_retries ]; then warn "Some containers may still be starting. Check status with: $0 --logs" fi step "Waiting for database migrations (handled by entrypoint)..." # The docker-entrypoint.sh runs prisma migrate deploy automatically on every start. # It handles: fresh DB, existing DB with migrations, and P3005 baseline recovery. # No manual migration step needed here. echo "" echo -e "${GREEN}${BOLD}============================================${NC}" echo -e "${GREEN}${BOLD} Memento is running!${NC}" echo -e "${GREEN}${BOLD}============================================${NC}" echo "" $COMPOSE_CMD ps echo "" local app_url app_url=$(grep '^NEXTAUTH_URL=' "$ENV_FILE" | cut -d= -f2 | tr -d '"') local admin_email admin_email=$(grep '^ADMIN_EMAIL=' "$ENV_FILE" | cut -d= -f2 | tr -d '"') echo -e " ${BOLD}App:${NC} $app_url" echo -e " ${BOLD}Admin:${NC} Register with $admin_email to get admin access" echo -e " ${BOLD}MCP:${NC} $([ -n "$(grep '^MCP_SERVER_MODE=sse' "$ENV_FILE" 2>/dev/null)" ] && echo "$app_url:$(grep '^MCP_PORT=' "$ENV_FILE" | cut -d= -f2 | tr -d '"')/mcp" || echo "disabled")" echo "" echo -e " ${CYAN}Useful commands:${NC}" echo " $0 --logs View logs" echo " $0 --stop Stop containers" echo " $0 --env-only Reconfigure .env.docker" } # ----------------------------------------------------------- # Stop containers # ----------------------------------------------------------- stop_containers() { cd "$PROJECT_DIR" step "Stopping containers..." $COMPOSE_CMD down ok "Containers stopped" } # ----------------------------------------------------------- # Show logs # ----------------------------------------------------------- show_logs() { cd "$PROJECT_DIR" $COMPOSE_CMD logs -f --tail=100 } # ----------------------------------------------------------- # Main # ----------------------------------------------------------- check_deps ACTION="${1:---full}" case "$ACTION" in --env-only) generate_env ;; --build) deploy ;; --full) generate_env echo "" deploy ;; --stop) stop_containers ;; --logs) show_logs ;; *) echo "Usage: $0 [--env-only | --build | --full | --stop | --logs]" echo "" echo " --env-only Generate .env.docker interactively" echo " --build Build and start containers (requires existing .env.docker)" echo " --full Generate .env.docker + build + deploy (default)" echo " --stop Stop all containers" echo " --logs Show container logs" exit 1 ;; esac