#!/bin/bash # ============================================================================= # Life OS Infrastructure Setup Script # Server: defiant-01 (46.225.166.142) - Ubuntu 24.04 LTS # Run as: root # Purpose: Repeatable setup of Life OS DEV and PROD environments on Hetzner VM # ============================================================================= # USAGE: # Full run: bash lifeos-setup.sh # Single section: bash lifeos-setup.sh network # bash lifeos-setup.sh database # bash lifeos-setup.sh app # bash lifeos-setup.sh nginx # bash lifeos-setup.sh ssl # ============================================================================= set -e # Exit on any error # --- Configuration ----------------------------------------------------------- LIFEOS_NETWORK="lifeos_network" DB_CONTAINER="lifeos-db" DB_IMAGE="postgres:16-alpine" DB_PROD="lifeos_prod" DB_DEV="lifeos_dev" APP_PROD_CONTAINER="lifeos-prod" APP_DEV_CONTAINER="lifeos-dev" APP_PROD_PORT="8002" APP_DEV_PORT="8003" DOMAIN_PROD="lifeos.invixiom.com" DOMAIN_DEV="lifeos-dev.invixiom.com" CERT_PATH="/etc/letsencrypt/live/kasm.invixiom.com" LIFEOS_DIR="/opt/lifeos" # DB passwords - change these before running DB_PROD_PASSWORD="CHANGE_ME_PROD" DB_DEV_PASSWORD="CHANGE_ME_DEV" # ----------------------------------------------------------------------------- section() { echo "" echo "==============================================" echo " $1" echo "==============================================" } # ============================================================================= # SECTION 1: Docker Network # ============================================================================= setup_network() { section "SECTION 1: Docker Network" if docker network ls | grep -q "$LIFEOS_NETWORK"; then echo "Network $LIFEOS_NETWORK already exists, skipping." else docker network create "$LIFEOS_NETWORK" echo "Created network: $LIFEOS_NETWORK" fi docker network ls | grep lifeos } # ============================================================================= # SECTION 2: PostgreSQL Container # ============================================================================= setup_database() { section "SECTION 2: PostgreSQL Container" if docker ps -a | grep -q "$DB_CONTAINER"; then echo "Container $DB_CONTAINER already exists, skipping creation." else docker run -d \ --name "$DB_CONTAINER" \ --network "$LIFEOS_NETWORK" \ --restart unless-stopped \ -e POSTGRES_PASSWORD="$DB_PROD_PASSWORD" \ -v lifeos_db_data:/var/lib/postgresql/data \ "$DB_IMAGE" echo "Created container: $DB_CONTAINER" echo "Waiting for Postgres to be ready..." sleep 5 fi # Create PROD database docker exec "$DB_CONTAINER" psql -U postgres -tc \ "SELECT 1 FROM pg_database WHERE datname='$DB_PROD'" | grep -q 1 || \ docker exec "$DB_CONTAINER" psql -U postgres \ -c "CREATE DATABASE $DB_PROD;" # Create DEV database docker exec "$DB_CONTAINER" psql -U postgres -tc \ "SELECT 1 FROM pg_database WHERE datname='$DB_DEV'" | grep -q 1 || \ docker exec "$DB_CONTAINER" psql -U postgres \ -c "CREATE DATABASE $DB_DEV;" # Create DEV user with separate password docker exec "$DB_CONTAINER" psql -U postgres -tc \ "SELECT 1 FROM pg_roles WHERE rolname='lifeos_dev'" | grep -q 1 || \ docker exec "$DB_CONTAINER" psql -U postgres \ -c "CREATE USER lifeos_dev WITH PASSWORD '$DB_DEV_PASSWORD';" docker exec "$DB_CONTAINER" psql -U postgres \ -c "GRANT ALL PRIVILEGES ON DATABASE $DB_DEV TO lifeos_dev;" echo "Databases ready:" docker exec "$DB_CONTAINER" psql -U postgres -c "\l" | grep lifeos } # ============================================================================= # SECTION 3: Application Directory Structure # ============================================================================= setup_app_dirs() { section "SECTION 3: Application Directory Structure" mkdir -p "$LIFEOS_DIR/prod" mkdir -p "$LIFEOS_DIR/dev" mkdir -p "$LIFEOS_DIR/prod/files" mkdir -p "$LIFEOS_DIR/dev/files" echo "Created directory structure:" ls -la "$LIFEOS_DIR" } # ============================================================================= # SECTION 4: Nginx Configuration # (Run after app containers are up and SSL cert is expanded) # ============================================================================= setup_nginx() { section "SECTION 4: Nginx Virtual Hosts" # Add Life OS PROD and DEV server blocks to existing invixiom config # We append to the existing file - kasm/files/code blocks remain untouched if grep -q "$DOMAIN_PROD" /etc/nginx/sites-available/invixiom; then echo "Nginx config for $DOMAIN_PROD already exists, skipping." else cat >> /etc/nginx/sites-available/invixiom << EOF server { listen 443 ssl; server_name $DOMAIN_PROD; ssl_certificate $CERT_PATH/fullchain.pem; ssl_certificate_key $CERT_PATH/privkey.pem; location / { proxy_pass http://127.0.0.1:$APP_PROD_PORT; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } server { listen 443 ssl; server_name $DOMAIN_DEV; ssl_certificate $CERT_PATH/fullchain.pem; ssl_certificate_key $CERT_PATH/privkey.pem; location / { proxy_pass http://127.0.0.1:$APP_DEV_PORT; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } EOF echo "Added Nginx config for $DOMAIN_PROD and $DOMAIN_DEV" fi # Add new domains to the HTTP->HTTPS redirect block # (manual step - see notes below) echo "" echo "NOTE: Also add $DOMAIN_PROD and $DOMAIN_DEV to the server_name line" echo "in the HTTP redirect block at the top of /etc/nginx/sites-available/invixiom" # Test and reload nginx -t && systemctl reload nginx echo "Nginx reloaded." } # ============================================================================= # SECTION 5: SSL Certificate Expansion # (Expand Let's Encrypt cert to cover new subdomains) # ============================================================================= setup_ssl() { section "SECTION 5: SSL Certificate Expansion" certbot certonly --nginx \ -d kasm.invixiom.com \ -d files.invixiom.com \ -d code.invixiom.com \ -d "$DOMAIN_PROD" \ -d "$DOMAIN_DEV" \ --expand systemctl reload nginx echo "SSL cert expanded and Nginx reloaded." } # ============================================================================= # MAIN # ============================================================================= case "${1:-all}" in network) setup_network ;; database) setup_database ;; dirs) setup_app_dirs ;; nginx) setup_nginx ;; ssl) setup_ssl ;; all) setup_network setup_database setup_app_dirs # nginx and ssl run after app containers are built echo "" echo "==============================================" echo " Sections 1-3 complete." echo " Next: build Life OS Docker image, then run:" echo " bash lifeos-setup.sh ssl" echo " bash lifeos-setup.sh nginx" echo "==============================================" ;; *) echo "Unknown section: $1" echo "Usage: bash lifeos-setup.sh [network|database|dirs|nginx|ssl|all]" exit 1 ;; esac # ============================================================================= # SECTION 6: Data Migration (reference - already completed) # Documents the steps used to migrate Supabase prod data to lifeos_prod # ============================================================================= setup_migration_notes() { section "SECTION 6: Data Migration Notes" echo "Migration completed 2026-02-27" echo "" echo "Steps used:" echo " 1. Exported data from Supabase using Python supabase client (supabase_export.py)" echo " 2. Applied schema: docker exec -i lifeos-db psql -U postgres -d lifeos_prod < lifeos_schema_r0.sql" echo " 3. Imported data: docker exec -i lifeos-db psql -U postgres -d lifeos_prod < lifeos_export.sql" echo "" echo "Final row counts:" docker exec lifeos-db psql -U postgres -d lifeos_prod -c " SELECT 'domains' as table_name, count(*) FROM domains UNION ALL SELECT 'areas', count(*) FROM areas UNION ALL SELECT 'projects', count(*) FROM projects UNION ALL SELECT 'tasks', count(*) FROM tasks UNION ALL SELECT 'notes', count(*) FROM notes UNION ALL SELECT 'links', count(*) FROM links UNION ALL SELECT 'files', count(*) FROM files UNION ALL SELECT 'daily_focus', count(*) FROM daily_focus UNION ALL SELECT 'capture', count(*) FROM capture UNION ALL SELECT 'context_types', count(*) FROM context_types; " echo "" echo "Note: files table is empty - Supabase Storage paths are obsolete." echo "File uploads start fresh in Release 1 using local storage." }