Add comprehensive Docker setup with PostgreSQL, Redis, ChromaDB, and Qdrant

- Enhanced install.py with Docker detection and automatic service setup
- Added docker-compose.services.yml for standalone database services
- Created docker-services.sh management script for easy service control
- Added DOCKER.md documentation with complete setup instructions
- Updated requirements.txt for Python 3.13 compatibility
- Added multiple test scripts and configuration files
- Enhanced collaborative creative projects with proper database integration
- Fixed SQLAlchemy metadata field conflicts in database models
- Added comprehensive quickstart and testing guides

Services now available:
- PostgreSQL with Docker
- Redis with Docker
- ChromaDB vector database
- Qdrant vector database (recommended)
- PgAdmin for database administration

The setup script now automatically detects Docker and offers streamlined
installation with one-command service deployment.
This commit is contained in:
2025-07-05 10:01:41 -07:00
parent 1b586582d4
commit 824b118e93
29 changed files with 2738 additions and 92 deletions

View File

@@ -24,6 +24,8 @@ class FishbowlSetup:
self.python_executable = None
self.config = {}
self.docker_available = False
self.use_docker_services = False
def print_header(self):
"""Print welcome header"""
@@ -129,6 +131,18 @@ class FishbowlSetup:
self.print_info("Please install Git from https://git-scm.com/")
sys.exit(1)
# Check for Docker and Docker Compose
self.docker_available = False
try:
docker_result = subprocess.run(["docker", "--version"], check=True, capture_output=True, text=True)
compose_result = subprocess.run(["docker", "compose", "version"], check=True, capture_output=True, text=True)
self.print_success(f"Docker found: {docker_result.stdout.strip()}")
self.print_success(f"Docker Compose found: {compose_result.stdout.strip()}")
self.docker_available = True
except (subprocess.CalledProcessError, FileNotFoundError):
self.print_warning("Docker/Docker Compose not found.")
self.print_info("Install Docker Desktop for easier PostgreSQL/Redis setup: https://docker.com/")
# Check for Node.js (optional, for frontend)
try:
result = subprocess.run(["node", "--version"], check=True, capture_output=True, text=True)
@@ -160,13 +174,33 @@ class FishbowlSetup:
self.print_info("Creating virtual environment...")
try:
subprocess.run([sys.executable, "-m", "venv", str(self.venv_path)],
check=True, capture_output=True)
# Try with sys.executable first
result = subprocess.run([sys.executable, "-m", "venv", str(self.venv_path)],
check=True, capture_output=True, text=True)
self.print_success("Virtual environment created successfully")
self.python_executable = self.get_venv_python()
except subprocess.CalledProcessError as e:
self.print_error(f"Failed to create virtual environment: {e}")
sys.exit(1)
self.print_warning(f"Failed with {sys.executable}: {e}")
# Try with python3 as fallback
try:
self.print_info("Trying with python3...")
result = subprocess.run(["python3", "-m", "venv", str(self.venv_path)],
check=True, capture_output=True, text=True)
self.print_success("Virtual environment created successfully")
self.python_executable = self.get_venv_python()
except subprocess.CalledProcessError as e2:
self.print_error(f"Failed to create virtual environment: {e2}")
self.print_info("Error details:")
if hasattr(e2, 'stderr') and e2.stderr:
print(f" {e2.stderr}")
self.print_info("Try manually: python3 -m venv venv")
if self.ask_yes_no("Continue without virtual environment? (not recommended)", False):
self.python_executable = sys.executable
self.print_warning("Proceeding without virtual environment")
else:
sys.exit(1)
def get_venv_python(self) -> str:
"""Get path to Python executable in virtual environment"""
@@ -245,10 +279,29 @@ class FishbowlSetup:
"""Collect database configuration"""
print("\n🗄️ Database Configuration")
db_choices = ["SQLite (simple, file-based)", "PostgreSQL (recommended for production)"]
if self.docker_available:
db_choices = [
"SQLite (simple, file-based)",
"PostgreSQL with Docker (recommended)",
"PostgreSQL (manual setup)"
]
else:
db_choices = ["SQLite (simple, file-based)", "PostgreSQL (manual setup)"]
db_choice = self.ask_choice("Choose database type:", db_choices, 0)
if "PostgreSQL" in db_choice:
if "PostgreSQL with Docker" in db_choice:
self.config["database"] = {
"type": "postgresql",
"host": "localhost",
"port": 5432,
"name": "discord_fishbowl",
"username": "postgres",
"password": self.ask_question("Database password", "fishbowl_password"),
"use_docker": True
}
self.use_docker_services = True
elif "PostgreSQL" in db_choice:
self.config["database"] = {
"type": "postgresql",
"host": self.ask_question("PostgreSQL host", "localhost"),
@@ -256,11 +309,13 @@ class FishbowlSetup:
"name": self.ask_question("Database name", "discord_fishbowl"),
"username": self.ask_question("Database username", "postgres"),
"password": self.ask_question("Database password", secret=True),
"use_docker": False
}
else:
self.config["database"] = {
"type": "sqlite",
"path": "data/fishbowl.db"
"path": "data/fishbowl.db",
"use_docker": False
}
self.print_info("SQLite database will be created automatically")
@@ -270,36 +325,85 @@ class FishbowlSetup:
self.print_info("Redis is used for caching and pub/sub messaging")
if self.ask_yes_no("Use Redis? (recommended)", True):
self.config["redis"] = {
"enabled": True,
"host": self.ask_question("Redis host", "localhost"),
"port": int(self.ask_question("Redis port", "6379")),
"password": self.ask_question("Redis password (leave empty if none)", "", required=False),
"db": int(self.ask_question("Redis database number", "0")),
}
if self.docker_available and hasattr(self, 'use_docker_services') and self.use_docker_services:
if self.ask_yes_no("Use Redis with Docker?", True):
self.config["redis"] = {
"enabled": True,
"host": "localhost",
"port": 6379,
"password": self.ask_question("Redis password", "redis_password"),
"db": 0,
"use_docker": True
}
else:
self.config["redis"] = {
"enabled": True,
"host": self.ask_question("Redis host", "localhost"),
"port": int(self.ask_question("Redis port", "6379")),
"password": self.ask_question("Redis password (leave empty if none)", "", required=False),
"db": int(self.ask_question("Redis database number", "0")),
"use_docker": False
}
else:
self.config["redis"] = {
"enabled": True,
"host": self.ask_question("Redis host", "localhost"),
"port": int(self.ask_question("Redis port", "6379")),
"password": self.ask_question("Redis password (leave empty if none)", "", required=False),
"db": int(self.ask_question("Redis database number", "0")),
"use_docker": False
}
else:
self.config["redis"] = {"enabled": False}
self.config["redis"] = {"enabled": False, "use_docker": False}
def collect_vector_db_config(self):
"""Collect vector database configuration"""
print("\n🔍 Vector Database Configuration")
self.print_info("Vector database stores character memories and enables semantic search")
vector_choices = ["Qdrant (recommended)", "In-memory (for testing)", "Skip vector database"]
if self.docker_available:
vector_choices = [
"ChromaDB with Docker (simple)",
"Qdrant with Docker (recommended)",
"Qdrant (manual setup)",
"In-memory (for testing)",
"Skip vector database"
]
else:
vector_choices = ["Qdrant (manual setup)", "In-memory (for testing)", "Skip vector database"]
vector_choice = self.ask_choice("Choose vector database:", vector_choices, 0)
if "Qdrant" in vector_choice:
if "ChromaDB with Docker" in vector_choice:
self.config["vector_db"] = {
"type": "chromadb",
"host": "localhost",
"port": 8000,
"use_docker": True
}
self.use_docker_services = True
elif "Qdrant with Docker" in vector_choice:
self.config["vector_db"] = {
"type": "qdrant",
"host": "localhost",
"port": 6333,
"collection_name": self.ask_question("Collection name", "fishbowl_memories"),
"use_docker": True
}
self.use_docker_services = True
elif "Qdrant" in vector_choice:
self.config["vector_db"] = {
"type": "qdrant",
"host": self.ask_question("Qdrant host", "localhost"),
"port": int(self.ask_question("Qdrant port", "6333")),
"collection_name": self.ask_question("Collection name", "fishbowl_memories"),
"use_docker": False
}
elif "In-memory" in vector_choice:
self.config["vector_db"] = {"type": "memory"}
self.config["vector_db"] = {"type": "memory", "use_docker": False}
self.print_warning("In-memory vector database won't persist between restarts")
else:
self.config["vector_db"] = {"type": "none"}
self.config["vector_db"] = {"type": "none", "use_docker": False}
self.print_warning("Without vector database, character memories will be limited")
def collect_ai_config(self):
@@ -620,6 +724,91 @@ python -m src.admin.app
self.print_warning("Character initialization failed - you can run it manually later")
self.print_info("Run: python -m scripts.init_characters")
def setup_docker_services(self):
"""Set up Docker services if enabled"""
if not self.use_docker_services:
return
self.print_section("Setting Up Docker Services")
# Create .env file for Docker Compose
docker_env_content = self.create_docker_env_content()
docker_env_file = self.project_root / ".env.docker"
with open(docker_env_file, "w") as f:
f.write(docker_env_content)
self.print_success("Docker environment file created")
# Start Docker services
if self.ask_yes_no("Start Docker services now?", True):
try:
self.print_info("Starting PostgreSQL and Redis containers...")
subprocess.run([
"docker", "compose", "-f", "docker-compose.services.yml",
"--env-file", ".env.docker", "up", "-d"
], check=True, cwd=self.project_root)
self.print_success("Docker services started successfully")
# Wait for services to be ready
self.print_info("Waiting for services to be ready...")
import time
time.sleep(10)
# Check service health
self.check_docker_services()
except subprocess.CalledProcessError as e:
self.print_error(f"Failed to start Docker services: {e}")
self.print_info("You can start them manually with: docker compose -f docker-compose.services.yml up -d")
else:
self.print_info("To start services later: docker compose -f docker-compose.services.yml --env-file .env.docker up -d")
def create_docker_env_content(self) -> str:
"""Create Docker environment file content"""
lines = [
"# Docker Compose Environment Variables",
"# Generated by Discord Fishbowl setup script",
"",
]
if self.config["database"].get("use_docker"):
lines.extend([
f"DB_PASSWORD={self.config['database']['password']}",
"",
])
if self.config["redis"].get("use_docker"):
lines.extend([
f"REDIS_PASSWORD={self.config['redis']['password']}",
"",
])
lines.extend([
"# Optional PgAdmin credentials (if using --profile admin)",
"PGADMIN_PASSWORD=admin123",
"",
])
return "\n".join(lines)
def check_docker_services(self):
"""Check if Docker services are running"""
try:
result = subprocess.run([
"docker", "compose", "-f", "docker-compose.services.yml", "ps", "--services", "--filter", "status=running"
], check=True, capture_output=True, text=True, cwd=self.project_root)
running_services = result.stdout.strip().split('\n') if result.stdout.strip() else []
if "postgres" in running_services:
self.print_success("PostgreSQL is running")
if "redis" in running_services:
self.print_success("Redis is running")
if "chromadb" in running_services:
self.print_success("ChromaDB is running")
except subprocess.CalledProcessError:
self.print_warning("Could not check Docker service status")
def print_completion_summary(self):
"""Print setup completion summary"""
self.print_section("🎉 Setup Complete!")
@@ -693,6 +882,9 @@ python -m src.admin.app
self.collect_configuration()
self.create_config_files()
# Docker services setup
self.setup_docker_services()
# Database and scripts
self.setup_database_schema()
self.create_startup_scripts()