- Add Dockerfile + cron.js (daily 4pm UTC loop replacing EC2 cron) - Add infra/docker-compose.yml and deploy-stack.sh for Portainer - Support DATA_DIR env var in bot.js for persistent history volume - Support PROMPTS_JSON env var in cron.js (no SSH needed for config) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
3.1 KiB
Bash
106 lines
3.1 KiB
Bash
#!/bin/bash
|
|
# Deploy xBot stack to Portainer. No Caddy wiring needed (no HTTP endpoints).
|
|
#
|
|
# Required env:
|
|
# PORTAINER_URL — e.g. https://portainer.yourdomain.com
|
|
# PORTAINER_API_KEY — from docker-server setup.sh output
|
|
#
|
|
# Optional:
|
|
# STACK_NAME — default: xbot
|
|
# ENV_FILE — default: ./infra/.env
|
|
# COMPOSE_FILE — default: ./infra/docker-compose.yml
|
|
#
|
|
# Usage:
|
|
# PORTAINER_URL=https://portainer.yourdomain.com \
|
|
# PORTAINER_API_KEY=ptr_... \
|
|
# bash infra/deploy-stack.sh
|
|
set -euo pipefail
|
|
|
|
PORTAINER_URL="${PORTAINER_URL:?Set PORTAINER_URL}"
|
|
PORTAINER_API_KEY="${PORTAINER_API_KEY:?Set PORTAINER_API_KEY}"
|
|
STACK_NAME="${STACK_NAME:-xbot}"
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ENV_FILE="${ENV_FILE:-$SCRIPT_DIR/.env}"
|
|
COMPOSE_FILE="${COMPOSE_FILE:-$SCRIPT_DIR/docker-compose.yml}"
|
|
|
|
for f in "$ENV_FILE" "$COMPOSE_FILE"; do
|
|
[ -f "$f" ] || { echo "ERROR: not found: $f"; exit 1; }
|
|
done
|
|
|
|
API="$PORTAINER_URL/api"
|
|
|
|
echo "[$STACK_NAME] Looking up Portainer endpoint..."
|
|
ENDPOINT_ID=$(curl -s "$API/endpoints" \
|
|
-H "X-API-Key: $PORTAINER_API_KEY" | \
|
|
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Id'])")
|
|
|
|
[ -z "$ENDPOINT_ID" ] && { echo "ERROR: No Portainer endpoint found"; exit 1; }
|
|
|
|
build_payload() {
|
|
local mode="$1"
|
|
python3 - "$COMPOSE_FILE" "$ENV_FILE" "$STACK_NAME" "$mode" <<'PYEOF'
|
|
import json, sys, re
|
|
|
|
compose_file, env_file, stack_name, mode = sys.argv[1:5]
|
|
|
|
with open(compose_file) as f:
|
|
compose = f.read()
|
|
|
|
env_vars = []
|
|
with open(env_file) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
key, _, value = line.partition('=')
|
|
if key:
|
|
env_vars.append({"name": key, "value": value})
|
|
|
|
def replace_env_file(m):
|
|
indent = re.search(r'\n(\s+)env_file', m.group(0)).group(1)
|
|
lines = [f'\n{indent}environment:']
|
|
for var in env_vars:
|
|
lines.append(f'{indent} {var["name"]}: "${{{var["name"]}}}"')
|
|
return '\n'.join(lines)
|
|
|
|
compose = re.sub(r'\n\s+env_file:[^\n]*', replace_env_file, compose)
|
|
|
|
payload = {"stackFileContent": compose, "env": env_vars}
|
|
if mode == "create":
|
|
payload["name"] = stack_name
|
|
else:
|
|
payload["prune"] = True
|
|
|
|
json.dump(payload, sys.stdout)
|
|
PYEOF
|
|
}
|
|
|
|
echo "[$STACK_NAME] Checking for existing stack..."
|
|
EXISTING_ID=$(curl -s "$API/stacks" \
|
|
-H "X-API-Key: $PORTAINER_API_KEY" | \
|
|
python3 -c "
|
|
import sys, json
|
|
for s in json.load(sys.stdin):
|
|
if s['Name'] == '$STACK_NAME':
|
|
print(s['Id']); break
|
|
" 2>/dev/null || true)
|
|
|
|
if [ -n "$EXISTING_ID" ]; then
|
|
echo "[$STACK_NAME] Updating stack (ID: $EXISTING_ID)..."
|
|
build_payload update | curl -s -X PUT "$API/stacks/$EXISTING_ID?endpointId=$ENDPOINT_ID" \
|
|
-H "X-API-Key: $PORTAINER_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d @- > /dev/null
|
|
else
|
|
echo "[$STACK_NAME] Creating stack..."
|
|
build_payload create | curl -s -X POST "$API/stacks/create/standalone/string?endpointId=$ENDPOINT_ID" \
|
|
-H "X-API-Key: $PORTAINER_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d @- > /dev/null
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== $STACK_NAME deployed ==="
|
|
echo "Check status at: $PORTAINER_URL"
|