#!/bin/bash # Docker-specific functions for Mosaic Stack installer # Handles Docker Compose operations, health checks, and service management # shellcheck source=lib/platform.sh source "${BASH_SOURCE[0]%/*}/platform.sh" # ============================================================================ # Docker Compose Helpers # ============================================================================ # Get the docker compose command (handles both plugin and standalone) docker_compose_cmd() { if docker compose version &>/dev/null; then echo "docker compose" elif command -v docker-compose &>/dev/null; then echo "docker-compose" else return 1 fi } # Run docker compose with all arguments docker_compose() { local cmd cmd=$(docker_compose_cmd) || { echo -e "${ERROR}Error: Docker Compose not available${NC}" return 1 } # shellcheck disable=SC2086 $cmd "$@" } # ============================================================================ # Service Management # ============================================================================ # Pull all images defined in docker-compose.yml docker_pull_images() { local compose_file="${1:-docker-compose.yml}" local env_file="${2:-.env}" echo -e "${WARN}→${NC} Pulling Docker images..." if [[ -f "$env_file" ]]; then docker_compose -f "$compose_file" --env-file "$env_file" pull else docker_compose -f "$compose_file" pull fi } # Start services with Docker Compose docker_compose_up() { local compose_file="${1:-docker-compose.yml}" local env_file="${2:-.env}" local profiles="${3:-}" local detached="${4:-true}" echo -e "${WARN}→${NC} Starting services..." local args=("-f" "$compose_file") if [[ -f "$env_file" ]]; then args+=("--env-file" "$env_file") fi if [[ -n "$profiles" ]]; then args+=("--profile" "$profiles") fi if [[ "$detached" == "true" ]]; then args+=("up" "-d") else args+=("up") fi docker_compose "${args[@]}" } # Stop services docker_compose_down() { local compose_file="${1:-docker-compose.yml}" local env_file="${2:-.env}" echo -e "${WARN}→${NC} Stopping services..." if [[ -f "$env_file" ]]; then docker_compose -f "$compose_file" --env-file "$env_file" down else docker_compose -f "$compose_file" down fi } # Restart services docker_compose_restart() { local compose_file="${1:-docker-compose.yml}" local env_file="${2:-.env}" echo -e "${WARN}→${NC} Restarting services..." if [[ -f "$env_file" ]]; then docker_compose -f "$compose_file" --env-file "$env_file" restart else docker_compose -f "$compose_file" restart fi } # ============================================================================ # Health Checks # ============================================================================ # Wait for a container to be healthy wait_for_healthy_container() { local container_name="$1" local timeout="${2:-120}" local interval="${3:-5}" echo -e "${INFO}i${NC} Waiting for ${INFO}$container_name${NC} to be healthy..." local elapsed=0 while [[ $elapsed -lt $timeout ]]; do local status status=$(docker inspect --format='{{.State.Health.Status}}' "$container_name" 2>/dev/null || echo "not_found") case "$status" in healthy) echo -e "${SUCCESS}✓${NC} $container_name is healthy" return 0 ;; unhealthy) echo -e "${ERROR}✗${NC} $container_name is unhealthy" return 1 ;; not_found) echo -e "${WARN}→${NC} Container $container_name not found" return 1 ;; esac sleep "$interval" ((elapsed += interval)) done echo -e "${ERROR}✗${NC} Timeout waiting for $container_name to be healthy" return 1 } # Wait for multiple containers to be healthy wait_for_healthy_containers() { local containers=("$@") local timeout="${containers[-1]}" unset 'containers[-1]' for container in "${containers[@]}"; do if ! wait_for_healthy_container "$container" "$timeout"; then return 1 fi done return 0 } # Wait for a service to respond on a port wait_for_service() { local host="$1" local port="$2" local name="$3" local timeout="${4:-60}" echo -e "${INFO}i${NC} Waiting for ${INFO}$name${NC} at $host:$port..." local elapsed=0 while [[ $elapsed -lt $timeout ]]; do if docker run --rm --network host alpine:latest nc -z "$host" "$port" 2>/dev/null; then echo -e "${SUCCESS}✓${NC} $name is responding" return 0 fi sleep 2 ((elapsed += 2)) done echo -e "${ERROR}✗${NC} Timeout waiting for $name" return 1 } # ============================================================================ # Container Status # ============================================================================ # Get container status get_container_status() { local container_name="$1" docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null || echo "not_found" } # Check if container is running is_container_running() { local container_name="$1" local status status=$(get_container_status "$container_name") [[ "$status" == "running" ]] } # List all Mosaic containers list_mosaic_containers() { docker ps -a --filter "name=mosaic-" --format "{{.Names}}\t{{.Status}}" } # Get container logs get_container_logs() { local container_name="$1" local lines="${2:-100}" docker logs --tail "$lines" "$container_name" 2>&1 } # Tail container logs tail_container_logs() { local container_name="$1" docker logs -f "$container_name" } # ============================================================================ # Database Operations # ============================================================================ # Wait for PostgreSQL to be ready wait_for_postgres() { local container_name="${1:-mosaic-postgres}" local user="${2:-mosaic}" local database="${3:-mosaic}" local timeout="${4:-60}" echo -e "${INFO}i${NC} Waiting for PostgreSQL to be ready..." local elapsed=0 while [[ $elapsed -lt $timeout ]]; do if docker exec "$container_name" pg_isready -U "$user" -d "$database" &>/dev/null; then echo -e "${SUCCESS}✓${NC} PostgreSQL is ready" return 0 fi sleep 2 ((elapsed += 2)) done echo -e "${ERROR}✗${NC} Timeout waiting for PostgreSQL" return 1 } # Run database migrations run_database_migrations() { local api_container="${1:-mosaic-api}" echo -e "${WARN}→${NC} Running database migrations..." if ! docker exec "$api_container" npx prisma migrate deploy &>/dev/null; then echo -e "${WARN}→${NC} Could not run migrations via API container" echo -e "${INFO}i${NC} Migrations will run automatically when API starts" return 0 fi echo -e "${SUCCESS}✓${NC} Database migrations complete" } # ============================================================================ # Service URLs # ============================================================================ # Get the URL for a service get_service_url() { local service="$1" local port="${2:-}" local host="localhost" # Check if we're in WSL and need to use Windows host if is_wsl; then host=$(cat /etc/resolv.conf 2>/dev/null | grep nameserver | awk '{print $2}' | head -1) fi if [[ -n "$port" ]]; then echo "http://${host}:${port}" else echo "http://${host}" fi } # Get all service URLs get_all_service_urls() { local env_file="${1:-.env}" declare -A urls=() if [[ -f "$env_file" ]]; then # shellcheck source=/dev/null source "$env_file" fi urls[web]="http://localhost:${WEB_PORT:-3000}" urls[api]="http://localhost:${API_PORT:-3001}" urls[postgres]="localhost:${POSTGRES_PORT:-5432}" urls[valkey]="localhost:${VALKEY_PORT:-6379}" if [[ "${OIDC_ENABLED:-false}" == "true" ]]; then urls[authentik]="http://localhost:${AUTHENTIK_PORT_HTTP:-9000}" fi if [[ "${OLLAMA_MODE:-disabled}" != "disabled" ]]; then urls[ollama]="http://localhost:${OLLAMA_PORT:-11434}" fi for service in "${!urls[@]}"; do echo "$service: ${urls[$service]}" done } # ============================================================================ # Docker Cleanup # ============================================================================ # Remove unused Docker resources docker_cleanup() { echo -e "${WARN}→${NC} Cleaning up unused Docker resources..." # Remove dangling images docker image prune -f # Remove unused networks docker network prune -f echo -e "${SUCCESS}✓${NC} Docker cleanup complete" } # Remove all Mosaic containers and volumes docker_remove_all() { local compose_file="${1:-docker-compose.yml}" local env_file="${2:-.env}" echo -e "${WARN}→${NC} Removing all Mosaic containers and volumes..." if [[ -f "$env_file" ]]; then docker_compose -f "$compose_file" --env-file "$env_file" down -v --remove-orphans else docker_compose -f "$compose_file" down -v --remove-orphans fi echo -e "${SUCCESS}✓${NC} All containers and volumes removed" } # ============================================================================ # Docker Info # ============================================================================ # Print Docker system info print_docker_info() { echo -e "${BOLD}Docker Information:${NC}" echo "" echo -e " Docker Version:" docker --version 2>/dev/null | sed 's/^/ /' echo "" echo -e " Docker Compose:" docker_compose version 2>/dev/null | sed 's/^/ /' echo "" echo -e " Docker Storage:" docker system df 2>/dev/null | sed 's/^/ /' echo "" echo -e " Running Containers:" docker ps --format " {{.Names}}\t{{.Status}}" 2>/dev/null | head -10 } # ============================================================================ # Volume Management # ============================================================================ # List all Mosaic volumes list_mosaic_volumes() { docker volume ls --filter "name=mosaic" --format "{{.Name}}" } # Backup a Docker volume backup_volume() { local volume_name="$1" local backup_file="${2:-${volume_name}-backup-$(date +%Y%m%d-%H%M%S).tar.gz}" echo -e "${WARN}→${NC} Backing up volume ${INFO}$volume_name${NC}..." docker run --rm \ -v "$volume_name":/source:ro \ -v "$(pwd)":/backup \ alpine:latest \ tar czf "/backup/$backup_file" -C /source . echo -e "${SUCCESS}✓${NC} Backup created: $backup_file" } # Restore a Docker volume restore_volume() { local volume_name="$1" local backup_file="$2" echo -e "${WARN}→${NC} Restoring volume ${INFO}$volume_name${NC} from $backup_file..." # Create volume if it doesn't exist docker volume create "$volume_name" &>/dev/null || true docker run --rm \ -v "$volume_name":/target \ -v "$(pwd)":/backup \ alpine:latest \ tar xzf "/backup/$backup_file" -C /target echo -e "${SUCCESS}✓${NC} Volume restored" } # ============================================================================ # Network Management # ============================================================================ # Create a Docker network if it doesn't exist ensure_network() { local network_name="$1" if ! docker network inspect "$network_name" &>/dev/null; then echo -e "${WARN}→${NC} Creating network ${INFO}$network_name${NC}..." docker network create "$network_name" echo -e "${SUCCESS}✓${NC} Network created" fi } # Check if a network exists network_exists() { local network_name="$1" docker network inspect "$network_name" &>/dev/null } # ============================================================================ # Build Operations # ============================================================================ # Build Docker images docker_build() { local compose_file="${1:-docker-compose.yml}" local env_file="${2:-.env}" local parallel="${3:-true}" echo -e "${WARN}→${NC} Building Docker images..." local args=("-f" "$compose_file") if [[ -f "$env_file" ]]; then args+=("--env-file" "$env_file") fi args+=("build") if [[ "$parallel" == "true" ]]; then args+=("--parallel") fi docker_compose "${args[@]}" } # Check if buildx is available check_buildx() { docker buildx version &>/dev/null } # Set up buildx builder setup_buildx() { if ! check_buildx; then echo -e "${WARN}→${NC} buildx not available" return 1 fi # Create or use existing builder if ! docker buildx inspect mosaic-builder &>/dev/null; then echo -e "${WARN}→${NC} Creating buildx builder..." docker buildx create --name mosaic-builder --use else docker buildx use mosaic-builder fi }