Add production install script and migrate service
- install.sh: Interactive setup script for Linux VPS/LXC deployment - Checks prerequisites (Docker, Docker Compose, OpenSSL) - Auto-generates all secrets (Postgres, MinIO, NextAuth, encryption) - Creates .env.production with proper Docker service hostnames - Builds and starts all services via docker-compose.prod.yml - Health check loop with status reporting - Idempotent (safe to re-run) - docker-compose.prod.yml: Add migrate service - One-shot container that runs prisma db push before app starts - App depends on migrate completing successfully - Override DATABASE_URL and MINIO_ENDPOINT for Docker networking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,23 @@ services:
|
|||||||
exit 0;
|
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:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -57,8 +74,13 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
minio:
|
minio:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
env_file:
|
env_file:
|
||||||
- .env.production
|
- .env.production
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: "postgresql://${POSTGRES_USER:-drinktracker}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-drinktracker}"
|
||||||
|
MINIO_ENDPOINT: "minio"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
342
install.sh
Normal file
342
install.sh
Normal file
@@ -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" <<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 compose -f "$COMPOSE_FILE" build --no-cache
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Starting services..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
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 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 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 ""
|
||||||
Reference in New Issue
Block a user