platform-codebase/infrastructure/scripts/database/deploy-databases.sh

529 lines
15 KiB
Bash
Raw Normal View History

#!/bin/bash
set -euo pipefail
#
# Database Deployment Script - lilith-platform
#
# Deploys database services (PostgreSQL, Redis, SQLite) to target host
#
# Architecture:
# - Databases run on apricot (local machine at 10.9.0.1)
# - VPS services access via WireGuard VPN
# - Data stored on /mnt/bigdisk (network drive)
# - Docker Compose for container orchestration
# - Systemd services for auto-restart
#
# Prerequisites:
# - Docker and Docker Compose installed
# - WireGuard VPN configured
# - /mnt/bigdisk mounted and writable
#
# Usage:
# ./deploy-databases.sh [OPTIONS]
#
# Options:
# --host HOST Target host (apricot, vps, localhost) [default: apricot]
# --service SERVICE Service to deploy (postgres, redis, all) [default: all]
# --no-systemd Skip systemd service creation
# --rebuild Rebuild containers from scratch
# --dry-run Show what would be done without executing
# --help Show this help message
#
# =============================================================================
# INITIALIZATION
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export SCRIPT_LIB_DIR="${SCRIPT_DIR}/../lib"
# Load shared libraries
source "${SCRIPT_LIB_DIR}/colors.sh"
source "${SCRIPT_LIB_DIR}/logger.sh"
source "${SCRIPT_LIB_DIR}/config.sh"
source "${SCRIPT_DIR}/database-config.sh"
# Initialize logging
log_init "DB-DEPLOY"
# Parse command line arguments
TARGET_HOST="${DB_HOST:-apricot}"
TARGET_SERVICE="all"
SKIP_SYSTEMD=false
REBUILD=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case $1 in
--host)
TARGET_HOST="$2"
shift 2
;;
--service)
TARGET_SERVICE="$2"
shift 2
;;
--no-systemd)
SKIP_SYSTEMD=true
shift
;;
--rebuild)
REBUILD=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--help|-h)
grep '^#' "$0" | grep -v '#!/bin/bash' | sed 's/^# \?//'
exit 0
;;
*)
log_error "Unknown option: $1"
log_error "Use --help for usage information"
exit 1
;;
esac
done
# Set host-specific configuration
export DB_HOST="$TARGET_HOST"
case "$TARGET_HOST" in
apricot)
export DB_DEPLOYMENT_MODE="local"
export DB_HOST_IP="$APRICOT_IP"
;;
vps)
export DB_DEPLOYMENT_MODE="remote"
export DB_HOST_IP="$VPS_IP"
;;
localhost)
export DB_DEPLOYMENT_MODE="local"
export DB_HOST_IP="127.0.0.1"
;;
*)
log_error "Unknown host: $TARGET_HOST"
log_error "Valid hosts: apricot, vps, localhost"
exit 1
;;
esac
# =============================================================================
# PREREQUISITE CHECKS
# =============================================================================
check_prerequisites() {
log_step "Checking prerequisites..."
local errors=0
# Check required commands
local required_cmds=("docker")
for cmd in "${required_cmds[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
log_error "$cmd not installed"
((errors++))
fi
done
# Check docker compose (v2 plugin or standalone)
if ! docker compose version &>/dev/null && ! command -v docker-compose &>/dev/null; then
log_error "Docker Compose not available (neither 'docker compose' nor 'docker-compose')"
((errors++))
fi
# Validate database configuration
if ! db_config_validate; then
((errors++))
fi
# Check if running on correct host
if [ "$DB_DEPLOYMENT_MODE" = "local" ] && [ "$TARGET_HOST" != "localhost" ]; then
local current_host
current_host="$(hostname)"
if [ "$current_host" != "$TARGET_HOST" ]; then
log_warn "Running on '$current_host' but target is '$TARGET_HOST'"
log_warn "This may fail if the data directory is not accessible"
fi
fi
# Check data directory accessibility
if [ ! -d "$(dirname "$DB_BASE_DIR")" ]; then
log_error "Base directory not found: $(dirname "$DB_BASE_DIR")"
log_error "Is /mnt/bigdisk mounted?"
((errors++))
fi
if [ $errors -gt 0 ]; then
log_failure "Prerequisites check failed with $errors error(s)"
exit 1
fi
log_success "Prerequisites satisfied"
}
# =============================================================================
# DIRECTORY INITIALIZATION
# =============================================================================
initialize_directories() {
log_step "Initializing data directories..."
if [ "$DRY_RUN" = true ]; then
log_info "[DRY RUN] Would create directories:"
log_info " - $POSTGRES_DATA_DIR"
log_info " - $REDIS_DATA_DIR"
log_info " - $SQLITE_DATA_DIR"
log_info " - $BACKUP_BASE_DIR"
log_info " - $LOG_DIR"
return
fi
db_config_init_dirs
# Set proper permissions
chmod 700 "$POSTGRES_DATA_DIR" || true
chmod 700 "$REDIS_DATA_DIR" || true
log_success "Directories initialized"
}
# =============================================================================
# DOCKER NETWORK SETUP
# =============================================================================
setup_docker_network() {
log_step "Setting up Docker network..."
if [ "$DRY_RUN" = true ]; then
log_info "[DRY RUN] Would create network: $DOCKER_NETWORK"
return
fi
# Check if network exists
if docker network inspect "$DOCKER_NETWORK" &>/dev/null; then
log_info "Network $DOCKER_NETWORK already exists"
else
log_info "Creating network $DOCKER_NETWORK..."
docker network create \
--driver bridge \
--subnet "${VPN_SUBNET}" \
"$DOCKER_NETWORK"
log_success "Network created"
fi
}
# =============================================================================
# DOCKER COMPOSE FILE GENERATION
# =============================================================================
generate_docker_compose() {
log_step "Generating docker-compose.yml..."
local compose_file="${SCRIPT_DIR}/docker-compose.databases.yml"
if [ "$DRY_RUN" = true ]; then
log_info "[DRY RUN] Would generate: $compose_file"
return
fi
cat > "$compose_file" <<EOF
version: '3.8'
services:
EOF
# PostgreSQL service
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "postgres" ]; then
cat >> "$compose_file" <<EOF
postgres:
image: postgres:${POSTGRES_VERSION}
container_name: lilith-db-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- ${POSTGRES_DATA_DIR}:/var/lib/postgresql/data
ports:
- "${DB_HOST_IP}:${POSTGRES_PORT}:5432"
networks:
- ${DOCKER_NETWORK}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: ${HEALTH_CHECK_INTERVAL}s
timeout: 5s
retries: ${HEALTH_CHECK_RETRIES}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
EOF
fi
# Redis service
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "redis" ]; then
cat >> "$compose_file" <<EOF
redis:
image: redis:${REDIS_VERSION}
container_name: lilith-db-redis
restart: unless-stopped
command: redis-server --maxmemory ${REDIS_MAXMEMORY} --maxmemory-policy ${REDIS_MAXMEMORY_POLICY} --appendonly yes
volumes:
- ${REDIS_DATA_DIR}:/data
ports:
- "${DB_HOST_IP}:${REDIS_PORT}:6379"
networks:
- ${DOCKER_NETWORK}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: ${HEALTH_CHECK_INTERVAL}s
timeout: 5s
retries: ${HEALTH_CHECK_RETRIES}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
EOF
fi
# Network definition
cat >> "$compose_file" <<EOF
networks:
${DOCKER_NETWORK}:
external: true
EOF
log_success "Docker Compose file generated: $compose_file"
}
# =============================================================================
# SERVICE DEPLOYMENT
# =============================================================================
deploy_services() {
log_step "Deploying database services..."
local compose_file="${SCRIPT_DIR}/docker-compose.databases.yml"
if [ "$DRY_RUN" = true ]; then
log_info "[DRY RUN] Would run: docker compose -f $compose_file up -d"
return
fi
# Stop existing services if rebuilding
if [ "$REBUILD" = true ]; then
log_info "Stopping existing services..."
docker compose -f "$compose_file" down || true
fi
# Start services
log_info "Starting services..."
if [ "$REBUILD" = true ]; then
docker compose -f "$compose_file" up -d --build --force-recreate
else
docker compose -f "$compose_file" up -d
fi
# Wait for health checks
log_info "Waiting for services to become healthy..."
sleep 5
local max_wait=60
local elapsed=0
while [ $elapsed -lt $max_wait ]; do
local all_healthy=true
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "postgres" ]; then
if ! docker exec lilith-db-postgres pg_isready -U "$POSTGRES_USER" &>/dev/null; then
all_healthy=false
fi
fi
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "redis" ]; then
if ! docker exec lilith-db-redis redis-cli ping &>/dev/null; then
all_healthy=false
fi
fi
if [ "$all_healthy" = true ]; then
log_success "All services are healthy"
return
fi
sleep 2
elapsed=$((elapsed + 2))
done
log_warn "Some services may not be fully healthy yet"
}
# =============================================================================
# SYSTEMD SERVICE CREATION
# =============================================================================
create_systemd_service() {
if [ "$SKIP_SYSTEMD" = true ]; then
log_info "Skipping systemd service creation (--no-systemd)"
return
fi
log_step "Creating systemd service..."
if [ "$DRY_RUN" = true ]; then
log_info "[DRY RUN] Would create systemd service: ${SYSTEMD_SERVICE_PREFIX}.service"
return
fi
local service_file="${SYSTEMD_SERVICE_DIR}/${SYSTEMD_SERVICE_PREFIX}.service"
local compose_file="${SCRIPT_DIR}/docker-compose.databases.yml"
# Create systemd service file
sudo tee "$service_file" > /dev/null <<EOF
[Unit]
Description=Lilith Platform Database Services
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=${SCRIPT_DIR}
ExecStart=/usr/bin/docker compose -f ${compose_file} up -d
ExecStop=/usr/bin/docker compose -f ${compose_file} down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable service
sudo systemctl daemon-reload
sudo systemctl enable "${SYSTEMD_SERVICE_PREFIX}.service"
log_success "Systemd service created and enabled"
log_info "Service file: $service_file"
log_info "To manage: sudo systemctl {start|stop|restart|status} ${SYSTEMD_SERVICE_PREFIX}"
}
# =============================================================================
# VERIFICATION
# =============================================================================
verify_deployment() {
log_step "Verifying deployment..."
if [ "$DRY_RUN" = true ]; then
log_info "[DRY RUN] Would verify services"
return
fi
local errors=0
# Check PostgreSQL
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "postgres" ]; then
log_info "Testing PostgreSQL connection..."
if docker exec lilith-db-postgres pg_isready -U "$POSTGRES_USER" &>/dev/null; then
log_success "PostgreSQL is ready"
log_info " Connection: postgres://${POSTGRES_USER}@${DB_HOST_IP}:${POSTGRES_PORT}/${POSTGRES_DB}"
else
log_failure "PostgreSQL is not responding"
((errors++))
fi
fi
# Check Redis
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "redis" ]; then
log_info "Testing Redis connection..."
if docker exec lilith-db-redis redis-cli ping | grep -q PONG; then
log_success "Redis is ready"
log_info " Connection: redis://${DB_HOST_IP}:${REDIS_PORT}"
else
log_failure "Redis is not responding"
((errors++))
fi
fi
if [ $errors -gt 0 ]; then
log_failure "Verification failed with $errors error(s)"
return 1
fi
log_success "All services verified successfully"
}
# =============================================================================
# MAIN DEPLOYMENT WORKFLOW
# =============================================================================
main() {
log_banner "Lilith Platform - Database Deployment"
echo ""
log_info "Target Host: $TARGET_HOST ($DB_HOST_IP)"
log_info "Service: $TARGET_SERVICE"
log_info "Data Directory: $DB_BASE_DIR"
log_info "Mode: $DB_DEPLOYMENT_MODE"
if [ "$DRY_RUN" = true ]; then
log_warn "DRY RUN MODE - No changes will be made"
fi
echo ""
# Configuration summary
db_config_summary
echo ""
# Confirmation prompt
if [ "$DRY_RUN" = false ]; then
read -p "Proceed with deployment? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Deployment cancelled"
exit 0
fi
fi
# Execute deployment steps
check_prerequisites
initialize_directories
setup_docker_network
generate_docker_compose
deploy_services
create_systemd_service
verify_deployment
log_banner "Deployment Complete!"
if [ "$DRY_RUN" = false ]; then
echo ""
log_info "Next steps:"
log_info " 1. Configure application .env files with database URLs"
log_info " 2. Run database migrations"
log_info " 3. Configure VPN access from VPS (if needed)"
log_info ""
log_info "Database connections:"
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "postgres" ]; then
log_info " PostgreSQL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST_IP}:${POSTGRES_PORT}/${POSTGRES_DB}"
fi
if [ "$TARGET_SERVICE" = "all" ] || [ "$TARGET_SERVICE" = "redis" ]; then
log_info " Redis: redis://${DB_HOST_IP}:${REDIS_PORT}"
fi
echo ""
log_info "Management commands:"
log_info " Status: ./status-databases.sh"
log_info " Backup: ./backup-databases.sh"
log_info " Logs: docker compose -f ${SCRIPT_DIR}/docker-compose.databases.yml logs -f"
fi
}
main "$@"