diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 800bf74..eae68d8 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -45,6 +45,23 @@ services: exit 0; " + migrate: + image: node:20-alpine + depends_on: + db: + condition: service_healthy + working_dir: /app + volumes: + - ./prisma:/app/prisma + - ./package.json:/app/package.json + - ./package-lock.json:/app/package-lock.json + env_file: + - .env.production + environment: + DATABASE_URL: "postgresql://${POSTGRES_USER:-drinktracker}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-drinktracker}" + command: sh -c "npm install prisma @prisma/client --silent && npx prisma db push --skip-generate --accept-data-loss" + restart: "no" + app: build: context: . @@ -57,8 +74,13 @@ services: condition: service_healthy minio: condition: service_healthy + migrate: + condition: service_completed_successfully env_file: - .env.production + environment: + DATABASE_URL: "postgresql://${POSTGRES_USER:-drinktracker}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-drinktracker}" + MINIO_ENDPOINT: "minio" volumes: pgdata: diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..0b9993a --- /dev/null +++ b/install.sh @@ -0,0 +1,342 @@ +#!/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 ""