Files
drinktracker/install.sh
JP f0c745c50c Remove Google and GitHub sign-in
Both providers were registered but never usable: all four client
env vars are empty in production and no Account row has ever been
created. Email and password is the only path that has ever worked.

This also simplifies the invite gating that follows. With OAuth there
were two redemption paths, an invite token that had to survive the
provider round trip in a SameSite=Lax cookie, and an
OAuthAccountNotLinked dead end for anyone who signed up with a password
and later clicked a provider button. Now there is one path.

Account, Session and VerificationToken stay in the schema. They are
Auth.js's tables and dropping them would be a destructive migration for
no benefit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:42:40 +00:00

411 lines
13 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# ─────────────────────────────────────────────────────────────
# DrinkTracker — Production Install Script (Docker)
#
# For a fresh Docker-based install on a normal host or VPS.
#
# NOTE: the production LXC (192.168.2.169) no longer uses this — it runs the app
# natively under systemd because Docker can't work in an unprivileged LXC. To
# deploy there, use ./deploy/deploy.sh. See deploy/README.md.
# ─────────────────────────────────────────────────────────────
# 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}AI Gateway:${NC}"
prompt_value SWITCHBOARD_URL "Switchboard gateway URL" "http://192.168.2.11:8787/v1"
# 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"
# ─── 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}"
# ─── AI Gateway (Switchboard) ────────────────────────────
SWITCHBOARD_BASE_URL="${SWITCHBOARD_URL}"
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 Switchboard API key in Settings"
echo " 3. Set up a reverse proxy (nginx/Caddy) for HTTPS"
echo ""