- Add one-line installer (scripts/install.sh) with platform detection - Add doctor command (scripts/commands/doctor.sh) for environment diagnostics - Add shared libraries: dependencies, docker, platform, validation - Update README with quick-start installer instructions - Add AGENTS.md with codebase patterns for AI agent context Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
492 lines
13 KiB
Bash
492 lines
13 KiB
Bash
#!/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
|
|
}
|