Migrate to Docker: containerize for docker-server deployment

- 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>
This commit is contained in:
2026-03-17 17:15:18 -07:00
parent ab32ef4cc2
commit dacc7604cc
8 changed files with 277 additions and 1 deletions

105
infra/deploy-stack.sh Normal file
View File

@@ -0,0 +1,105 @@
#!/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"