Files
drinktracker/install.sh
JP Scott 44c70e7825 Add auto-install for Docker and dependencies in install script
- Automatically installs Docker via get.docker.com if not found
- Installs Docker Compose plugin if missing
- Installs OpenSSL and curl if missing
- Detects package manager (apt, dnf, yum, apk)
- Handles docker group permissions for current user
- Falls back to sudo for docker commands when needed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:28:01 -07:00

405 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"
# ─── 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}@db: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}"
# ─── 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="minio"
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: Build & Start ──────────────────────────────────
echo ""
info "Building Docker images (this may take a few minutes on first run)..."
echo ""
${DOCKER_SUDO} docker compose -f "$COMPOSE_FILE" build --no-cache
echo ""
info "Starting services..."
echo ""
${DOCKER_SUDO} docker compose -f "$COMPOSE_FILE" 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=$(${DOCKER_SUDO} docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null | \
grep -o '"db"[^}]*"healthy"' 2>/dev/null && echo "yes" || echo "no")
# Check app
APP_RUNNING=$(${DOCKER_SUDO} 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_SUDO} 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_SUDO} 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_SUDO} docker compose -f $COMPOSE_FILE logs migrate)"
fi
if [[ "$SERVICES_UP" == "false" ]]; then
echo ""
error "Some services failed to start. Check logs:"
echo " ${DOCKER_SUDO} 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 ""