- Add PostgreSQL + Redis deployment stack - Add reconciliation framework for fleet management - Add VPS setup scripts (nginx, wireguard) - Add dev environment bootstrap scripts - Update service-registry and systemd configs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
528 lines
15 KiB
Bash
Executable file
528 lines
15 KiB
Bash
Executable file
#!/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 "$@"
|