412 lines
14 KiB
Bash
412 lines
14 KiB
Bash
#!/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"
|
|
|
|
# Helper: full docker compose command with env file
|
|
dc() {
|
|
${DOCKER_SUDO:-} docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
|
|
}
|
|
|
|
# ─── 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..."
|
|
|
|
# Detect package manager
|
|
install_pkg() {
|
|
if command -v apt-get &>/dev/null; then
|
|
sudo apt-get update -qq && sudo apt-get install -y -qq "$@"
|
|
elif command -v dnf &>/dev/null; then
|
|
sudo dnf install -y -q "$@"
|
|
elif command -v yum &>/dev/null; then
|
|
sudo yum install -y -q "$@"
|
|
elif command -v apk &>/dev/null; then
|
|
sudo apk add --quiet "$@"
|
|
else
|
|
fatal "Could not detect package manager. Install manually: $*"
|
|
fi
|
|
}
|
|
|
|
# Docker
|
|
if command -v docker &>/dev/null; then
|
|
DOCKER_VER=$(docker --version 2>/dev/null | head -1)
|
|
success "Docker found: $DOCKER_VER"
|
|
else
|
|
warn "Docker is not installed."
|
|
echo -en "${CYAN}Install Docker now? [Y/n]:${NC} "
|
|
read -r install_docker
|
|
if [[ ! "$install_docker" =~ ^[Nn] ]]; then
|
|
info "Installing Docker via official install script..."
|
|
curl -fsSL https://get.docker.com | sudo sh
|
|
sudo systemctl enable --now docker 2>/dev/null || true
|
|
# Add current user to docker group so sudo isn't needed
|
|
if ! groups | grep -q docker; then
|
|
sudo usermod -aG docker "$USER"
|
|
warn "Added $USER to docker group. You may need to log out and back in."
|
|
warn "For now, the script will use sudo for docker commands."
|
|
# Use sudo for docker for the rest of this script
|
|
DOCKER_SUDO="sudo"
|
|
fi
|
|
success "Docker installed: $(docker --version 2>/dev/null || sudo docker --version 2>/dev/null)"
|
|
else
|
|
fatal "Docker is required. Install it from https://docs.docker.com/engine/install/"
|
|
fi
|
|
fi
|
|
|
|
DOCKER_SUDO="${DOCKER_SUDO:-}"
|
|
|
|
# Docker Compose v2
|
|
if ${DOCKER_SUDO} docker compose version &>/dev/null; then
|
|
COMPOSE_VER=$(${DOCKER_SUDO} docker compose version 2>/dev/null | head -1)
|
|
success "Docker Compose found: $COMPOSE_VER"
|
|
else
|
|
warn "Docker Compose v2 not found."
|
|
echo -en "${CYAN}Install Docker Compose plugin now? [Y/n]:${NC} "
|
|
read -r install_compose
|
|
if [[ ! "$install_compose" =~ ^[Nn] ]]; then
|
|
info "Installing Docker Compose plugin..."
|
|
if command -v apt-get &>/dev/null; then
|
|
sudo apt-get update -qq && sudo apt-get install -y -qq docker-compose-plugin
|
|
else
|
|
# Manual install for non-apt systems
|
|
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '"tag_name"' | head -1 | cut -d'"' -f4)
|
|
sudo mkdir -p /usr/local/lib/docker/cli-plugins
|
|
sudo curl -SL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-$(uname -m)" \
|
|
-o /usr/local/lib/docker/cli-plugins/docker-compose
|
|
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
|
fi
|
|
success "Docker Compose installed: $(${DOCKER_SUDO} docker compose version 2>/dev/null)"
|
|
else
|
|
fatal "Docker Compose v2 is required. Install it: https://docs.docker.com/compose/install/"
|
|
fi
|
|
fi
|
|
|
|
# OpenSSL
|
|
if command -v openssl &>/dev/null; then
|
|
success "OpenSSL found"
|
|
else
|
|
warn "OpenSSL not found. Installing..."
|
|
install_pkg openssl
|
|
success "OpenSSL installed"
|
|
fi
|
|
|
|
# curl (needed for health checks)
|
|
if ! command -v curl &>/dev/null; then
|
|
warn "curl not found. Installing..."
|
|
install_pkg curl
|
|
success "curl installed"
|
|
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}@localhost:5432/${PG_DB}"
|
|
|
|
# ─── Step 4: Write .env.production ───────────────────────
|
|
|
|
echo ""
|
|
info "Writing $ENV_FILE..."
|
|
|
|
cat > "$ENV_FILE" <<ENVEOF
|
|
# ─── Database ────────────────────────────────────────────
|
|
DATABASE_URL="${DATABASE_URL}"
|
|
POSTGRES_USER="${PG_USER}"
|
|
POSTGRES_PASSWORD="${PG_PASSWORD}"
|
|
POSTGRES_DB="${PG_DB}"
|
|
|
|
# ─── NextAuth ────────────────────────────────────────────
|
|
NEXTAUTH_URL="${APP_URL}"
|
|
NEXTAUTH_SECRET="${AUTH_SECRET}"
|
|
AUTH_TRUST_HOST="true"
|
|
|
|
# ─── OAuth Providers ─────────────────────────────────────
|
|
GOOGLE_CLIENT_ID="${GOOGLE_CID}"
|
|
GOOGLE_CLIENT_SECRET="${GOOGLE_CS}"
|
|
GITHUB_CLIENT_ID="${GITHUB_CID}"
|
|
GITHUB_CLIENT_SECRET="${GITHUB_CS}"
|
|
|
|
# ─── MinIO / S3-compatible storage ───────────────────────
|
|
MINIO_ENDPOINT="localhost"
|
|
MINIO_PORT="9000"
|
|
MINIO_ACCESS_KEY="${MINIO_AK}"
|
|
MINIO_SECRET_KEY="${MINIO_SK}"
|
|
MINIO_BUCKET="drink-images"
|
|
MINIO_USE_SSL="false"
|
|
|
|
# ─── Encryption (for API key storage) ────────────────────
|
|
ENCRYPTION_KEY="${ENC_KEY}"
|
|
ENVEOF
|
|
|
|
chmod 600 "$ENV_FILE"
|
|
success "$ENV_FILE created (permissions: 600)"
|
|
fi
|
|
|
|
# ─── Step 5: Pull & Start ────────────────────────────────────
|
|
|
|
echo ""
|
|
info "Pulling Docker images..."
|
|
echo ""
|
|
|
|
dc pull
|
|
|
|
echo ""
|
|
info "Starting services..."
|
|
echo ""
|
|
|
|
dc up -d
|
|
|
|
# ─── Step 6: Health Check ───────────────────────────────────
|
|
|
|
echo ""
|
|
info "Waiting for services to become healthy..."
|
|
|
|
MAX_WAIT=90
|
|
ELAPSED=0
|
|
INTERVAL=5
|
|
|
|
while [[ $ELAPSED -lt $MAX_WAIT ]]; do
|
|
# Check database
|
|
DB_HEALTHY=$(dc ps --format json 2>/dev/null | \
|
|
grep -o '"db"[^}]*"healthy"' 2>/dev/null && echo "yes" || echo "no")
|
|
|
|
# Check app
|
|
APP_RUNNING=$(dc 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=$(dc 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=$(dc 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 --env-file $ENV_FILE -f $COMPOSE_FILE logs migrate)"
|
|
fi
|
|
|
|
if [[ "$SERVICES_UP" == "false" ]]; then
|
|
echo ""
|
|
error "Some services failed to start. Check logs:"
|
|
echo " docker compose --env-file $ENV_FILE -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 ""
|
|
DC_CMD="docker compose --env-file $ENV_FILE -f $COMPOSE_FILE"
|
|
echo -e " ${BOLD}Useful commands:${NC}"
|
|
echo " View logs: $DC_CMD logs -f"
|
|
echo " Stop: $DC_CMD down"
|
|
echo " Restart: $DC_CMD restart"
|
|
echo " Update: $DC_CMD pull && $DC_CMD up -d"
|
|
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 ""
|