2026-04-04 14:34:56 -04:00
|
|
|
#!/bin/bash
|
2026-04-04 15:09:26 -04:00
|
|
|
# ============================================================================
|
|
|
|
|
# Claude Code Stack - TrueNAS SSH Deployment Script
|
|
|
|
|
# Usage: ./deploy-ssh.sh <TRUENAS_IP> [TRUENAS_USER] [POOL_NAME]
|
|
|
|
|
# Example: ./deploy-ssh.sh 192.168.1.100 root tank
|
|
|
|
|
# ============================================================================
|
2026-04-04 14:34:56 -04:00
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
# Color output
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
YELLOW='\033[1;33m'
|
2026-04-04 15:09:26 -04:00
|
|
|
BLUE='\033[0;34m'
|
|
|
|
|
NC='\033[0m'
|
2026-04-04 14:34:56 -04:00
|
|
|
|
|
|
|
|
# Configuration
|
|
|
|
|
TRUENAS_IP="${1:-}"
|
|
|
|
|
TRUENAS_USER="${2:-root}"
|
|
|
|
|
POOL_NAME="${3:-tank}"
|
|
|
|
|
PROJECT_DIR="/mnt/${POOL_NAME}/docker/claude-code-stack"
|
|
|
|
|
|
|
|
|
|
# Functions
|
2026-04-04 15:09:26 -04:00
|
|
|
print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
|
|
|
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
|
|
|
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
|
|
|
print_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
|
2026-04-04 14:34:56 -04:00
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
validate_input() {
|
|
|
|
|
if [ -z "$TRUENAS_IP" ]; then
|
|
|
|
|
print_error "Usage: $0 <TRUENAS_IP> [TRUENAS_USER] [POOL_NAME]"
|
|
|
|
|
print_info "Example: $0 192.168.1.100 root tank"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-04-04 14:34:56 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
check_prerequisites() {
|
2026-04-04 15:09:26 -04:00
|
|
|
print_step "Checking prerequisites..."
|
|
|
|
|
|
|
|
|
|
if ! command -v ssh &>/dev/null; then
|
|
|
|
|
print_error "SSH client not found. Please install it."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ! command -v scp &>/dev/null; then
|
|
|
|
|
print_error "scp not found. Please install openssh-client."
|
2026-04-04 14:34:56 -04:00
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-04-04 15:09:26 -04:00
|
|
|
|
|
|
|
|
# Prompt for API key if not already set
|
2026-04-04 14:34:56 -04:00
|
|
|
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
2026-04-04 15:09:26 -04:00
|
|
|
print_warn "ANTHROPIC_API_KEY is not set in your environment."
|
|
|
|
|
echo -n " Enter your Anthropic API key (sk-ant-...): "
|
|
|
|
|
read -r ANTHROPIC_API_KEY
|
2026-04-04 14:34:56 -04:00
|
|
|
export ANTHROPIC_API_KEY
|
|
|
|
|
fi
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
|
|
|
|
print_error "ANTHROPIC_API_KEY is required. Obtain one at https://console.anthropic.com/"
|
2026-04-04 14:34:56 -04:00
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-04-04 15:09:26 -04:00
|
|
|
|
|
|
|
|
print_info "Prerequisites OK"
|
2026-04-04 14:34:56 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
test_ssh_connection() {
|
2026-04-04 15:09:26 -04:00
|
|
|
print_step "Testing SSH connection to ${TRUENAS_USER}@${TRUENAS_IP}..."
|
|
|
|
|
if ssh -o ConnectTimeout=8 -o BatchMode=yes \
|
|
|
|
|
"${TRUENAS_USER}@${TRUENAS_IP}" "echo 'SSH OK'" >/dev/null 2>&1; then
|
2026-04-04 14:34:56 -04:00
|
|
|
print_info "SSH connection successful"
|
|
|
|
|
else
|
2026-04-04 15:09:26 -04:00
|
|
|
print_error "Cannot connect to ${TRUENAS_USER}@${TRUENAS_IP} via SSH."
|
|
|
|
|
print_info "Check: TrueNAS IP, SSH enabled, SSH keys / credentials configured."
|
2026-04-04 14:34:56 -04:00
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
create_project_directory() {
|
2026-04-04 15:09:26 -04:00
|
|
|
print_step "Creating project directory on TrueNAS..."
|
|
|
|
|
ssh "${TRUENAS_USER}@${TRUENAS_IP}" "
|
|
|
|
|
mkdir -p '${PROJECT_DIR}'
|
|
|
|
|
chmod 755 '${PROJECT_DIR}'
|
|
|
|
|
echo 'Directory ready: ${PROJECT_DIR}'
|
|
|
|
|
"
|
2026-04-04 14:34:56 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
upload_files() {
|
|
|
|
|
print_step "Uploading deployment files..."
|
|
|
|
|
|
2026-04-04 14:34:56 -04:00
|
|
|
local files=(
|
2026-04-04 15:09:26 -04:00
|
|
|
"claude-agents-ui-Dockerfile"
|
|
|
|
|
"claude-code-stack-docker-compose.yml"
|
2026-04-04 14:34:56 -04:00
|
|
|
"claude-code-stack.env"
|
|
|
|
|
"nginx.conf"
|
2026-04-04 15:09:26 -04:00
|
|
|
"deploy-mcp.py"
|
|
|
|
|
"CHECKLIST.md"
|
|
|
|
|
"QUICKSTART.md"
|
|
|
|
|
"DEPLOYMENT_GUIDE.md"
|
2026-04-04 14:34:56 -04:00
|
|
|
)
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
for f in "${files[@]}"; do
|
|
|
|
|
if [ -f "./${f}" ]; then
|
|
|
|
|
scp "./${f}" "${TRUENAS_USER}@${TRUENAS_IP}:${PROJECT_DIR}/" && \
|
|
|
|
|
print_info " Uploaded: ${f}"
|
2026-04-04 14:34:56 -04:00
|
|
|
else
|
2026-04-04 15:09:26 -04:00
|
|
|
print_warn " Skipping (not found locally): ${f}"
|
2026-04-04 14:34:56 -04:00
|
|
|
fi
|
|
|
|
|
done
|
2026-04-04 15:09:26 -04:00
|
|
|
|
|
|
|
|
# Rename docker-compose to the standard name docker-compose expects
|
|
|
|
|
ssh "${TRUENAS_USER}@${TRUENAS_IP}" "
|
|
|
|
|
cd '${PROJECT_DIR}'
|
|
|
|
|
[ -f claude-code-stack-docker-compose.yml ] && \
|
|
|
|
|
cp claude-code-stack-docker-compose.yml docker-compose.yml
|
|
|
|
|
[ -f claude-agents-ui-Dockerfile ] && \
|
|
|
|
|
cp claude-agents-ui-Dockerfile Dockerfile
|
|
|
|
|
"
|
|
|
|
|
print_info " Aliased docker-compose.yml and Dockerfile"
|
2026-04-04 14:34:56 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
configure_environment() {
|
2026-04-04 15:09:26 -04:00
|
|
|
print_step "Writing .env file on TrueNAS..."
|
|
|
|
|
|
|
|
|
|
# Generate random secrets server-side
|
|
|
|
|
ssh "${TRUENAS_USER}@${TRUENAS_IP}" bash -s -- \
|
|
|
|
|
"${ANTHROPIC_API_KEY}" "${PROJECT_DIR}" << 'REMOTE_EOF'
|
|
|
|
|
API_KEY="$1"
|
|
|
|
|
PROJECT_DIR="$2"
|
|
|
|
|
|
|
|
|
|
POSTGRES_PASS=$(openssl rand -base64 16 | tr -d '/+=' | head -c 20)
|
|
|
|
|
SESSION_SEC=$(openssl rand -base64 32)
|
2026-04-04 14:34:56 -04:00
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
cat > "${PROJECT_DIR}/.env" << ENV_EOF
|
|
|
|
|
# Auto-generated by deploy-ssh.sh on $(date)
|
|
|
|
|
ANTHROPIC_API_KEY=${API_KEY}
|
|
|
|
|
CLAUDE_MODEL=claude-sonnet-4-6
|
2026-04-04 14:34:56 -04:00
|
|
|
NODE_ENV=production
|
2026-04-04 15:09:26 -04:00
|
|
|
POSTGRES_USER=claude
|
|
|
|
|
POSTGRES_PASSWORD=${POSTGRES_PASS}
|
|
|
|
|
POSTGRES_DB=claude_agents
|
|
|
|
|
REDIS_HOST=redis
|
|
|
|
|
REDIS_PORT=6379
|
|
|
|
|
SESSION_SECRET=${SESSION_SEC}
|
|
|
|
|
CORS_ORIGIN=*
|
2026-04-04 14:34:56 -04:00
|
|
|
LOG_LEVEL=info
|
2026-04-04 15:09:26 -04:00
|
|
|
WORKSPACE_DIR=/workspace
|
|
|
|
|
CLAUDE_CONFIG_DIR=/root/.claude
|
2026-04-04 14:34:56 -04:00
|
|
|
ENV_EOF
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
chmod 600 "${PROJECT_DIR}/.env"
|
|
|
|
|
echo " .env written with auto-generated secrets"
|
|
|
|
|
REMOTE_EOF
|
2026-04-04 14:34:56 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
deploy_stack() {
|
2026-04-04 15:09:26 -04:00
|
|
|
print_step "Pulling images and deploying stack..."
|
|
|
|
|
ssh "${TRUENAS_USER}@${TRUENAS_IP}" "
|
|
|
|
|
cd '${PROJECT_DIR}'
|
|
|
|
|
echo ' Pulling base images...'
|
|
|
|
|
docker-compose pull --quiet 2>/dev/null || true
|
|
|
|
|
echo ' Building custom images...'
|
|
|
|
|
docker-compose build --pull
|
|
|
|
|
echo ' Starting services...'
|
|
|
|
|
docker-compose up -d
|
|
|
|
|
echo ' Waiting 15s for services to stabilise...'
|
|
|
|
|
sleep 15
|
|
|
|
|
docker-compose ps
|
|
|
|
|
"
|
|
|
|
|
}
|
2026-04-04 14:34:56 -04:00
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
verify_auth() {
|
|
|
|
|
print_step "Verifying Claude auth inside agents-ui container..."
|
|
|
|
|
ssh "${TRUENAS_USER}@${TRUENAS_IP}" "
|
|
|
|
|
cd '${PROJECT_DIR}'
|
|
|
|
|
echo '--- Claude auth status ---'
|
|
|
|
|
docker-compose exec -T agents-ui claude auth status 2>/dev/null || \
|
|
|
|
|
echo ' (auth check not available yet - check logs if UI fails)'
|
|
|
|
|
"
|
2026-04-04 14:34:56 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
verify_deployment() {
|
2026-04-04 15:09:26 -04:00
|
|
|
print_step "Verifying deployment..."
|
2026-04-04 14:34:56 -04:00
|
|
|
sleep 5
|
2026-04-04 15:09:26 -04:00
|
|
|
|
|
|
|
|
print_info "Service status:"
|
|
|
|
|
ssh "${TRUENAS_USER}@${TRUENAS_IP}" "cd '${PROJECT_DIR}' && docker-compose ps"
|
|
|
|
|
|
|
|
|
|
print_info "Testing Agents UI (port 3000)..."
|
|
|
|
|
if curl -s -f --max-time 10 "http://${TRUENAS_IP}:3000" >/dev/null 2>&1; then
|
|
|
|
|
print_info " ✓ Agents UI is responding"
|
2026-04-04 14:34:56 -04:00
|
|
|
else
|
2026-04-04 15:09:26 -04:00
|
|
|
print_warn " ! Agents UI not yet responding — may still be starting up."
|
|
|
|
|
print_info " Check logs: ssh ${TRUENAS_USER}@${TRUENAS_IP} \"cd ${PROJECT_DIR} && docker-compose logs -f agents-ui\""
|
2026-04-04 14:34:56 -04:00
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
setup_monitoring() {
|
2026-04-04 15:09:26 -04:00
|
|
|
print_step "Installing monitoring script..."
|
|
|
|
|
ssh "${TRUENAS_USER}@${TRUENAS_IP}" "
|
|
|
|
|
cat > '${PROJECT_DIR}/monitor.sh' << 'MON_EOF'
|
2026-04-04 14:34:56 -04:00
|
|
|
#!/bin/bash
|
|
|
|
|
while true; do
|
|
|
|
|
clear
|
2026-04-04 15:09:26 -04:00
|
|
|
echo '=== Claude Code Stack Status ==='
|
|
|
|
|
echo \"Time: \$(date)\"
|
|
|
|
|
echo ''
|
|
|
|
|
docker-compose -f '${PROJECT_DIR}/docker-compose.yml' ps
|
|
|
|
|
echo ''
|
|
|
|
|
echo 'Resource Usage:'
|
|
|
|
|
docker stats --no-stream --format 'table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}'
|
|
|
|
|
echo ''
|
|
|
|
|
echo 'Press Ctrl+C to exit'
|
2026-04-04 14:34:56 -04:00
|
|
|
sleep 10
|
|
|
|
|
done
|
2026-04-04 15:09:26 -04:00
|
|
|
MON_EOF
|
|
|
|
|
chmod +x '${PROJECT_DIR}/monitor.sh'
|
|
|
|
|
"
|
|
|
|
|
print_info " Monitoring script: ${PROJECT_DIR}/monitor.sh"
|
2026-04-04 14:34:56 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ----------------------------------------------------------------------------
|
2026-04-04 14:34:56 -04:00
|
|
|
print_summary() {
|
|
|
|
|
echo ""
|
2026-04-04 15:09:26 -04:00
|
|
|
echo -e "${GREEN}============================================${NC}"
|
|
|
|
|
echo -e "${GREEN} Claude Code Stack — Deployment Complete!${NC}"
|
|
|
|
|
echo -e "${GREEN}============================================${NC}"
|
2026-04-04 14:34:56 -04:00
|
|
|
echo ""
|
2026-04-04 15:09:26 -04:00
|
|
|
echo -e " Agents UI: ${YELLOW}http://${TRUENAS_IP}:3000${NC}"
|
|
|
|
|
echo -e " Project: ${YELLOW}${PROJECT_DIR}${NC}"
|
2026-04-04 14:34:56 -04:00
|
|
|
echo ""
|
2026-04-04 15:09:26 -04:00
|
|
|
echo " Useful commands:"
|
|
|
|
|
echo -e " Logs: ${YELLOW}ssh ${TRUENAS_USER}@${TRUENAS_IP} \"cd ${PROJECT_DIR} && docker-compose logs -f\"${NC}"
|
|
|
|
|
echo -e " Status: ${YELLOW}ssh ${TRUENAS_USER}@${TRUENAS_IP} \"cd ${PROJECT_DIR} && docker-compose ps\"${NC}"
|
|
|
|
|
echo -e " Stop: ${YELLOW}ssh ${TRUENAS_USER}@${TRUENAS_IP} \"cd ${PROJECT_DIR} && docker-compose down\"${NC}"
|
|
|
|
|
echo -e " Auth: ${YELLOW}ssh ${TRUENAS_USER}@${TRUENAS_IP} \"cd ${PROJECT_DIR} && docker-compose exec agents-ui claude auth status\"${NC}"
|
|
|
|
|
echo -e " Monitor: ${YELLOW}ssh ${TRUENAS_USER}@${TRUENAS_IP} \"${PROJECT_DIR}/monitor.sh\"${NC}"
|
2026-04-04 14:34:56 -04:00
|
|
|
echo ""
|
2026-04-04 15:09:26 -04:00
|
|
|
echo " Next steps:"
|
|
|
|
|
echo " 1. Open http://${TRUENAS_IP}:3000"
|
|
|
|
|
echo " 2. Create your first agent in the UI"
|
|
|
|
|
echo " 3. Run a test prompt to verify Claude Code is working"
|
2026-04-04 14:34:56 -04:00
|
|
|
echo ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:09:26 -04:00
|
|
|
# ============================================================================
|
|
|
|
|
# Main
|
|
|
|
|
# ============================================================================
|
2026-04-04 14:34:56 -04:00
|
|
|
main() {
|
2026-04-04 15:09:26 -04:00
|
|
|
echo -e "${GREEN}Claude Code Stack — TrueNAS SSH Deployment${NC}"
|
|
|
|
|
echo "============================================"
|
2026-04-04 14:34:56 -04:00
|
|
|
echo ""
|
|
|
|
|
validate_input
|
|
|
|
|
check_prerequisites
|
|
|
|
|
test_ssh_connection
|
|
|
|
|
create_project_directory
|
2026-04-04 15:09:26 -04:00
|
|
|
upload_files
|
2026-04-04 14:34:56 -04:00
|
|
|
configure_environment
|
|
|
|
|
deploy_stack
|
2026-04-04 15:09:26 -04:00
|
|
|
verify_auth
|
2026-04-04 14:34:56 -04:00
|
|
|
verify_deployment
|
|
|
|
|
setup_monitoring
|
|
|
|
|
print_summary
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main "$@"
|