Publish deployment and backup scripts
This commit is contained in:
20
README.md
20
README.md
@@ -1,20 +0,0 @@
|
||||
# openclaw-deploy-scripts
|
||||
|
||||
Deployment and backup scripts for services on this host.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Docker and local deployment helpers under `/opt/deploy`
|
||||
- Shared Traefik helpers in `common.sh`
|
||||
- Local OpenClaw deployment now supports automatic Traefik wiring
|
||||
- Companion how-to docs live under `how-to/`
|
||||
|
||||
## Docs
|
||||
|
||||
- `how-to/traefik-openclaw.md`
|
||||
- `how-to/mattermost-openclaw.md`
|
||||
- `how-to/gitea-openclaw-bootstrap.md`
|
||||
|
||||
## Notes
|
||||
|
||||
These scripts are evolving toward unattended rebuilds.
|
||||
50
backup-agenticseek.sh
Executable file
50
backup-agenticseek.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# AgenticSeek Backup Script
|
||||
# ==============================================================================
|
||||
# Usage: backup-agenticseek.sh
|
||||
# backup-agenticseek.sh --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="agenticseek"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "AgenticSeek is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Cold backup: stop containers ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping containers)..."
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
fi
|
||||
|
||||
# --- Create archive ---
|
||||
info "Creating archive..."
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}" \
|
||||
"opt/traefik/dynamic/${CONFIG_NAME}.yml" 2>/dev/null || \
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
ok "Containers restarted."
|
||||
fi
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
120
backup-all.sh
Executable file
120
backup-all.sh
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Full Server Backup Script
|
||||
# ==============================================================================
|
||||
# Discovers deployed services by scanning /opt/*/docker-compose.yml and
|
||||
# local service units, runs the appropriate backup script for each, then
|
||||
# creates a combined full-server archive.
|
||||
#
|
||||
# Usage: backup-all.sh
|
||||
# backup-all.sh --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
COLD_FLAG=""
|
||||
[[ "$ARG_COLD" == "1" ]] && COLD_FLAG="--cold"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Full Server Backup${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
if [[ -n "$COLD_FLAG" ]]; then
|
||||
echo -e " Mode: ${YELLOW}Cold (services will be stopped)${NC}"
|
||||
else
|
||||
echo -e " Mode: Hot (services stay running)"
|
||||
fi
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
failed=0
|
||||
|
||||
# --- Traefik ---
|
||||
if [[ -d /opt/traefik ]]; then
|
||||
info "Backing up traefik..."
|
||||
"${SCRIPT_DIR}/backup-traefik.sh" $COLD_FLAG || { warn "Traefik backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- Mattermost ---
|
||||
if [[ -f /opt/mattermost/docker-compose.yml ]]; then
|
||||
info "Backing up mattermost..."
|
||||
"${SCRIPT_DIR}/backup-mattermost.sh" $COLD_FLAG || { warn "Mattermost backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- Gitea ---
|
||||
if [[ -f /opt/gitea/docker-compose.yml ]]; then
|
||||
info "Backing up gitea..."
|
||||
"${SCRIPT_DIR}/backup-gitea.sh" $COLD_FLAG || { warn "Gitea backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- Hermes ---
|
||||
if [[ -f /opt/hermes/docker-compose.yml ]] || [[ -f /etc/systemd/system/hermes.service ]]; then
|
||||
info "Backing up hermes..."
|
||||
"${SCRIPT_DIR}/backup-hermes.sh" $COLD_FLAG || { warn "Hermes backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- Paperclip ---
|
||||
if [[ -d /opt/paperclip ]]; then
|
||||
info "Backing up paperclip..."
|
||||
"${SCRIPT_DIR}/backup-paperclip.sh" $COLD_FLAG || { warn "Paperclip backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- OpenClaw ---
|
||||
if [[ -d /opt/openclaw ]]; then
|
||||
info "Backing up openclaw..."
|
||||
"${SCRIPT_DIR}/backup-openclaw.sh" $COLD_FLAG || { warn "OpenClaw backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- Open WebUI ---
|
||||
if [[ -f /opt/open-webui/docker-compose.yml ]]; then
|
||||
info "Backing up open-webui..."
|
||||
"${SCRIPT_DIR}/backup-open-webui.sh" $COLD_FLAG || { warn "Open WebUI backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- AgenticSeek ---
|
||||
if [[ -f /opt/agenticseek/docker-compose.yml ]]; then
|
||||
info "Backing up agenticseek..."
|
||||
"${SCRIPT_DIR}/backup-agenticseek.sh" $COLD_FLAG || { warn "AgenticSeek backup failed."; (( failed++ )); }
|
||||
fi
|
||||
|
||||
# --- Web instances (discover by /opt/*/docker-compose.yml with web- container) ---
|
||||
for dir in /opt/*/; do
|
||||
dir_name="$(basename "$dir")"
|
||||
compose="${dir}docker-compose.yml"
|
||||
if [[ -f "$compose" ]] && grep -q "web-${dir_name}" "$compose" 2>/dev/null; then
|
||||
info "Backing up web instance: ${dir_name}..."
|
||||
"${SCRIPT_DIR}/backup-web.sh" --dir "$dir_name" $COLD_FLAG || { warn "Web (${dir_name}) backup failed."; (( failed++ )); }
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Combined full-server archive ---
|
||||
FULL_BACKUP="${BACKUP_DIR}/backup-all-${STAMP}.tar.gz"
|
||||
info "Creating combined full-server archive..."
|
||||
tar czf "$FULL_BACKUP" -C / opt/ 2>/dev/null || warn "Full archive creation failed."
|
||||
|
||||
# --- Rotate full archives (keep last 3) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-all-"*.tar.gz 2>/dev/null | tail -n +4 | xargs -r rm --
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Backup complete${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
if [[ -f "$FULL_BACKUP" ]]; then
|
||||
echo -e " Full: ${FULL_BACKUP} ($(du -h "$FULL_BACKUP" | cut -f1))"
|
||||
fi
|
||||
echo -e " Dir: ${BACKUP_DIR}/"
|
||||
if (( failed > 0 )); then
|
||||
echo -e " ${YELLOW}Warnings: ${failed} backup(s) had issues${NC}"
|
||||
fi
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
77
backup-gitea.sh
Executable file
77
backup-gitea.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Gitea Backup Script
|
||||
# ==============================================================================
|
||||
# Creates a backup archive of Gitea + PostgreSQL data and Traefik config.
|
||||
#
|
||||
# Hot backup (default): runs gitea dump + pg_dump while containers stay up.
|
||||
# Cold backup (--cold): stops containers, tars data, restarts.
|
||||
#
|
||||
# Usage: backup-gitea.sh
|
||||
# backup-gitea.sh --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="gitea"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "Gitea is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Hot backup: application-level dumps ---
|
||||
if [[ "$ARG_COLD" != "1" ]]; then
|
||||
info "Running hot backup (containers stay up)..."
|
||||
|
||||
# Gitea built-in dump
|
||||
info "Running gitea dump..."
|
||||
mkdir -p "${BASE_DIR}/gitea/data/backup"
|
||||
docker exec -u git gitea bash -c \
|
||||
'/app/gitea/gitea dump -c /data/gitea/conf/app.ini --type tar.gz -f /data/backup/gitea-dump.tar.gz' \
|
||||
2>/dev/null || warn "Gitea dump failed — archive will contain live data files only."
|
||||
|
||||
# PostgreSQL dump
|
||||
info "Running pg_dump..."
|
||||
docker exec gitea-postgres pg_dump -U gitea -d gitea \
|
||||
> "${BASE_DIR}/postgres/dump.sql" \
|
||||
2>/dev/null || warn "pg_dump failed — archive will contain live data files only."
|
||||
fi
|
||||
|
||||
# --- Cold backup: stop containers ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping containers)..."
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
fi
|
||||
|
||||
# --- Create archive ---
|
||||
info "Creating archive..."
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}" \
|
||||
"opt/traefik/dynamic/${CONFIG_NAME}.yml" 2>/dev/null || \
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
ok "Containers restarted."
|
||||
fi
|
||||
|
||||
# --- Clean up dump files ---
|
||||
rm -f "${BASE_DIR}/gitea/data/backup/gitea-dump.tar.gz" 2>/dev/null
|
||||
rm -f "${BASE_DIR}/postgres/dump.sql" 2>/dev/null
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
55
backup-hermes.sh
Executable file
55
backup-hermes.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Hermes Agent Backup Script
|
||||
# ==============================================================================
|
||||
# Works for both local and Docker deployments.
|
||||
#
|
||||
# Usage: backup-hermes.sh
|
||||
# backup-hermes.sh --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="hermes"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "Hermes is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Cold backup: stop service ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping service)..."
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
elif systemctl is-active --quiet "${SERVICE_NAME}.service" 2>/dev/null; then
|
||||
systemctl stop "${SERVICE_NAME}.service"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Create archive (no traefik config for hermes) ---
|
||||
info "Creating archive..."
|
||||
tar czf "$BACKUP_FILE" -C / "opt/${SERVICE_NAME}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
elif [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
|
||||
systemctl start "${SERVICE_NAME}.service"
|
||||
fi
|
||||
ok "Service restarted."
|
||||
fi
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
66
backup-mattermost.sh
Executable file
66
backup-mattermost.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Mattermost Backup Script
|
||||
# ==============================================================================
|
||||
# Hot backup (default): pg_dump while containers stay up.
|
||||
# Cold backup (--cold): stops containers, tars data, restarts.
|
||||
#
|
||||
# Usage: backup-mattermost.sh
|
||||
# backup-mattermost.sh --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="mattermost"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "Mattermost is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Hot backup: pg_dump ---
|
||||
if [[ "$ARG_COLD" != "1" ]]; then
|
||||
info "Running hot backup (containers stay up)..."
|
||||
|
||||
info "Running pg_dump..."
|
||||
docker exec mm-postgres pg_dump -U mmuser -d mattermost \
|
||||
> "${BASE_DIR}/postgres/dump.sql" \
|
||||
2>/dev/null || warn "pg_dump failed — archive will contain live data files only."
|
||||
fi
|
||||
|
||||
# --- Cold backup: stop containers ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping containers)..."
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
fi
|
||||
|
||||
# --- Create archive ---
|
||||
info "Creating archive..."
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}" \
|
||||
"opt/traefik/dynamic/${CONFIG_NAME}.yml" 2>/dev/null || \
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
ok "Containers restarted."
|
||||
fi
|
||||
|
||||
# --- Clean up dump ---
|
||||
rm -f "${BASE_DIR}/postgres/dump.sql" 2>/dev/null
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
69
backup-open-webui.sh
Executable file
69
backup-open-webui.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Open WebUI Backup Script
|
||||
# ==============================================================================
|
||||
# Creates a backup archive of Open WebUI data and Traefik config.
|
||||
#
|
||||
# Open WebUI uses SQLite internally. Hot backup copies the data directory
|
||||
# while running (safe for most cases). Use --cold for guaranteed consistency.
|
||||
#
|
||||
# Usage: backup-open-webui.sh
|
||||
# backup-open-webui.sh --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="open-webui"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "Open WebUI is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Hot backup: attempt SQLite .backup if sqlite3 is available ---
|
||||
if [[ "$ARG_COLD" != "1" ]]; then
|
||||
info "Running hot backup (containers stay up)..."
|
||||
local_db="${BASE_DIR}/data/webui.db"
|
||||
if [[ -f "$local_db" ]] && command -v sqlite3 &>/dev/null; then
|
||||
info "Creating SQLite snapshot..."
|
||||
sqlite3 "$local_db" ".backup '${BASE_DIR}/data/webui-backup.db'" 2>/dev/null || \
|
||||
warn "SQLite .backup failed — archive will contain live database file."
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Cold backup: stop containers ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping containers)..."
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
fi
|
||||
|
||||
# --- Create archive ---
|
||||
info "Creating archive..."
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}" \
|
||||
"opt/traefik/dynamic/${CONFIG_NAME}.yml" 2>/dev/null || \
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${SERVICE_NAME}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
ok "Containers restarted."
|
||||
fi
|
||||
|
||||
# --- Clean up snapshot ---
|
||||
rm -f "${BASE_DIR}/data/webui-backup.db" 2>/dev/null
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
60
backup-openclaw.sh
Executable file
60
backup-openclaw.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# OpenClaw Backup Script
|
||||
# ==============================================================================
|
||||
# Works for both local and Docker deployments.
|
||||
#
|
||||
# Usage: backup-openclaw.sh
|
||||
# backup-openclaw.sh --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="openclaw"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "OpenClaw is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Cold backup: stop service ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping service)..."
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
elif systemctl is-active --quiet "${SERVICE_NAME}.service" 2>/dev/null; then
|
||||
systemctl stop "${SERVICE_NAME}.service"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Create archive ---
|
||||
info "Creating archive..."
|
||||
tar_paths=("opt/${SERVICE_NAME}")
|
||||
if [[ -f "${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml" ]]; then
|
||||
tar_paths+=("opt/traefik/dynamic/${CONFIG_NAME}.yml")
|
||||
fi
|
||||
tar czf "$BACKUP_FILE" -C / "${tar_paths[@]}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
elif [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
|
||||
systemctl start "${SERVICE_NAME}.service"
|
||||
fi
|
||||
ok "Service restarted."
|
||||
fi
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
64
backup-paperclip.sh
Executable file
64
backup-paperclip.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Paperclip Backup Script
|
||||
# ==============================================================================
|
||||
# Creates a backup archive of Paperclip data (works for both local and Docker).
|
||||
#
|
||||
# Usage: backup-paperclip.sh
|
||||
# backup-paperclip.sh --cold # Stop service before backup
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="paperclip"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "Paperclip is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Cold backup: stop service ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping service)..."
|
||||
# Handle both Docker and local deployments
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
elif systemctl is-active --quiet "${SERVICE_NAME}.service" 2>/dev/null; then
|
||||
systemctl stop "${SERVICE_NAME}.service"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Create archive ---
|
||||
info "Creating archive..."
|
||||
tar_paths=("opt/${SERVICE_NAME}")
|
||||
|
||||
# Include traefik dynamic config if it exists (Docker variant or manual setup)
|
||||
if [[ -f "${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml" ]]; then
|
||||
tar_paths+=("opt/traefik/dynamic/${CONFIG_NAME}.yml")
|
||||
fi
|
||||
|
||||
tar czf "$BACKUP_FILE" -C / "${tar_paths[@]}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
elif [[ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]]; then
|
||||
systemctl start "${SERVICE_NAME}.service"
|
||||
fi
|
||||
ok "Service restarted."
|
||||
fi
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
34
backup-traefik.sh
Executable file
34
backup-traefik.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Traefik Backup Script
|
||||
# ==============================================================================
|
||||
# Archives /opt/traefik/ (acme.json certs + all dynamic configs).
|
||||
# Always hot — Traefik doesn't need to be stopped for file copy.
|
||||
#
|
||||
# Usage: backup-traefik.sh
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="traefik"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${SERVICE_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "Traefik is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
info "Creating archive (hot — Traefik stays running)..."
|
||||
tar czf "$BACKUP_FILE" -C / "opt/${SERVICE_NAME}"
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${SERVICE_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
55
backup-web.sh
Executable file
55
backup-web.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Web Instance Backup Script
|
||||
# ==============================================================================
|
||||
# Requires --dir to identify which instance to back up.
|
||||
# No pre-backup commands — just static files.
|
||||
#
|
||||
# Usage: backup-web.sh --dir acme
|
||||
# backup-web.sh --dir acme --cold
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
parse_args "$@"
|
||||
|
||||
[[ -n "$ARG_DIR" ]] || fatal "--dir is required. Example: $0 --dir acme"
|
||||
|
||||
DIR_NAME="$ARG_DIR"
|
||||
BASE_DIR="/opt/${DIR_NAME}"
|
||||
CONFIG_NAME="web-${DIR_NAME}"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BACKUP_FILE="${BACKUP_DIR}/backup-${DIR_NAME}-${STAMP}.tar.gz"
|
||||
|
||||
[[ -d "$BASE_DIR" ]] || fatal "Web instance '${DIR_NAME}' is not deployed at ${BASE_DIR}."
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Cold backup: stop container ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
info "Running cold backup (stopping container)..."
|
||||
(cd "$BASE_DIR" && docker compose down)
|
||||
fi
|
||||
|
||||
# --- Create archive ---
|
||||
info "Creating archive..."
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${DIR_NAME}" \
|
||||
"opt/traefik/dynamic/${CONFIG_NAME}.yml" 2>/dev/null || \
|
||||
tar czf "$BACKUP_FILE" -C / \
|
||||
"opt/${DIR_NAME}"
|
||||
|
||||
# --- Cold: restart ---
|
||||
if [[ "$ARG_COLD" == "1" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
ok "Container restarted."
|
||||
fi
|
||||
|
||||
# --- Rotate old backups (keep last 5) ---
|
||||
# shellcheck disable=SC2012
|
||||
ls -t "${BACKUP_DIR}/backup-${DIR_NAME}-"*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm --
|
||||
|
||||
ok "Backup: ${BACKUP_FILE} ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
514
common.sh
Normal file
514
common.sh
Normal file
@@ -0,0 +1,514 @@
|
||||
# common.sh — Shared functions sourced by all deploy and backup scripts
|
||||
# This file should NOT be executed directly.
|
||||
#
|
||||
# Usage: source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# ──────────────────────────── VARIABLES ───────────────────────────────────────
|
||||
|
||||
ACME_EMAIL="${ACME_EMAIL:-admin@an2.io}"
|
||||
TRAEFIK_DIR="/opt/traefik"
|
||||
TRAEFIK_DYNAMIC_DIR="/opt/traefik/dynamic"
|
||||
DOCKER_NETWORK="traefik-public"
|
||||
|
||||
# ──────────────────────────── COLOURS / LOGGING ───────────────────────────────
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
fatal() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; }
|
||||
|
||||
# ──────────────────────────── ARGUMENT PARSING ────────────────────────────────
|
||||
|
||||
parse_args() {
|
||||
ARG_DOMAIN=""
|
||||
ARG_REMOVE="0"
|
||||
ARG_PURGE="0"
|
||||
ARG_YES="0"
|
||||
ARG_COLD="0"
|
||||
ARG_DIR=""
|
||||
ARG_EXTRA=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain)
|
||||
[[ $# -ge 2 ]] || fatal "--domain requires a value"
|
||||
ARG_DOMAIN="$2"; shift 2 ;;
|
||||
--dir)
|
||||
[[ $# -ge 2 ]] || fatal "--dir requires a value"
|
||||
ARG_DIR="$2"; shift 2 ;;
|
||||
--remove) ARG_REMOVE="1"; shift ;;
|
||||
--purge) ARG_PURGE="1"; shift ;;
|
||||
--yes) ARG_YES="1"; shift ;;
|
||||
--cold) ARG_COLD="1"; shift ;;
|
||||
*) ARG_EXTRA+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ──────────────────────────── REQUIRE ROOT ────────────────────────────────────
|
||||
|
||||
require_root() {
|
||||
[[ $EUID -eq 0 ]] || fatal "This script must be run as root (or via sudo)."
|
||||
}
|
||||
|
||||
# ──────────────────────────── OS DETECTION ────────────────────────────────────
|
||||
|
||||
detect_os() {
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/os-release
|
||||
OS_ID="${ID,,}"
|
||||
OS_VERSION="${VERSION_ID:-}"
|
||||
OS_NAME="${PRETTY_NAME}"
|
||||
else
|
||||
fatal "Cannot detect OS — /etc/os-release not found."
|
||||
fi
|
||||
|
||||
case "$OS_ID" in
|
||||
fedora|rhel|centos|rocky|alma|amzn)
|
||||
DISTRO_FAMILY="fedora" ;;
|
||||
debian|ubuntu|pop|linuxmint|raspbian|kali)
|
||||
DISTRO_FAMILY="debian" ;;
|
||||
*)
|
||||
fatal "Unsupported distro: $OS_ID ($OS_NAME). Supported: Fedora/RHEL family, Debian/Ubuntu family." ;;
|
||||
esac
|
||||
ok "Detected OS: $OS_NAME (family: $DISTRO_FAMILY)"
|
||||
}
|
||||
|
||||
# ──────────────────────────── INSTALL PREREQUISITES ───────────────────────────
|
||||
|
||||
install_prerequisites() {
|
||||
case "$DISTRO_FAMILY" in
|
||||
debian)
|
||||
info "Installing prerequisites (Debian/Ubuntu)..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq \
|
||||
apt-transport-https ca-certificates curl gnupg lsb-release \
|
||||
openssl jq net-tools > /dev/null 2>&1
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
info "Installing Docker Engine..."
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" | \
|
||||
gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
local codename
|
||||
codename="$(. /etc/os-release && echo "${VERSION_CODENAME:-$(lsb_release -cs)}")"
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/${OS_ID} ${codename} stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin > /dev/null 2>&1
|
||||
fi
|
||||
ok "Prerequisites installed (Debian/Ubuntu)."
|
||||
;;
|
||||
fedora)
|
||||
info "Installing prerequisites (Fedora/RHEL)..."
|
||||
dnf install -y -q \
|
||||
ca-certificates curl gnupg2 openssl jq net-tools > /dev/null 2>&1
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
info "Installing Docker Engine..."
|
||||
dnf -y install dnf-plugins-core > /dev/null 2>&1
|
||||
dnf config-manager --add-repo \
|
||||
https://download.docker.com/linux/fedora/docker-ce.repo 2>/dev/null || \
|
||||
dnf config-manager --add-repo \
|
||||
https://download.docker.com/linux/centos/docker-ce.repo 2>/dev/null
|
||||
dnf install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin > /dev/null 2>&1
|
||||
fi
|
||||
ok "Prerequisites installed (Fedora/RHEL)."
|
||||
;;
|
||||
esac
|
||||
|
||||
systemctl enable --now docker > /dev/null 2>&1
|
||||
ok "Docker $(docker --version | awk '{print $3}') is running."
|
||||
|
||||
if ! docker compose version &>/dev/null; then
|
||||
fatal "docker compose v2 plugin not found. Please install docker-compose-plugin."
|
||||
fi
|
||||
ok "Docker Compose $(docker compose version --short) available."
|
||||
}
|
||||
|
||||
# ──────────────────────────── DOCKER NETWORK ──────────────────────────────────
|
||||
|
||||
ensure_docker_network() {
|
||||
docker network inspect "$DOCKER_NETWORK" &>/dev/null || \
|
||||
docker network create "$DOCKER_NETWORK"
|
||||
ok "Docker network '$DOCKER_NETWORK' ready."
|
||||
}
|
||||
|
||||
# ──────────────────────────── TRAEFIK ─────────────────────────────────────────
|
||||
# Deploys shared Traefik at /opt/traefik/ if not already present.
|
||||
# Uses file provider (not Docker socket) — Docker Engine 29+ has API
|
||||
# negotiation bugs with Traefik's Go client, and the docs warn about
|
||||
# Docker socket security.
|
||||
|
||||
ensure_traefik() {
|
||||
# Already running — nothing to do
|
||||
if [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
if docker ps --format '{{.Names}}' | grep -q '^traefik$'; then
|
||||
ok "Traefik is already running."
|
||||
return 0
|
||||
fi
|
||||
info "Traefik compose exists but not running. Starting..."
|
||||
(cd "$TRAEFIK_DIR" && docker compose up -d)
|
||||
ok "Traefik started."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Full deploy
|
||||
info "Deploying Traefik (file provider, Let's Encrypt)..."
|
||||
|
||||
# Check port 443
|
||||
if ss -tlnp 2>/dev/null | grep -q ':443 '; then
|
||||
local proc
|
||||
proc="$(ss -tlnp 2>/dev/null | grep ':443 ')"
|
||||
if ! echo "$proc" | grep -q 'traefik'; then
|
||||
echo "$proc" | sed 's/^/ /'
|
||||
fatal "Port 443 is in use by another process. Free it before deploying Traefik."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Warn if port 80 is busy (LE HTTP challenge needs it)
|
||||
if ss -tlnp 2>/dev/null | grep -q ':80 '; then
|
||||
warn "Port 80 is in use. Let's Encrypt HTTP challenge requires port 80."
|
||||
fi
|
||||
|
||||
mkdir -p "$TRAEFIK_DIR" "$TRAEFIK_DYNAMIC_DIR"
|
||||
|
||||
touch "${TRAEFIK_DIR}/acme.json"
|
||||
chmod 600 "${TRAEFIK_DIR}/acme.json"
|
||||
|
||||
cat > "${TRAEFIK_DIR}/docker-compose.yml" <<EOF
|
||||
# Traefik reverse proxy — shared by all services
|
||||
# Static config via CLI flags (per official Traefik docs)
|
||||
# Dynamic routing: one .yml per service in ./dynamic/, hot-reloaded
|
||||
# NO Docker socket — secure, avoids Docker Engine 29+ API bugs
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.4
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 1G
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
command:
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=600s"
|
||||
- "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=600s"
|
||||
- "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=600s"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
|
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||
- "--providers.file.directory=/dynamic"
|
||||
- "--providers.file.watch=true"
|
||||
- "--log.level=WARN"
|
||||
volumes:
|
||||
- ./acme.json:/acme.json
|
||||
- ./dynamic:/dynamic:ro
|
||||
networks:
|
||||
- traefik-public
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
EOF
|
||||
|
||||
(cd "$TRAEFIK_DIR" && docker compose up -d)
|
||||
install_systemd_unit "traefik-docker" "$TRAEFIK_DIR"
|
||||
ok "Traefik deployed at $TRAEFIK_DIR."
|
||||
}
|
||||
|
||||
# ──────────────────────────── FIREWALL ────────────────────────────────────────
|
||||
|
||||
configure_firewall() {
|
||||
local extra_ports=("$@")
|
||||
|
||||
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "active"; then
|
||||
ufw allow 80/tcp > /dev/null 2>&1
|
||||
ufw allow 443/tcp > /dev/null 2>&1
|
||||
for port in "${extra_ports[@]}"; do
|
||||
ufw allow "$port" > /dev/null 2>&1
|
||||
done
|
||||
ok "UFW rules added (80, 443${extra_ports:+, ${extra_ports[*]}})."
|
||||
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then
|
||||
firewall-cmd --permanent --add-service=http > /dev/null 2>&1
|
||||
firewall-cmd --permanent --add-service=https > /dev/null 2>&1
|
||||
for port in "${extra_ports[@]}"; do
|
||||
firewall-cmd --permanent --add-port="$port" > /dev/null 2>&1
|
||||
done
|
||||
firewall-cmd --reload > /dev/null 2>&1
|
||||
ok "Firewalld rules added (http, https${extra_ports:+, ${extra_ports[*]}})."
|
||||
else
|
||||
warn "No active firewall detected. Ensure ports 80, 443${extra_ports:+, ${extra_ports[*]}} are open."
|
||||
fi
|
||||
}
|
||||
|
||||
# ──────────────────────────── TRAEFIK DYNAMIC CONFIG ──────────────────────────
|
||||
|
||||
write_traefik_dynamic_config() {
|
||||
local config_name="$1"
|
||||
local domain="$2"
|
||||
local backend_url="$3"
|
||||
|
||||
info "Writing Traefik dynamic config: ${config_name}.yml"
|
||||
|
||||
cat > "${TRAEFIK_DYNAMIC_DIR}/${config_name}.yml" <<EOF
|
||||
http:
|
||||
routers:
|
||||
${config_name}:
|
||||
rule: "Host(\`${domain}\`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
service: ${config_name}
|
||||
middlewares:
|
||||
- ${config_name}-headers
|
||||
|
||||
services:
|
||||
${config_name}:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "${backend_url}"
|
||||
|
||||
middlewares:
|
||||
${config_name}-headers:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: "https"
|
||||
stsSeconds: 63072000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
forceSTSHeader: true
|
||||
frameDeny: true
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
EOF
|
||||
|
||||
ok "Traefik dynamic config: ${TRAEFIK_DYNAMIC_DIR}/${config_name}.yml"
|
||||
}
|
||||
|
||||
remove_traefik_dynamic_config() {
|
||||
local config_name="$1"
|
||||
local config_file="${TRAEFIK_DYNAMIC_DIR}/${config_name}.yml"
|
||||
if [[ -f "$config_file" ]]; then
|
||||
rm -f "$config_file"
|
||||
ok "Removed Traefik dynamic config: ${config_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ──────────────────────────── BACKUP / RESTORE ────────────────────────────────
|
||||
|
||||
check_and_restore_backup() {
|
||||
local service_name="$1"
|
||||
local backup_file=""
|
||||
|
||||
# Search order: scp'd backups near the script, then server-side locations
|
||||
local search_paths=(
|
||||
"${SCRIPT_DIR}/backups/backup-${service_name}.tar.gz"
|
||||
"${SCRIPT_DIR}/backup-${service_name}.tar.gz"
|
||||
"/root/backup-${service_name}.tar.gz"
|
||||
)
|
||||
|
||||
for path in "${search_paths[@]}"; do
|
||||
if [[ -f "$path" ]]; then
|
||||
backup_file="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check /opt/backup/ for timestamped backups (most recent first)
|
||||
if [[ -z "$backup_file" ]]; then
|
||||
local latest
|
||||
latest="$(ls -t "/opt/backup/backup-${service_name}-"*.tar.gz 2>/dev/null | head -1)" || true
|
||||
if [[ -n "$latest" ]]; then
|
||||
backup_file="$latest"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$backup_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local size
|
||||
size="$(du -h "$backup_file" | cut -f1)"
|
||||
info "Found backup: $backup_file ($size)"
|
||||
|
||||
if [[ "$ARG_YES" != "1" ]]; then
|
||||
read -rp "Restore from this backup? [y/N] " confirm
|
||||
if [[ "${confirm,,}" != "y" ]]; then
|
||||
info "Skipping backup restore."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Restoring backup..."
|
||||
tar xf "$backup_file" -C /
|
||||
ok "Backup restored from $backup_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ──────────────────────────── HEALTH CHECK ────────────────────────────────────
|
||||
|
||||
wait_for_healthy() {
|
||||
local container_name="$1"
|
||||
local health_url="$2"
|
||||
local timeout="${3:-120}"
|
||||
|
||||
info "Waiting for ${container_name} to become healthy (up to ${timeout}s)..."
|
||||
local elapsed=0
|
||||
while (( elapsed < timeout )); do
|
||||
# Try curl first, fall back to wget (Alpine images lack curl)
|
||||
if docker exec "$container_name" curl -sf "$health_url" > /dev/null 2>&1 || \
|
||||
docker exec "$container_name" wget -q --spider "$health_url" 2>/dev/null; then
|
||||
ok "${container_name} is healthy."
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
(( elapsed += 5 ))
|
||||
done
|
||||
warn "${container_name} did not become healthy within ${timeout}s. Check: docker logs ${container_name}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ──────────────────────────── PASSWORD GENERATION ─────────────────────────────
|
||||
|
||||
generate_password() {
|
||||
openssl rand -base64 32 | tr -d '/+=' | head -c 32
|
||||
}
|
||||
|
||||
# ──────────────────────────── SYSTEMD UNITS ───────────────────────────────────
|
||||
|
||||
install_systemd_unit() {
|
||||
local unit_name="$1"
|
||||
local working_directory="$2"
|
||||
|
||||
cat > "/etc/systemd/system/${unit_name}.service" <<EOF
|
||||
[Unit]
|
||||
Description=${unit_name} Docker Compose Stack
|
||||
Requires=docker.service
|
||||
After=docker.service traefik-docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=${working_directory}
|
||||
ExecStart=/usr/bin/docker compose up -d
|
||||
ExecStop=/usr/bin/docker compose down
|
||||
TimeoutStartSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${unit_name}.service" > /dev/null 2>&1
|
||||
ok "Systemd unit installed: ${unit_name}.service"
|
||||
}
|
||||
|
||||
remove_systemd_unit() {
|
||||
local unit_name="$1"
|
||||
local unit_file="/etc/systemd/system/${unit_name}.service"
|
||||
|
||||
if [[ -f "$unit_file" ]]; then
|
||||
systemctl stop "${unit_name}.service" > /dev/null 2>&1 || true
|
||||
systemctl disable "${unit_name}.service" > /dev/null 2>&1 || true
|
||||
rm -f "$unit_file"
|
||||
systemctl daemon-reload
|
||||
ok "Removed systemd unit: ${unit_name}.service"
|
||||
fi
|
||||
}
|
||||
|
||||
# ──────────────────────────── REMOVAL ─────────────────────────────────────────
|
||||
|
||||
do_remove() {
|
||||
local service_name="$1"
|
||||
local config_name="$2"
|
||||
local unit_name="$3"
|
||||
local base_dir="$4"
|
||||
|
||||
local backup_script="${SCRIPT_DIR}/backup-${service_name}.sh"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Removing ${service_name}${NC}"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
echo -e " Mode: ${RED}Purge (delete all data)${NC}"
|
||||
else
|
||||
echo -e " Mode: Safe (keep data)"
|
||||
fi
|
||||
echo -e " Data: ${base_dir}"
|
||||
[[ -n "$config_name" ]] && \
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${config_name}.yml"
|
||||
echo -e " Systemd: ${unit_name}.service"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Suggest backup before purge
|
||||
if [[ "$ARG_PURGE" == "1" ]] && [[ -d "$base_dir" ]]; then
|
||||
if [[ -f "$backup_script" ]]; then
|
||||
warn "Back up first? ${backup_script}"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Stop containers
|
||||
if [[ -d "$base_dir" ]] && [[ -f "${base_dir}/docker-compose.yml" ]]; then
|
||||
(cd "$base_dir" && docker compose down) || true
|
||||
ok "Containers stopped."
|
||||
fi
|
||||
|
||||
# Remove traefik dynamic config
|
||||
[[ -n "$config_name" ]] && remove_traefik_dynamic_config "$config_name"
|
||||
|
||||
# Remove systemd unit
|
||||
remove_systemd_unit "$unit_name"
|
||||
|
||||
# Purge data if requested
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
echo ""
|
||||
warn "This will permanently delete: $base_dir"
|
||||
if [[ "$ARG_YES" != "1" ]]; then
|
||||
read -rp "Are you sure? [y/N] " confirm
|
||||
if [[ "${confirm,,}" != "y" ]]; then
|
||||
info "Purge cancelled."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
rm -rf "$base_dir"
|
||||
ok "Purged: $base_dir"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} ${service_name} removed${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" != "1" ]]; then
|
||||
echo -e " Data preserved at ${base_dir}"
|
||||
echo -e " To purge: $0 --remove --purge"
|
||||
fi
|
||||
echo -e " To redeploy: $0"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
return 0
|
||||
}
|
||||
199
deploy
Executable file
199
deploy
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Unified Deploy Wrapper
|
||||
# ==============================================================================
|
||||
# Auto-discovers deploy-{docker,local}-*.sh scripts in the same directory and
|
||||
# dispatches to them with a shorter CLI interface.
|
||||
#
|
||||
# Usage:
|
||||
# deploy.sh # List available services
|
||||
# deploy.sh mattermost # Deploy via docker (default)
|
||||
# deploy.sh -l paperclip # Deploy via local (bare-metal)
|
||||
# deploy.sh --local hermes --domain h.an2.io # Local deploy with flags
|
||||
# deploy.sh -r mattermost # --remove
|
||||
# deploy.sh -r -p mattermost # --remove --purge
|
||||
# deploy.sh -r -p -y mattermost # --remove --purge --yes
|
||||
# deploy.sh web --domain w.an2.io --dir acme # Pass-through flags
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
|
||||
|
||||
# ── Discover available deploy scripts ────────────────────────────────────────
|
||||
# Builds two associative arrays:
|
||||
# DOCKER_SCRIPTS[service] = script_path
|
||||
# LOCAL_SCRIPTS[service] = script_path
|
||||
# And a sorted list of unique service names.
|
||||
|
||||
declare -A DOCKER_SCRIPTS=()
|
||||
declare -A LOCAL_SCRIPTS=()
|
||||
declare -a ALL_SERVICES=()
|
||||
|
||||
discover_scripts() {
|
||||
local script basename method service
|
||||
for script in "${SCRIPT_DIR}"/deploy-docker-*.sh "${SCRIPT_DIR}"/deploy-local-*.sh; do
|
||||
[[ -f "$script" ]] || continue
|
||||
basename="$(basename "$script" .sh)" # deploy-docker-hermes
|
||||
method="${basename#deploy-}" # docker-hermes
|
||||
method="${method%%-*}" # docker or local
|
||||
service="${basename#deploy-${method}-}" # hermes
|
||||
|
||||
case "$method" in
|
||||
docker) DOCKER_SCRIPTS["$service"]="$script" ;;
|
||||
local) LOCAL_SCRIPTS["$service"]="$script" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build sorted unique service list
|
||||
local -A seen=()
|
||||
for s in "${!DOCKER_SCRIPTS[@]}" "${!LOCAL_SCRIPTS[@]}"; do
|
||||
seen["$s"]=1
|
||||
done
|
||||
IFS=$'\n' read -r -d '' -a ALL_SERVICES < <(printf '%s\n' "${!seen[@]}" | sort && printf '\0') || true
|
||||
}
|
||||
|
||||
# ── Usage / list ─────────────────────────────────────────────────────────────
|
||||
|
||||
print_usage() {
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploy — Unified Wrapper${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Usage:${NC} deploy.sh [flags] <service>"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Flags:${NC}"
|
||||
echo -e " ${GREEN}--local, -l${NC} Use bare-metal (local) deploy instead of Docker"
|
||||
echo -e " ${GREEN}--remove, -r${NC} Remove the service (keep data)"
|
||||
echo -e " ${GREEN}--purge, -p${NC} Delete all data (requires --remove)"
|
||||
echo -e " ${GREEN}--yes, -y${NC} Skip confirmations"
|
||||
echo -e " ${GREEN}--domain <fqdn>${NC} Set the service domain"
|
||||
echo -e " ${GREEN}--dir <name>${NC} Instance directory (for web)"
|
||||
echo -e " ${GREEN}--cold${NC} Cold backup mode (passed through)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Available services:${NC}"
|
||||
echo ""
|
||||
|
||||
# Table header
|
||||
printf " ${DIM}%-16s %-8s %-8s${NC}\n" "SERVICE" "DOCKER" "LOCAL"
|
||||
printf " ${DIM}%-16s %-8s %-8s${NC}\n" "───────────────" "────────" "────────"
|
||||
|
||||
for service in "${ALL_SERVICES[@]}"; do
|
||||
local d_mark l_mark
|
||||
if [[ -n "${DOCKER_SCRIPTS[$service]+x}" ]]; then
|
||||
d_mark="${GREEN} ✓${NC}"
|
||||
else
|
||||
d_mark="${DIM} —${NC}"
|
||||
fi
|
||||
if [[ -n "${LOCAL_SCRIPTS[$service]+x}" ]]; then
|
||||
l_mark="${GREEN} ✓${NC}"
|
||||
else
|
||||
l_mark="${DIM} —${NC}"
|
||||
fi
|
||||
printf " %-16s %b %b\n" "$service" "$d_mark" "$l_mark"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}Examples:${NC}"
|
||||
echo -e " ${DIM}deploy.sh mattermost${NC} # Docker deploy"
|
||||
echo -e " ${DIM}deploy.sh -l paperclip --domain clip.an2.io${NC} # Local deploy"
|
||||
echo -e " ${DIM}deploy.sh -r -p gitea${NC} # Remove + purge"
|
||||
echo -e " ${DIM}deploy.sh web --domain w.an2.io --dir acme${NC} # Multi-instance"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
discover_scripts
|
||||
|
||||
# Parse wrapper flags — separate them from pass-through flags
|
||||
USE_LOCAL=0
|
||||
SERVICE=""
|
||||
PASSTHROUGH=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--local|-l)
|
||||
USE_LOCAL=1
|
||||
shift
|
||||
;;
|
||||
--remove|-r)
|
||||
PASSTHROUGH+=("--remove")
|
||||
shift
|
||||
;;
|
||||
--purge|-p)
|
||||
PASSTHROUGH+=("--purge")
|
||||
shift
|
||||
;;
|
||||
--yes|-y)
|
||||
PASSTHROUGH+=("--yes")
|
||||
shift
|
||||
;;
|
||||
--domain)
|
||||
PASSTHROUGH+=("--domain" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--dir)
|
||||
PASSTHROUGH+=("--dir" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--cold)
|
||||
PASSTHROUGH+=("--cold")
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
# Unknown flag — pass through
|
||||
PASSTHROUGH+=("$1")
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$SERVICE" ]]; then
|
||||
SERVICE="$1"
|
||||
else
|
||||
# Extra positional — pass through
|
||||
PASSTHROUGH+=("$1")
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# No service specified — show usage
|
||||
if [[ -z "$SERVICE" ]]; then
|
||||
print_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Resolve script
|
||||
TARGET_SCRIPT=""
|
||||
if (( USE_LOCAL )); then
|
||||
if [[ -n "${LOCAL_SCRIPTS[$SERVICE]+x}" ]]; then
|
||||
TARGET_SCRIPT="${LOCAL_SCRIPTS[$SERVICE]}"
|
||||
else
|
||||
echo -e "${RED}[FAIL]${NC} No local deploy script for '${SERVICE}'."
|
||||
if [[ -n "${DOCKER_SCRIPTS[$SERVICE]+x}" ]]; then
|
||||
echo -e " A Docker variant exists — run without ${GREEN}-l${NC}."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [[ -n "${DOCKER_SCRIPTS[$SERVICE]+x}" ]]; then
|
||||
TARGET_SCRIPT="${DOCKER_SCRIPTS[$SERVICE]}"
|
||||
elif [[ -n "${LOCAL_SCRIPTS[$SERVICE]+x}" ]]; then
|
||||
echo -e "${YELLOW}[WARN]${NC} No Docker variant for '${SERVICE}' — using local."
|
||||
TARGET_SCRIPT="${LOCAL_SCRIPTS[$SERVICE]}"
|
||||
else
|
||||
echo -e "${RED}[FAIL]${NC} Unknown service '${SERVICE}'."
|
||||
echo ""
|
||||
echo -e " Available: ${ALL_SERVICES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Dispatch
|
||||
exec bash "$TARGET_SCRIPT" "${PASSTHROUGH[@]}"
|
||||
173
deploy-docker-agenticseek.sh
Executable file
173
deploy-docker-agenticseek.sh
Executable file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# AgenticSeek Deployment Script (Docker Compose + Traefik)
|
||||
# ==============================================================================
|
||||
# Clones and deploys AgenticSeek (multi-container) behind Traefik.
|
||||
# Source: https://github.com/Fosowl/agenticSeek
|
||||
#
|
||||
# Usage: deploy-docker-agenticseek.sh
|
||||
# deploy-docker-agenticseek.sh --domain agent.an2.io
|
||||
# deploy-docker-agenticseek.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="agenticseek"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}-docker"
|
||||
AGENTICSEEK_REPO="${AGENTICSEEK_REPO:-https://github.com/Fosowl/agenticSeek.git}"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Domain resolution ---
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
DOMAIN="seek.an2.io"
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying AgenticSeek (Docker)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
# AgenticSeek uses repo compose — check for frontend container
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker compose -f "${BASE_DIR}/docker-compose.yml" ps --format '{{.State}}' 2>/dev/null | grep -q 'running'; then
|
||||
echo -e "${GREEN} AgenticSeek is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: cd ${BASE_DIR} && docker compose logs -f"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-agenticseek.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
# --- Fresh install / update ---
|
||||
|
||||
# Clone or update repo
|
||||
if [[ -d "${BASE_DIR}/.git" ]]; then
|
||||
info "Updating AgenticSeek source..."
|
||||
(cd "$BASE_DIR" && git pull)
|
||||
else
|
||||
info "Cloning AgenticSeek from ${AGENTICSEEK_REPO}..."
|
||||
git clone "$AGENTICSEEK_REPO" "$BASE_DIR"
|
||||
fi
|
||||
|
||||
# Set up .env from example if fresh
|
||||
if [[ ! -f "${BASE_DIR}/.env" ]] && [[ -f "${BASE_DIR}/.env.example" ]]; then
|
||||
cp "${BASE_DIR}/.env.example" "${BASE_DIR}/.env"
|
||||
fi
|
||||
|
||||
# Ensure our DOMAIN is in .env
|
||||
if grep -q '^DOMAIN=' "${BASE_DIR}/.env" 2>/dev/null; then
|
||||
sed -i "s|^DOMAIN=.*|DOMAIN=${DOMAIN}|" "${BASE_DIR}/.env"
|
||||
else
|
||||
echo "DOMAIN=${DOMAIN}" >> "${BASE_DIR}/.env"
|
||||
fi
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
|
||||
mkdir -p "${BASE_DIR}/data"
|
||||
|
||||
# Create override to add traefik-public network to frontend
|
||||
cat > "${BASE_DIR}/docker-compose.override.yml" <<'OVERRIDE'
|
||||
services:
|
||||
frontend:
|
||||
networks:
|
||||
- default
|
||||
- traefik-public
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
OVERRIDE
|
||||
ok "docker-compose.override.yml written (traefik-public network)."
|
||||
fi
|
||||
|
||||
# --- Always do these (fresh or restored) ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://frontend:3000"
|
||||
|
||||
# AgenticSeek uses repo compose + our override — no custom systemd unit for compose
|
||||
# since the compose file is the repo's own, install unit pointing to BASE_DIR
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d --build)
|
||||
|
||||
info "Waiting for AgenticSeek frontend (up to 120s)..."
|
||||
elapsed=0
|
||||
while (( elapsed < 120 )); do
|
||||
if curl -sf "http://localhost:3000" > /dev/null 2>&1; then
|
||||
ok "AgenticSeek frontend is responding."
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
(( elapsed += 5 ))
|
||||
done
|
||||
if (( elapsed >= 120 )); then
|
||||
warn "Frontend did not respond within 120s. Check: cd ${BASE_DIR} && docker compose logs"
|
||||
fi
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} AgenticSeek deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: cd ${BASE_DIR} && docker compose logs -f"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Configure API keys in ${BASE_DIR}/.env${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
218
deploy-docker-gitea.sh
Executable file
218
deploy-docker-gitea.sh
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Gitea Deployment Script (Docker Compose + Traefik)
|
||||
# ==============================================================================
|
||||
# Deploys Gitea + PostgreSQL behind the shared Traefik reverse proxy.
|
||||
#
|
||||
# Usage: deploy-docker-gitea.sh
|
||||
# deploy-docker-gitea.sh --domain git.us.an2.io
|
||||
# deploy-docker-gitea.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="gitea"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}-docker"
|
||||
SSH_PORT="${SSH_PORT:-222}"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Domain resolution: --domain > existing .env > default ---
|
||||
# Read domain from .env without sourcing (avoids clobbering other vars)
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
DOMAIN="git.an2.io"
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Gitea (Docker)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " SSH Port: ${SSH_PORT}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^gitea$'; then
|
||||
echo -e "${GREEN} Gitea is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: docker logs -f gitea"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-gitea.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall "${SSH_PORT}/tcp"
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
# Backup restored — source .env for credentials, but keep our resolved domain
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
# --- Fresh install ---
|
||||
|
||||
# Preserve credentials from existing .env on re-run
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
POSTGRES_USER="gitea"
|
||||
POSTGRES_PASSWORD="$(generate_password)"
|
||||
POSTGRES_DB="gitea"
|
||||
fi
|
||||
|
||||
info "Creating directory layout..."
|
||||
mkdir -p "${BASE_DIR}"/{postgres/data,gitea/data}
|
||||
|
||||
# --- Generate .env ---
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-docker-gitea.sh on $(date -Iseconds)
|
||||
DOMAIN=${DOMAIN}
|
||||
SSH_PORT=${SSH_PORT}
|
||||
POSTGRES_USER=${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB=${POSTGRES_DB}
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
ok ".env written."
|
||||
|
||||
# --- Generate docker-compose.yml ---
|
||||
cat > "${BASE_DIR}/docker-compose.yml" <<'COMPOSE'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: gitea-postgres
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 2G
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/run/postgresql
|
||||
volumes:
|
||||
- ./postgres/data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
- POSTGRES_DB
|
||||
networks:
|
||||
- gitea-internal
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:1.25.5
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_started
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 2G
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=gitea-postgres:5432
|
||||
- GITEA__database__NAME=${POSTGRES_DB}
|
||||
- GITEA__database__USER=${POSTGRES_USER}
|
||||
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
|
||||
- GITEA__server__ROOT_URL=https://${DOMAIN}
|
||||
- GITEA__server__SSH_DOMAIN=${DOMAIN}
|
||||
- GITEA__server__SSH_PORT=${SSH_PORT}
|
||||
volumes:
|
||||
- ./gitea/data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "${SSH_PORT}:22"
|
||||
networks:
|
||||
- gitea-internal
|
||||
- traefik-public
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
gitea-internal:
|
||||
driver: bridge
|
||||
traefik-public:
|
||||
external: true
|
||||
COMPOSE
|
||||
ok "docker-compose.yml written."
|
||||
fi
|
||||
|
||||
# --- Always do these (fresh or restored) ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://gitea:3000"
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
wait_for_healthy "gitea" "http://localhost:3000/" 120
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Gitea deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " SSH: ssh://git@${DOMAIN}:${SSH_PORT}"
|
||||
echo -e " Data: ${BASE_DIR}/"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f gitea"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
134
deploy-docker-hermes.sh
Executable file
134
deploy-docker-hermes.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Hermes Agent Deployment Script (Docker — Gateway Mode)
|
||||
# ==============================================================================
|
||||
# Deploys Hermes Agent as a gateway connecting to Telegram/Discord/Slack/WhatsApp.
|
||||
# No web UI — does NOT use Traefik.
|
||||
#
|
||||
# First run requires interactive setup for API keys:
|
||||
# docker run -it --rm -v /opt/hermes/data:/opt/data nousresearch/hermes-agent setup
|
||||
#
|
||||
# Usage: deploy-docker-hermes.sh
|
||||
# deploy-docker-hermes.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="hermes"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="" # No Traefik
|
||||
UNIT_NAME="${SERVICE_NAME}-docker"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Hermes Agent (Docker/Gateway)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Traefik: Not used (no web UI)"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^hermes$'; then
|
||||
echo -e "${GREEN} Hermes is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: docker logs -f hermes"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-hermes.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Infrastructure (Docker only, no Traefik) ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
ok "Backup restored."
|
||||
else
|
||||
# --- Fresh install ---
|
||||
mkdir -p "${BASE_DIR}/data"
|
||||
|
||||
if [[ ! -f "${BASE_DIR}/.env" ]]; then
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-docker-hermes.sh on $(date -Iseconds)
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
fi
|
||||
|
||||
# --- Generate docker-compose.yml ---
|
||||
cat > "${BASE_DIR}/docker-compose.yml" <<'COMPOSE'
|
||||
services:
|
||||
hermes:
|
||||
image: nousresearch/hermes-agent:latest
|
||||
container_name: hermes
|
||||
restart: unless-stopped
|
||||
command: gateway run
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 4G
|
||||
shm_size: 1g
|
||||
volumes:
|
||||
- ./data:/opt/data
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
COMPOSE
|
||||
ok "docker-compose.yml written."
|
||||
fi
|
||||
|
||||
# --- Start ---
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
|
||||
# Health check — hermes doesn't have HTTP, use version command
|
||||
info "Checking Hermes is running..."
|
||||
sleep 5
|
||||
if docker exec hermes hermes version > /dev/null 2>&1; then
|
||||
ok "Hermes is running."
|
||||
else
|
||||
warn "Hermes container started but 'hermes version' failed. May need initial setup."
|
||||
fi
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Hermes Agent deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f hermes"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}First run? Set up API keys:${NC}"
|
||||
echo -e " docker run -it --rm -v ${BASE_DIR}/data:/opt/data nousresearch/hermes-agent setup"
|
||||
echo -e " Then: cd ${BASE_DIR} && docker compose restart"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
255
deploy-docker-mattermost.sh
Executable file
255
deploy-docker-mattermost.sh
Executable file
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Mattermost Deployment Script (Docker Compose + Traefik)
|
||||
# ==============================================================================
|
||||
# Deploys Mattermost + PostgreSQL behind the shared Traefik reverse proxy.
|
||||
# ChatOps-optimized: bot accounts, webhooks, personal access tokens enabled.
|
||||
#
|
||||
# Usage: deploy-docker-mattermost.sh
|
||||
# deploy-docker-mattermost.sh --domain chat.us.an2.io
|
||||
# deploy-docker-mattermost.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="mattermost"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}-docker"
|
||||
CALLS_PORT="${CALLS_PORT:-8443}"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Domain resolution: --domain > existing .env > default ---
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
DOMAIN="mm.an2.io"
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Mattermost (Docker)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Calls Port: ${CALLS_PORT}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^mm-app$'; then
|
||||
echo -e "${GREEN} Mattermost is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: docker logs -f mm-app"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-mattermost.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall "${CALLS_PORT}/udp" "${CALLS_PORT}/tcp"
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
# --- Fresh install ---
|
||||
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
POSTGRES_USER="mmuser"
|
||||
POSTGRES_PASSWORD="$(generate_password)"
|
||||
POSTGRES_DB="mattermost"
|
||||
fi
|
||||
|
||||
info "Creating directory layout..."
|
||||
mkdir -p "${BASE_DIR}"/{postgres/data,mattermost/{config,data,logs,plugins,client-plugins,bleve-indexes}}
|
||||
chown -R 2000:2000 "${BASE_DIR}/mattermost"
|
||||
|
||||
# --- Generate .env ---
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-docker-mattermost.sh on $(date -Iseconds)
|
||||
DOMAIN=${DOMAIN}
|
||||
TZ=UTC
|
||||
CALLS_PORT=${CALLS_PORT}
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_USER=${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB=${POSTGRES_DB}
|
||||
|
||||
# Mattermost
|
||||
MM_SQLSETTINGS_DRIVERNAME=postgres
|
||||
MM_SQLSETTINGS_DATASOURCE=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@mm-postgres:5432/${POSTGRES_DB}?sslmode=disable&connect_timeout=10
|
||||
MM_BLEVESETTINGS_INDEXDIR=/mattermost/bleve-indexes
|
||||
MM_SERVICESETTINGS_SITEURL=https://${DOMAIN}
|
||||
|
||||
# ChatOps settings
|
||||
MM_SERVICESETTINGS_ENABLEBOTACCOUNTCREATION=true
|
||||
MM_SERVICESETTINGS_ENABLEOAUTHSERVICEPROVIDER=true
|
||||
MM_SERVICESETTINGS_ENABLEINCOMINGWEBHOOKS=true
|
||||
MM_SERVICESETTINGS_ENABLEOUTGOINGWEBHOOKS=true
|
||||
MM_SERVICESETTINGS_ENABLEPOSTUSERNAMEOVERRIDE=true
|
||||
MM_SERVICESETTINGS_ENABLEPOSTICONOVERRIDE=true
|
||||
MM_SERVICESETTINGS_ENABLECUSTOMEMOJI=true
|
||||
MM_SERVICESETTINGS_ENABLEUSERACCESSTOKENS=true
|
||||
MM_PLUGINSETTINGS_ENABLEUPLOADS=true
|
||||
MM_TEAMSETTINGS_ENABLEOPENSERVER=false
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
ok ".env written."
|
||||
|
||||
# --- Generate docker-compose.yml ---
|
||||
cat > "${BASE_DIR}/docker-compose.yml" <<'COMPOSE'
|
||||
# Mattermost + PostgreSQL
|
||||
# Based on https://github.com/mattermost/docker
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: mm-postgres
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 4G
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/run/postgresql
|
||||
volumes:
|
||||
- ./postgres/data:/var/lib/postgresql
|
||||
environment:
|
||||
- TZ
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
- POSTGRES_DB
|
||||
networks:
|
||||
- mm-internal
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
mattermost:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_started
|
||||
image: mattermost/mattermost-enterprise-edition:release-11.0
|
||||
container_name: mm-app
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 4G
|
||||
tmpfs:
|
||||
- /tmp
|
||||
volumes:
|
||||
- ./mattermost/config:/mattermost/config:rw
|
||||
- ./mattermost/data:/mattermost/data:rw
|
||||
- ./mattermost/logs:/mattermost/logs:rw
|
||||
- ./mattermost/plugins:/mattermost/plugins:rw
|
||||
- ./mattermost/client-plugins:/mattermost/client/plugins:rw
|
||||
- ./mattermost/bleve-indexes:/mattermost/bleve-indexes:rw
|
||||
environment:
|
||||
- TZ
|
||||
- MM_SQLSETTINGS_DRIVERNAME
|
||||
- MM_SQLSETTINGS_DATASOURCE
|
||||
- MM_BLEVESETTINGS_INDEXDIR
|
||||
- MM_SERVICESETTINGS_SITEURL
|
||||
- MM_SERVICESETTINGS_ENABLEBOTACCOUNTCREATION
|
||||
- MM_SERVICESETTINGS_ENABLEOAUTHSERVICEPROVIDER
|
||||
- MM_SERVICESETTINGS_ENABLEINCOMINGWEBHOOKS
|
||||
- MM_SERVICESETTINGS_ENABLEOUTGOINGWEBHOOKS
|
||||
- MM_SERVICESETTINGS_ENABLEPOSTUSERNAMEOVERRIDE
|
||||
- MM_SERVICESETTINGS_ENABLEPOSTICONOVERRIDE
|
||||
- MM_SERVICESETTINGS_ENABLECUSTOMEMOJI
|
||||
- MM_SERVICESETTINGS_ENABLEUSERACCESSTOKENS
|
||||
- MM_PLUGINSETTINGS_ENABLEUPLOADS
|
||||
- MM_TEAMSETTINGS_ENABLEOPENSERVER
|
||||
ports:
|
||||
- "${CALLS_PORT}:${CALLS_PORT}/udp"
|
||||
- "${CALLS_PORT}:${CALLS_PORT}/tcp"
|
||||
networks:
|
||||
- mm-internal
|
||||
- traefik-public
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
mm-internal:
|
||||
driver: bridge
|
||||
traefik-public:
|
||||
external: true
|
||||
COMPOSE
|
||||
ok "docker-compose.yml written."
|
||||
fi
|
||||
|
||||
# --- Always do these (fresh or restored) ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://mm-app:8065"
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
wait_for_healthy "mm-app" "http://localhost:8443/api/v4/system/ping" 120
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Mattermost deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " Calls: Port ${CALLS_PORT} (UDP+TCP)"
|
||||
echo -e " Data: ${BASE_DIR}/"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f mm-app"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Next: Open https://${DOMAIN} and create your admin account.${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
169
deploy-docker-open-webui.sh
Executable file
169
deploy-docker-open-webui.sh
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Open WebUI Deployment Script (Docker Compose + Traefik)
|
||||
# ==============================================================================
|
||||
# Deploys Open WebUI behind the shared Traefik reverse proxy.
|
||||
#
|
||||
# Usage: deploy-docker-open-webui.sh
|
||||
# deploy-docker-open-webui.sh --domain chat.us.an2.io
|
||||
# deploy-docker-open-webui.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="open-webui"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}-docker"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Domain resolution: --domain > existing .env > default ---
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
DOMAIN="ai.an2.io"
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Open WebUI (Docker)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^open-webui$'; then
|
||||
echo -e "${GREEN} Open WebUI is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: docker logs -f open-webui"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-open-webui.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
# --- Fresh install ---
|
||||
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
WEBUI_SECRET_KEY="$(openssl rand -hex 32)"
|
||||
fi
|
||||
|
||||
info "Creating directory layout..."
|
||||
mkdir -p "${BASE_DIR}/data"
|
||||
|
||||
# --- Generate .env ---
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-docker-open-webui.sh on $(date -Iseconds)
|
||||
DOMAIN=${DOMAIN}
|
||||
WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
ok ".env written."
|
||||
|
||||
# --- Generate docker-compose.yml ---
|
||||
cat > "${BASE_DIR}/docker-compose.yml" <<'COMPOSE'
|
||||
services:
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 4G
|
||||
volumes:
|
||||
- ./data:/app/backend/data
|
||||
environment:
|
||||
- WEBUI_SECRET_KEY
|
||||
- WEBUI_AUTH=False
|
||||
# Use host gateway so container can reach host-side Ollama
|
||||
- OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- traefik-public
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
COMPOSE
|
||||
ok "docker-compose.yml written."
|
||||
fi
|
||||
|
||||
# --- Always do these (fresh or restored) ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://open-webui:8080"
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
wait_for_healthy "open-webui" "http://localhost:8080" 120
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Open WebUI deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Auth: disabled (single-user mode)"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f open-webui"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
162
deploy-docker-openclaw.sh
Executable file
162
deploy-docker-openclaw.sh
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# OpenClaw Deployment Script (Docker Compose + Traefik)
|
||||
# ==============================================================================
|
||||
# Deploys OpenClaw gateway behind the shared Traefik reverse proxy.
|
||||
# Source: https://docs.openclaw.ai/install/docker
|
||||
#
|
||||
# Usage: deploy-docker-openclaw.sh
|
||||
# deploy-docker-openclaw.sh --domain claw.an2.io
|
||||
# deploy-docker-openclaw.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="openclaw"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}-docker"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Domain resolution ---
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
DOMAIN="openclaw.an2.io"
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying OpenClaw (Docker)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^openclaw$'; then
|
||||
echo -e "${GREEN} OpenClaw is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: docker logs -f openclaw"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-openclaw.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
# --- Fresh install ---
|
||||
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
fi
|
||||
|
||||
mkdir -p "${BASE_DIR}/data"
|
||||
chown -R 1000:1000 "${BASE_DIR}/data"
|
||||
|
||||
# --- Generate .env ---
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-docker-openclaw.sh on $(date -Iseconds)
|
||||
DOMAIN=${DOMAIN}
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
ok ".env written."
|
||||
|
||||
# --- Generate docker-compose.yml ---
|
||||
cat > "${BASE_DIR}/docker-compose.yml" <<'COMPOSE'
|
||||
services:
|
||||
openclaw:
|
||||
image: ghcr.io/openclaw/openclaw:latest
|
||||
container_name: openclaw
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 2G
|
||||
user: "1000:1000"
|
||||
environment:
|
||||
- OPENCLAW_CONFIG_DIR=/home/node/.openclaw
|
||||
volumes:
|
||||
- ./data:/home/node/.openclaw
|
||||
networks:
|
||||
- traefik-public
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
COMPOSE
|
||||
ok "docker-compose.yml written."
|
||||
fi
|
||||
|
||||
# --- Always do these (fresh or restored) ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://openclaw:18789"
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
wait_for_healthy "openclaw" "http://localhost:18789/healthz" 120
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} OpenClaw deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f openclaw"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
216
deploy-docker-paperclip.sh
Executable file
216
deploy-docker-paperclip.sh
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Paperclip Deployment Script (Docker Compose + Traefik)
|
||||
# ==============================================================================
|
||||
# Builds and deploys Paperclip from source behind the shared Traefik reverse proxy.
|
||||
# Source: https://github.com/paperclipai/paperclip
|
||||
#
|
||||
# Usage: deploy-docker-paperclip.sh
|
||||
# deploy-docker-paperclip.sh --domain clip.us.an2.io
|
||||
# deploy-docker-paperclip.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="paperclip"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}-docker"
|
||||
PAPERCLIP_REPO="${PAPERCLIP_REPO:-https://github.com/paperclipai/paperclip.git}"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Domain resolution ---
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
DOMAIN="paperclip.an2.io"
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Paperclip (Docker)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^paperclip$'; then
|
||||
echo -e "${GREEN} Paperclip is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: docker logs -f paperclip"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-paperclip.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
# --- Fresh install ---
|
||||
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
fi
|
||||
|
||||
mkdir -p "${BASE_DIR}/data" "${BASE_DIR}/postgres/data"
|
||||
|
||||
# Clone or update source
|
||||
if [[ -d "${BASE_DIR}/repo/.git" ]]; then
|
||||
info "Updating source..."
|
||||
(cd "${BASE_DIR}/repo" && git pull)
|
||||
else
|
||||
info "Cloning Paperclip from ${PAPERCLIP_REPO}..."
|
||||
git clone "$PAPERCLIP_REPO" "${BASE_DIR}/repo"
|
||||
fi
|
||||
|
||||
# --- Generate .env (preserve existing passwords on re-run) ---
|
||||
if [[ -z "${DB_PASS:-}" ]]; then
|
||||
DB_PASS="$(generate_password)"
|
||||
fi
|
||||
if [[ -z "${BETTER_AUTH_SECRET:-}" ]]; then
|
||||
BETTER_AUTH_SECRET="$(generate_password)"
|
||||
fi
|
||||
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-docker-paperclip.sh on $(date -Iseconds)
|
||||
DOMAIN=${DOMAIN}
|
||||
DB_PASS=${DB_PASS}
|
||||
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
ok ".env written."
|
||||
|
||||
# --- Generate docker-compose.yml ---
|
||||
cat > "${BASE_DIR}/docker-compose.yml" <<COMPOSE
|
||||
services:
|
||||
paperclip-postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: paperclip-postgres
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 1G
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/run/postgresql
|
||||
environment:
|
||||
- POSTGRES_USER=paperclip
|
||||
- POSTGRES_PASSWORD=${DB_PASS}
|
||||
- POSTGRES_DB=paperclip
|
||||
volumes:
|
||||
- ./postgres/data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- paperclip-internal
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
paperclip:
|
||||
build: ./repo
|
||||
container_name: paperclip
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- paperclip-postgres
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 4G
|
||||
environment:
|
||||
- PORT=3100
|
||||
- HOST=0.0.0.0
|
||||
- SERVE_UI=true
|
||||
- DATABASE_URL=postgres://paperclip:${DB_PASS}@paperclip-postgres:5432/paperclip
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
||||
volumes:
|
||||
- ./data:/paperclip
|
||||
networks:
|
||||
- paperclip-internal
|
||||
- traefik-public
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
paperclip-internal:
|
||||
driver: bridge
|
||||
traefik-public:
|
||||
external: true
|
||||
COMPOSE
|
||||
ok "docker-compose.yml written."
|
||||
|
||||
info "Building Paperclip image (this may take a few minutes)..."
|
||||
(cd "$BASE_DIR" && docker compose build)
|
||||
fi
|
||||
|
||||
# --- Always do these (fresh or restored) ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://paperclip:3100"
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
wait_for_healthy "paperclip" "http://localhost:3100/api/health" 120
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Paperclip deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f paperclip"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
140
deploy-docker-traefik.sh
Executable file
140
deploy-docker-traefik.sh
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Standalone Traefik Deployment
|
||||
# ==============================================================================
|
||||
# Deploys the shared Traefik reverse proxy at /opt/traefik/.
|
||||
# Usually auto-bootstrapped by the first service deploy script — this script
|
||||
# is only needed if you want to set up infrastructure ahead of time.
|
||||
#
|
||||
# Usage: deploy-docker-traefik.sh
|
||||
# deploy-docker-traefik.sh --remove # Stop, keep certs + configs
|
||||
# deploy-docker-traefik.sh --remove --purge # Delete everything
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
SERVICE_NAME="traefik"
|
||||
BASE_DIR="$TRAEFIK_DIR"
|
||||
UNIT_NAME="traefik-docker"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Removing Traefik${NC}"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
echo -e " Mode: ${RED}Purge (delete certs + configs)${NC}"
|
||||
else
|
||||
echo -e " Mode: Safe (keep acme.json + dynamic/)"
|
||||
fi
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Systemd: ${UNIT_NAME}.service"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
warn "Removing Traefik will break ALL services that depend on it."
|
||||
if [[ "$ARG_YES" != "1" ]]; then
|
||||
read -rp "Continue? [y/N] " confirm
|
||||
if [[ "${confirm,,}" != "y" ]]; then
|
||||
info "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Stop traefik
|
||||
if [[ -d "$BASE_DIR" ]] && [[ -f "${BASE_DIR}/docker-compose.yml" ]]; then
|
||||
(cd "$BASE_DIR" && docker compose down) || true
|
||||
ok "Traefik stopped."
|
||||
fi
|
||||
|
||||
remove_systemd_unit "$UNIT_NAME"
|
||||
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
warn "This will delete ALL Traefik data including certificates and dynamic configs."
|
||||
if [[ "$ARG_YES" != "1" ]]; then
|
||||
read -rp "Permanently delete ${BASE_DIR}? [y/N] " confirm
|
||||
if [[ "${confirm,,}" != "y" ]]; then
|
||||
info "Purge cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
rm -rf "$BASE_DIR"
|
||||
ok "Purged: $BASE_DIR"
|
||||
else
|
||||
rm -f "${BASE_DIR}/docker-compose.yml"
|
||||
ok "Removed compose file. Preserved acme.json and dynamic/ configs."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Traefik removed${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" != "1" ]]; then
|
||||
echo -e " Certs kept: ${BASE_DIR}/acme.json"
|
||||
echo -e " Configs: ${TRAEFIK_DYNAMIC_DIR}/"
|
||||
fi
|
||||
echo -e " To redeploy: $0"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not deployed"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Traefik${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Data: ${TRAEFIK_DIR}"
|
||||
echo -e " Dynamic: ${TRAEFIK_DYNAMIC_DIR}/"
|
||||
echo -e " ACME Email: ${ACME_EMAIL}"
|
||||
echo -e " Status: ${traefik_status}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
echo -e "${GREEN} Traefik is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Logs: docker logs -f traefik"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-traefik.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Deploy ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Traefik deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Dynamic: ${TRAEFIK_DYNAMIC_DIR}/"
|
||||
echo -e " Certs: ${TRAEFIK_DIR}/acme.json"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f traefik"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
190
deploy-docker-web.sh
Executable file
190
deploy-docker-web.sh
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Static Web (nginx) Deployment Script — Multi-Instance
|
||||
# ==============================================================================
|
||||
# Deploys an nginx container serving static files behind Traefik.
|
||||
# Each instance is identified by --dir. Multiple instances can coexist.
|
||||
#
|
||||
# Usage: deploy-docker-web.sh --domain web1.an2.io --dir acme
|
||||
# deploy-docker-web.sh --domain web2.an2.io --dir acme-dev
|
||||
# deploy-docker-web.sh --remove --dir acme [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Validate --dir ---
|
||||
[[ -n "$ARG_DIR" ]] || fatal "--dir is required. Example: $0 --domain web.an2.io --dir mysite"
|
||||
|
||||
if ! [[ "$ARG_DIR" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
|
||||
fatal "--dir value must be alphanumeric (hyphens/underscores ok, no spaces/slashes): ${ARG_DIR}"
|
||||
fi
|
||||
|
||||
# --- Derive names from --dir ---
|
||||
DIR_NAME="$ARG_DIR"
|
||||
SERVICE_NAME="web-${DIR_NAME}"
|
||||
BASE_DIR="/opt/${DIR_NAME}"
|
||||
CONFIG_NAME="web-${DIR_NAME}"
|
||||
UNIT_NAME="web-${DIR_NAME}-docker"
|
||||
CONTAINER_NAME="web-${DIR_NAME}"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
do_remove "$SERVICE_NAME" "$CONFIG_NAME" "$UNIT_NAME" "$BASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Validate --domain (required for deploy) ---
|
||||
if [[ -z "$ARG_DOMAIN" ]] && ! [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
fatal "--domain is required for deploy. Example: $0 --domain web.an2.io --dir ${DIR_NAME}"
|
||||
fi
|
||||
|
||||
# --- Domain resolution ---
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
fatal "--domain is required for deploy."
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Web: ${DIR_NAME} (Docker/nginx)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}/html/"
|
||||
echo -e " Container: ${CONTAINER_NAME}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/docker-compose.yml" ]] && \
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo -e "${GREEN} ${CONTAINER_NAME} is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Document root: ${BASE_DIR}/html/"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-web.sh --dir ${DIR_NAME}"
|
||||
echo -e " Remove: $0 --remove --dir ${DIR_NAME} [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$DIR_NAME"; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
else
|
||||
# --- Fresh install ---
|
||||
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
fi
|
||||
|
||||
mkdir -p "${BASE_DIR}/html"
|
||||
|
||||
# Default placeholder page
|
||||
if [[ ! -f "${BASE_DIR}/html/index.html" ]]; then
|
||||
cat > "${BASE_DIR}/html/index.html" <<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>${DOMAIN}</title></head>
|
||||
<body><h1>${DOMAIN}</h1><p>Replace this file at ${BASE_DIR}/html/index.html</p></body>
|
||||
</html>
|
||||
HTML
|
||||
fi
|
||||
|
||||
# --- Generate .env ---
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-docker-web.sh on $(date -Iseconds)
|
||||
DOMAIN=${DOMAIN}
|
||||
DIR_NAME=${DIR_NAME}
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
ok ".env written."
|
||||
|
||||
# --- Generate docker-compose.yml ---
|
||||
cat > "${BASE_DIR}/docker-compose.yml" <<COMPOSE
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
container_name: ${CONTAINER_NAME}
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 512M
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /var/cache/nginx
|
||||
- /var/run
|
||||
- /tmp
|
||||
volumes:
|
||||
- ./html:/usr/share/nginx/html:ro
|
||||
networks:
|
||||
- traefik-public
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
COMPOSE
|
||||
ok "docker-compose.yml written."
|
||||
fi
|
||||
|
||||
# --- Always do these (fresh or restored) ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://${CONTAINER_NAME}:80"
|
||||
install_systemd_unit "$UNIT_NAME" "$BASE_DIR"
|
||||
(cd "$BASE_DIR" && docker compose up -d)
|
||||
wait_for_healthy "$CONTAINER_NAME" "http://localhost:80/" 60
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Web (${DIR_NAME}) deployed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " Doc root: ${BASE_DIR}/html/"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: docker logs -f ${CONTAINER_NAME}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
243
deploy-local-hermes.sh
Executable file
243
deploy-local-hermes.sh
Executable file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Hermes Agent Local (Bare-Metal) Deployment Script
|
||||
# ==============================================================================
|
||||
# Installs Hermes Agent directly on the host via uv + Python 3.11.
|
||||
# Runs as a gateway connecting to Telegram/Discord/Slack/WhatsApp.
|
||||
# No web UI — does NOT use Traefik.
|
||||
#
|
||||
# Source: https://github.com/NousResearch/hermes-agent
|
||||
#
|
||||
# Usage: deploy-local-hermes.sh
|
||||
# deploy-local-hermes.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="hermes"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}"
|
||||
HERMES_REPO="${HERMES_REPO:-https://github.com/NousResearch/hermes-agent.git}"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Removing ${SERVICE_NAME} (local)${NC}"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
echo -e " Mode: ${RED}Purge (delete all data)${NC}"
|
||||
else
|
||||
echo -e " Mode: Safe (keep data)"
|
||||
fi
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Systemd: ${UNIT_NAME}.service"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ "$ARG_PURGE" == "1" ]] && [[ -d "$BASE_DIR" ]]; then
|
||||
backup_script="${SCRIPT_DIR}/backup-${SERVICE_NAME}.sh"
|
||||
[[ -f "$backup_script" ]] && warn "Back up first? ${backup_script}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
unit_file="/etc/systemd/system/${UNIT_NAME}.service"
|
||||
if [[ -f "$unit_file" ]]; then
|
||||
systemctl stop "${UNIT_NAME}.service" > /dev/null 2>&1 || true
|
||||
systemctl disable "${UNIT_NAME}.service" > /dev/null 2>&1 || true
|
||||
rm -f "$unit_file"
|
||||
systemctl daemon-reload
|
||||
ok "Removed systemd unit: ${UNIT_NAME}.service"
|
||||
fi
|
||||
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
echo ""
|
||||
warn "This will permanently delete: $BASE_DIR"
|
||||
if [[ "$ARG_YES" != "1" ]]; then
|
||||
read -rp "Are you sure? [y/N] " confirm
|
||||
if [[ "${confirm,,}" != "y" ]]; then
|
||||
info "Purge cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
rm -rf "$BASE_DIR"
|
||||
rm -rf "$HOME/.hermes" 2>/dev/null
|
||||
ok "Purged: $BASE_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} ${SERVICE_NAME} removed${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" != "1" ]]; then
|
||||
echo -e " Data preserved at ${BASE_DIR}"
|
||||
echo -e " To purge: $0 --remove --purge"
|
||||
fi
|
||||
echo -e " To redeploy: $0"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
service_status="Not running"
|
||||
if systemctl is-active --quiet "${UNIT_NAME}.service" 2>/dev/null; then
|
||||
service_status="Running"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Hermes Agent (Local/Gateway)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Traefik: Not used (no web UI)"
|
||||
echo -e " Service: ${service_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/.env" ]] && \
|
||||
systemctl is-active --quiet "${UNIT_NAME}.service" 2>/dev/null; then
|
||||
echo -e "${GREEN} Hermes is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Health: hermes doctor"
|
||||
echo -e " Logs: journalctl -u ${UNIT_NAME}.service -f"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-hermes.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
|
||||
# --- Install prerequisites ---
|
||||
info "Installing prerequisites..."
|
||||
case "$DISTRO_FAMILY" in
|
||||
debian)
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl git openssl ca-certificates \
|
||||
python3 python3-venv python3-pip > /dev/null 2>&1
|
||||
;;
|
||||
fedora)
|
||||
dnf install -y -q curl git openssl ca-certificates \
|
||||
python3 python3-pip > /dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
ok "System prerequisites installed."
|
||||
|
||||
# Install uv (Python package manager)
|
||||
if ! command -v uv &>/dev/null; then
|
||||
info "Installing uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
ok "uv $(uv --version 2>/dev/null || echo 'installed')."
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
ok "Backup restored."
|
||||
else
|
||||
# --- Fresh install ---
|
||||
if [[ -d "${BASE_DIR}/.git" ]]; then
|
||||
info "Updating Hermes source..."
|
||||
(cd "$BASE_DIR" && git pull)
|
||||
else
|
||||
info "Cloning Hermes from ${HERMES_REPO}..."
|
||||
git clone --recurse-submodules "$HERMES_REPO" "$BASE_DIR"
|
||||
fi
|
||||
|
||||
info "Creating Python venv and installing dependencies..."
|
||||
(cd "$BASE_DIR" && uv venv venv --python 3.11 2>/dev/null || uv venv venv)
|
||||
export VIRTUAL_ENV="${BASE_DIR}/venv"
|
||||
(cd "$BASE_DIR" && uv pip install -e ".[all]" 2>/dev/null) || \
|
||||
(cd "$BASE_DIR" && "${BASE_DIR}/venv/bin/pip" install -e ".[all]")
|
||||
ok "Hermes installed."
|
||||
|
||||
# Symlink binary
|
||||
mkdir -p /usr/local/bin
|
||||
ln -sf "${BASE_DIR}/venv/bin/hermes" /usr/local/bin/hermes
|
||||
|
||||
# Data directory — redirect ~/.hermes here
|
||||
mkdir -p "${BASE_DIR}/data"
|
||||
rm -rf "$HOME/.hermes" 2>/dev/null
|
||||
ln -sfn "${BASE_DIR}/data" "$HOME/.hermes"
|
||||
|
||||
# Create data subdirs if fresh
|
||||
if [[ ! -d "${BASE_DIR}/data/sessions" ]]; then
|
||||
mkdir -p "${BASE_DIR}/data"/{cron,sessions,logs,memories,skills,pairing,hooks,image_cache,audio_cache}
|
||||
fi
|
||||
|
||||
if [[ ! -f "${BASE_DIR}/.env" ]]; then
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-local-hermes.sh on $(date -Iseconds)
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Install systemd unit ---
|
||||
cat > "/etc/systemd/system/${UNIT_NAME}.service" <<EOF
|
||||
[Unit]
|
||||
Description=Hermes Agent Gateway
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${BASE_DIR}
|
||||
Environment=VIRTUAL_ENV=${BASE_DIR}/venv
|
||||
Environment=PATH=${BASE_DIR}/venv/bin:/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=${BASE_DIR}/venv/bin/hermes gateway run
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStartSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${UNIT_NAME}.service" > /dev/null 2>&1
|
||||
systemctl restart "${UNIT_NAME}.service"
|
||||
ok "Systemd unit installed and started: ${UNIT_NAME}.service"
|
||||
|
||||
# --- Health check ---
|
||||
sleep 3
|
||||
if hermes doctor > /dev/null 2>&1; then
|
||||
ok "Hermes is healthy."
|
||||
else
|
||||
warn "hermes doctor failed — may need initial setup (API keys)."
|
||||
fi
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Hermes Agent deployed successfully (local)${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: journalctl -u ${UNIT_NAME}.service -f"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}First run? Set up API keys:${NC}"
|
||||
echo -e " hermes setup"
|
||||
echo -e " Then: systemctl restart ${UNIT_NAME}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
446
deploy-local-paperclip.sh
Executable file
446
deploy-local-paperclip.sh
Executable file
@@ -0,0 +1,446 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Paperclip Local (Bare-Metal) Deployment Script
|
||||
# ==============================================================================
|
||||
# Installs Paperclip directly on the host with Node.js + pnpm.
|
||||
# Uses embedded PostgreSQL — no external database needed.
|
||||
# Does NOT auto-bootstrap Traefik (prints instructions if needed).
|
||||
#
|
||||
# Source: https://github.com/paperclipai/paperclip
|
||||
#
|
||||
# Usage: deploy-local-paperclip.sh
|
||||
# deploy-local-paperclip.sh --domain clip.us.an2.io
|
||||
# deploy-local-paperclip.sh --remove [--purge] [--yes]
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
# --- Parse arguments ---
|
||||
parse_args "$@"
|
||||
|
||||
# --- Service config ---
|
||||
SERVICE_NAME="paperclip"
|
||||
BASE_DIR="/opt/${SERVICE_NAME}"
|
||||
CONFIG_NAME="${SERVICE_NAME}"
|
||||
UNIT_NAME="${SERVICE_NAME}"
|
||||
|
||||
# --- Handle --remove ---
|
||||
if [[ "$ARG_REMOVE" == "1" ]]; then
|
||||
require_root
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Removing ${SERVICE_NAME} (local)${NC}"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
echo -e " Mode: ${RED}Purge (delete all data)${NC}"
|
||||
else
|
||||
echo -e " Mode: Safe (keep data)"
|
||||
fi
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: ${UNIT_NAME}.service"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ "$ARG_PURGE" == "1" ]] && [[ -d "$BASE_DIR" ]]; then
|
||||
backup_script="${SCRIPT_DIR}/backup-${SERVICE_NAME}.sh"
|
||||
if [[ -f "$backup_script" ]]; then
|
||||
warn "Back up first? ${backup_script}"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Stop and remove systemd unit
|
||||
unit_file="/etc/systemd/system/${UNIT_NAME}.service"
|
||||
if [[ -f "$unit_file" ]]; then
|
||||
systemctl stop "${UNIT_NAME}.service" > /dev/null 2>&1 || true
|
||||
systemctl disable "${UNIT_NAME}.service" > /dev/null 2>&1 || true
|
||||
rm -f "$unit_file"
|
||||
systemctl daemon-reload
|
||||
ok "Removed systemd unit: ${UNIT_NAME}.service"
|
||||
fi
|
||||
|
||||
# Remove traefik config if it exists (user may have set it up manually)
|
||||
remove_traefik_dynamic_config "$SERVICE_NAME"
|
||||
|
||||
if [[ "$ARG_PURGE" == "1" ]]; then
|
||||
echo ""
|
||||
warn "This will permanently delete: $BASE_DIR"
|
||||
if [[ "$ARG_YES" != "1" ]]; then
|
||||
read -rp "Are you sure? [y/N] " confirm
|
||||
if [[ "${confirm,,}" != "y" ]]; then
|
||||
info "Purge cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
rm -rf "$BASE_DIR"
|
||||
ok "Purged: $BASE_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} ${SERVICE_NAME} removed${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
if [[ "$ARG_PURGE" != "1" ]]; then
|
||||
echo -e " Data preserved at ${BASE_DIR}"
|
||||
echo -e " To purge: $0 --remove --purge"
|
||||
fi
|
||||
echo -e " To redeploy: $0"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Domain resolution ---
|
||||
if [[ -n "$ARG_DOMAIN" ]]; then
|
||||
DOMAIN="$ARG_DOMAIN"
|
||||
elif [[ -f "${BASE_DIR}/.env" ]] && grep -q '^DOMAIN=' "${BASE_DIR}/.env"; then
|
||||
DOMAIN="$(grep '^DOMAIN=' "${BASE_DIR}/.env" | cut -d= -f2)"
|
||||
else
|
||||
DOMAIN="paperclip.an2.io"
|
||||
fi
|
||||
|
||||
# --- Detect current state for banner ---
|
||||
traefik_status="Not found — will deploy"
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^traefik$'; then
|
||||
traefik_status="Running"
|
||||
elif [[ -f "${TRAEFIK_DIR}/docker-compose.yml" ]]; then
|
||||
traefik_status="Stopped — will start"
|
||||
fi
|
||||
|
||||
install_mode="Fresh install"
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
install_mode="Re-run (preserving data)"
|
||||
fi
|
||||
|
||||
service_status="Not running"
|
||||
if systemctl is-active --quiet "${UNIT_NAME}.service" 2>/dev/null; then
|
||||
service_status="Running"
|
||||
fi
|
||||
|
||||
# --- Print deployment plan ---
|
||||
echo ""
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Deploying Paperclip (Local)${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo -e " Domain: ${DOMAIN}"
|
||||
echo -e " Data: ${BASE_DIR}"
|
||||
echo -e " Service: ${service_status}"
|
||||
echo -e " Traefik: ${traefik_status}"
|
||||
echo -e " Mode: ${install_mode}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# --- Check if already deployed and running ---
|
||||
if [[ -f "${BASE_DIR}/.env" ]] && \
|
||||
systemctl is-active --quiet "${UNIT_NAME}.service" 2>/dev/null; then
|
||||
echo -e "${GREEN} Paperclip is already running — nothing to do.${NC}"
|
||||
echo ""
|
||||
echo -e " Health: curl http://localhost:3100/api/health"
|
||||
echo -e " Logs: journalctl -u ${UNIT_NAME}.service -f"
|
||||
echo -e " Backup: ${SCRIPT_DIR}/backup-paperclip.sh"
|
||||
echo -e " Remove: $0 --remove [--purge]"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Shared infrastructure ---
|
||||
require_root
|
||||
detect_os
|
||||
install_prerequisites
|
||||
ensure_docker_network
|
||||
ensure_traefik
|
||||
configure_firewall
|
||||
|
||||
# --- Install Node.js + pnpm ---
|
||||
install_nodejs() {
|
||||
if command -v node &>/dev/null; then
|
||||
local node_major
|
||||
node_major="$(node --version | sed 's/^v//' | cut -d. -f1)"
|
||||
if (( node_major >= 20 )); then
|
||||
ok "Node.js $(node --version) already installed."
|
||||
return 0
|
||||
fi
|
||||
warn "Node.js $(node --version) is too old (need 20+). Upgrading..."
|
||||
fi
|
||||
|
||||
info "Installing Node.js 20..."
|
||||
case "$DISTRO_FAMILY" in
|
||||
debian)
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1
|
||||
apt-get install -y -qq nodejs > /dev/null 2>&1
|
||||
;;
|
||||
fedora)
|
||||
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash - > /dev/null 2>&1
|
||||
dnf install -y -q nodejs > /dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
ok "Node.js $(node --version) installed."
|
||||
}
|
||||
|
||||
install_pnpm() {
|
||||
if command -v pnpm &>/dev/null; then
|
||||
local pnpm_major
|
||||
pnpm_major="$(pnpm --version | cut -d. -f1)"
|
||||
if (( pnpm_major >= 9 )); then
|
||||
ok "pnpm $(pnpm --version) already installed."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Installing pnpm 9+..."
|
||||
corepack enable 2>/dev/null || npm install -g corepack
|
||||
corepack prepare pnpm@latest --activate 2>/dev/null || npm install -g pnpm@latest
|
||||
ok "pnpm $(pnpm --version) installed."
|
||||
}
|
||||
|
||||
# Install base prerequisites (curl, openssl, etc.)
|
||||
case "$DISTRO_FAMILY" in
|
||||
debian)
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl openssl jq git ca-certificates > /dev/null 2>&1
|
||||
;;
|
||||
fedora)
|
||||
dnf install -y -q curl openssl jq git ca-certificates > /dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
|
||||
install_nodejs
|
||||
install_pnpm
|
||||
|
||||
# --- Ensure system-wide node exists ---
|
||||
# nvm installs node under /root/ which other users cannot access.
|
||||
# The paperclip service user needs a system-wide node binary.
|
||||
SYS_NODE=""
|
||||
for _p in /usr/bin/node /usr/local/bin/node; do
|
||||
[[ -x "$_p" ]] && SYS_NODE="$_p" && break
|
||||
done
|
||||
if [[ -z "$SYS_NODE" ]]; then
|
||||
warn "Node.js only available via nvm (under /root/) — installing system-wide..."
|
||||
case "$DISTRO_FAMILY" in
|
||||
debian)
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - > /dev/null 2>&1
|
||||
apt-get install -y -qq nodejs > /dev/null 2>&1
|
||||
;;
|
||||
fedora)
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - > /dev/null 2>&1
|
||||
dnf install -y -q nodejs > /dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
for _p in /usr/bin/node /usr/local/bin/node; do
|
||||
[[ -x "$_p" ]] && SYS_NODE="$_p" && break
|
||||
done
|
||||
[[ -x "$SYS_NODE" ]] || fatal "Failed to install system-wide Node.js"
|
||||
ok "System-wide Node.js installed: ${SYS_NODE}"
|
||||
fi
|
||||
|
||||
# --- Create service user (embedded Postgres refuses to run as root) ---
|
||||
if ! id -u paperclip &>/dev/null; then
|
||||
useradd -r -s /usr/sbin/nologin -d "$BASE_DIR" paperclip
|
||||
ok "Created system user: paperclip"
|
||||
fi
|
||||
|
||||
# --- Backup restore check ---
|
||||
if check_and_restore_backup "$SERVICE_NAME"; then
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
ok "Backup restored. Skipping fresh install."
|
||||
else
|
||||
# --- Fresh install ---
|
||||
if [[ -f "${BASE_DIR}/.env" ]]; then
|
||||
# Re-run: update dependencies only
|
||||
saved_domain="$DOMAIN"
|
||||
# shellcheck source=/dev/null
|
||||
source "${BASE_DIR}/.env"
|
||||
DOMAIN="$saved_domain"
|
||||
info "Existing installation found. Updating dependencies..."
|
||||
(cd "$BASE_DIR" && pnpm install --frozen-lockfile 2>/dev/null || pnpm install)
|
||||
info "Rebuilding project..."
|
||||
(cd "$BASE_DIR" && pnpm build)
|
||||
else
|
||||
# Fresh clone
|
||||
PAPERCLIP_REPO="${PAPERCLIP_REPO:-https://github.com/paperclipai/paperclip.git}"
|
||||
|
||||
info "Cloning Paperclip from ${PAPERCLIP_REPO}..."
|
||||
if [[ -d "${BASE_DIR}/.git" ]]; then
|
||||
(cd "$BASE_DIR" && git pull)
|
||||
else
|
||||
git clone "$PAPERCLIP_REPO" "$BASE_DIR"
|
||||
fi
|
||||
|
||||
info "Installing dependencies..."
|
||||
(cd "$BASE_DIR" && pnpm install)
|
||||
|
||||
info "Building project (this may take a minute)..."
|
||||
(cd "$BASE_DIR" && pnpm build)
|
||||
|
||||
mkdir -p "${BASE_DIR}/data"
|
||||
|
||||
# --- Generate .env ---
|
||||
# authenticated mode + HOST=0.0.0.0 so Traefik (in Docker) can reach the service
|
||||
BETTER_AUTH_SECRET="$(generate_password)"
|
||||
cat > "${BASE_DIR}/.env" <<DOTENV
|
||||
# Generated by deploy-local-paperclip.sh on $(date -Iseconds)
|
||||
DOMAIN=${DOMAIN}
|
||||
PAPERCLIP_HOME=${BASE_DIR}/data
|
||||
HOST=0.0.0.0
|
||||
PAPERCLIP_DEPLOYMENT_MODE=authenticated
|
||||
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
||||
DOTENV
|
||||
chmod 600 "${BASE_DIR}/.env"
|
||||
ok ".env written."
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Set ownership (must happen before onboarding and service start) ---
|
||||
chown -R paperclip:paperclip "$BASE_DIR"
|
||||
ok "Ownership set to paperclip:paperclip"
|
||||
|
||||
# --- Run onboarding as paperclip user (creates config for non-interactive use) ---
|
||||
# Use system node directly — pnpm may be under /root/.nvm/ and inaccessible
|
||||
# "pnpm paperclipai X" is equivalent to: node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts X
|
||||
PAPERCLIP_CMD="${SYS_NODE} cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts"
|
||||
|
||||
if [[ ! -f "${BASE_DIR}/data/instances/default/config.json" ]]; then
|
||||
info "Running initial onboarding..."
|
||||
# onboard --yes creates config then starts the server (blocking).
|
||||
# We only need the config — systemd manages the service.
|
||||
# Run in background, wait for config, then kill it.
|
||||
sudo -u paperclip bash -c "cd '${BASE_DIR}' && PAPERCLIP_HOME='${BASE_DIR}/data' ${PAPERCLIP_CMD} onboard --yes" &
|
||||
_onboard_pid=$!
|
||||
_waited=0
|
||||
while (( _waited < 120 )); do
|
||||
if [[ -f "${BASE_DIR}/data/instances/default/config.json" ]]; then
|
||||
ok "Onboarding complete — config created."
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
(( _waited += 2 ))
|
||||
done
|
||||
# Kill onboard + any server/postgres it spawned
|
||||
kill "$_onboard_pid" 2>/dev/null; wait "$_onboard_pid" 2>/dev/null || true
|
||||
pkill -u paperclip -f "cli/src/index" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Switch config to authenticated mode (onboard defaults to local_trusted)
|
||||
# Key is nested: server.deploymentMode (not top-level)
|
||||
PAPERCLIP_CONFIG="${BASE_DIR}/data/instances/default/config.json"
|
||||
if [[ -f "$PAPERCLIP_CONFIG" ]] && command -v jq &>/dev/null; then
|
||||
jq '.server.deploymentMode = "authenticated"' "$PAPERCLIP_CONFIG" > "${PAPERCLIP_CONFIG}.tmp" && \
|
||||
mv "${PAPERCLIP_CONFIG}.tmp" "$PAPERCLIP_CONFIG"
|
||||
chown paperclip:paperclip "$PAPERCLIP_CONFIG"
|
||||
ok "Config set to authenticated mode."
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Register domain as allowed hostname ---
|
||||
info "Registering allowed hostname: ${DOMAIN}"
|
||||
sudo -u paperclip bash -c "cd '${BASE_DIR}' && PAPERCLIP_HOME='${BASE_DIR}/data' ${PAPERCLIP_CMD} allowed-hostname '${DOMAIN}'" || \
|
||||
warn "Could not register hostname — you may need to run: pnpm paperclipai allowed-hostname ${DOMAIN}"
|
||||
|
||||
# --- Create paperclipai symlink ---
|
||||
cat > /usr/local/bin/paperclipai <<WRAPPER
|
||||
#!/usr/bin/env bash
|
||||
cd "${BASE_DIR}" && PAPERCLIP_HOME="${BASE_DIR}/data" exec ${PAPERCLIP_CMD} "\$@"
|
||||
WRAPPER
|
||||
chmod +x /usr/local/bin/paperclipai
|
||||
ok "Command available: paperclipai"
|
||||
|
||||
# --- Bootstrap admin (authenticated mode requires it) ---
|
||||
info "Bootstrapping admin invite..."
|
||||
INVITE_OUTPUT="$(sudo -u paperclip bash -c "cd '${BASE_DIR}' && PAPERCLIP_HOME='${BASE_DIR}/data' ${PAPERCLIP_CMD} auth bootstrap-ceo --data-dir '${BASE_DIR}/data' --base-url 'https://${DOMAIN}'" 2>&1)" || true
|
||||
|
||||
# --- Install systemd unit (process, not Docker) ---
|
||||
cat > "/etc/systemd/system/${UNIT_NAME}.service" <<EOF
|
||||
[Unit]
|
||||
Description=Paperclip Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=paperclip
|
||||
Group=paperclip
|
||||
WorkingDirectory=${BASE_DIR}
|
||||
EnvironmentFile=${BASE_DIR}/.env
|
||||
ExecStart=${PAPERCLIP_CMD} run
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${UNIT_NAME}.service" > /dev/null 2>&1
|
||||
systemctl restart "${UNIT_NAME}.service"
|
||||
ok "Systemd unit installed and started: ${UNIT_NAME}.service"
|
||||
|
||||
# --- Ensure Traefik can reach host services ---
|
||||
# Traefik runs in Docker; it needs host.docker.internal to reach bare-metal services.
|
||||
# New deployments get this via ensure_traefik. Patch existing ones if missing.
|
||||
if ! grep -q "host.docker.internal" "${TRAEFIK_DIR}/docker-compose.yml" 2>/dev/null; then
|
||||
info "Updating Traefik with host.docker.internal mapping..."
|
||||
cat > "${TRAEFIK_DIR}/docker-compose.override.yml" <<'OVERRIDE'
|
||||
services:
|
||||
traefik:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
OVERRIDE
|
||||
(cd "$TRAEFIK_DIR" && docker compose up -d)
|
||||
ok "Traefik updated."
|
||||
fi
|
||||
|
||||
# --- Write Traefik dynamic config ---
|
||||
write_traefik_dynamic_config "$CONFIG_NAME" "$DOMAIN" "http://host.docker.internal:3100"
|
||||
|
||||
# --- Wait for health ---
|
||||
info "Waiting for Paperclip to become healthy (up to 90s)..."
|
||||
healthy=0
|
||||
elapsed=0
|
||||
while (( elapsed < 90 )); do
|
||||
if curl -sf http://localhost:3100/api/health > /dev/null 2>&1; then
|
||||
ok "Paperclip is healthy."
|
||||
healthy=1
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
(( elapsed += 5 ))
|
||||
done
|
||||
|
||||
# --- Summary ---
|
||||
echo ""
|
||||
if (( healthy )); then
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Paperclip deployed successfully (local)${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Paperclip installed but not yet healthy${NC}"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
fi
|
||||
echo -e " URL: https://${DOMAIN}"
|
||||
echo -e " Local: http://localhost:3100"
|
||||
echo -e " Data: ${BASE_DIR}/data/"
|
||||
echo -e " Traefik: ${TRAEFIK_DYNAMIC_DIR}/${CONFIG_NAME}.yml"
|
||||
echo -e " Systemd: systemctl status ${UNIT_NAME}"
|
||||
echo -e " Logs: journalctl -u ${UNIT_NAME}.service -f"
|
||||
# Show invite URL if bootstrap-ceo produced one
|
||||
INVITE_URL="$(echo "$INVITE_OUTPUT" | grep -oE 'https?://[^ ]+' | head -1)" || true
|
||||
if [[ -n "$INVITE_URL" ]]; then
|
||||
echo ""
|
||||
echo -e " ${CYAN}Admin invite:${NC} ${INVITE_URL}"
|
||||
fi
|
||||
if (( healthy )); then
|
||||
echo -e "${GREEN}══════════════════════════════════════════════${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}══════════════════════════════════════════════${NC}"
|
||||
fi
|
||||
echo ""
|
||||
@@ -1,42 +0,0 @@
|
||||
# OpenClaw Gitea bootstrap
|
||||
=======================
|
||||
|
||||
Created user
|
||||
- username: openclaw
|
||||
- fullname: OpenClaw
|
||||
- email: openclaw@git.an2.io
|
||||
|
||||
SSH key
|
||||
- private key: /home/user/.ssh/id_gitea_openclaw
|
||||
- public key: /home/user/.ssh/id_gitea_openclaw.pub
|
||||
- Gitea key title: openclaw@xeon
|
||||
|
||||
SSH config
|
||||
- host alias: gitea
|
||||
- hostname: git.an2.io
|
||||
- port: 222
|
||||
- user: git
|
||||
- identity: /home/user/.ssh/id_gitea_openclaw
|
||||
|
||||
Verified
|
||||
- ssh -T gitea
|
||||
- result: authenticated successfully as openclaw
|
||||
|
||||
Access token
|
||||
- created for user: openclaw
|
||||
- token name: openclaw-bootstrap
|
||||
- scopes: all
|
||||
- token value was generated during bootstrap and should be stored securely if you want API repo management later
|
||||
|
||||
Recommended usage
|
||||
- clone/pull/push via SSH
|
||||
- repo create/delete via Gitea API token
|
||||
|
||||
Example remote forms
|
||||
- ssh://git@git.an2.io:222/openclaw/repo.git
|
||||
- gitea:openclaw/repo.git (via SSH config alias if your git tooling uses it explicitly)
|
||||
|
||||
Suggested next steps
|
||||
1. Store the generated Gitea token somewhere safe for future API use
|
||||
2. Create a first test repo under openclaw
|
||||
3. Verify clone, commit, and push with the SSH identity
|
||||
@@ -1,98 +0,0 @@
|
||||
# OpenClaw + Mattermost notes
|
||||
===========================
|
||||
|
||||
Current working state
|
||||
- OpenClaw gateway is reachable on port 18789
|
||||
- Mattermost channel connects as @claw
|
||||
- Native slash commands are working
|
||||
- Slash commands were registered successfully for 1 team
|
||||
- Inline buttons are enabled in channel config
|
||||
- Button callback handling is working
|
||||
|
||||
What was wrong
|
||||
1. A manual Mattermost plugin install was present even though Mattermost is bundled in current OpenClaw.
|
||||
2. The bot account was not a member of any Mattermost team.
|
||||
3. Because the bot had zero teams, OpenClaw had nowhere to register native slash commands.
|
||||
|
||||
Observed failure mode
|
||||
- Logs showed:
|
||||
- mattermost: registered slash command callback at /api/channels/mattermost/command
|
||||
- mattermost: connected as @claw
|
||||
- mattermost: native slash commands enabled but no commands could be registered; keeping slash callbacks inactive
|
||||
|
||||
Actual root cause
|
||||
- Mattermost API check showed the bot user belonged to zero teams.
|
||||
- Native slash command registration is team-scoped, so no team membership means no commands can be created.
|
||||
|
||||
What was cleaned up
|
||||
- Moved manual override plugin out of the active extensions path:
|
||||
- /home/user/.openclaw/extensions/mattermost
|
||||
- -> /home/user/.openclaw/extensions/mattermost.disabled-20260410-230312
|
||||
- Removed stale plugin install metadata from /home/user/.openclaw/openclaw.json:
|
||||
- plugins.installs.mattermost
|
||||
- Removed stale disabled bundled plugin config warning:
|
||||
- plugins.entries.huggingface
|
||||
|
||||
Mattermost config in use
|
||||
- baseUrl: https://mm.an2.io
|
||||
- commands.native: true
|
||||
- commands.nativeSkills: true
|
||||
- callbackPath: /api/channels/mattermost/command
|
||||
- callbackUrl: http://159.69.76.190:18789/api/channels/mattermost/command
|
||||
- interactions.allowedSourceIps: ["172.20.0.3", "172.20.0.0/16", "127.0.0.1", "::1"]
|
||||
|
||||
What fixed it
|
||||
- Added the @claw bot to a Mattermost team
|
||||
- After reconnect/restart, OpenClaw successfully registered slash commands
|
||||
|
||||
Successful log indicators
|
||||
- mattermost: connected as @claw
|
||||
- mattermost: registered command /oc_status
|
||||
- mattermost: registered command /oc_model
|
||||
- mattermost: registered command /oc_models
|
||||
- mattermost: slash commands activated for account default (20 commands)
|
||||
- mattermost: slash commands registered (20 commands across 1 teams, callback=http://159.69.76.190:18789/api/channels/mattermost/command)
|
||||
|
||||
Important reminder
|
||||
- The bot must belong to at least one team for native slash command registration to work.
|
||||
- The bot should also be added to the channels where you want it to interact.
|
||||
- If native slash commands fail again, first check team membership before debugging callback URLs.
|
||||
|
||||
Useful checks
|
||||
- curl -sS -H 'Authorization: Bearer <BOT_TOKEN>' https://mm.an2.io/api/v4/users/me
|
||||
- curl -sS -H 'Authorization: Bearer <BOT_TOKEN>' https://mm.an2.io/api/v4/users/<BOT_USER_ID>/teams
|
||||
- grep -Ei 'mattermost|slash|callback' /tmp/openclaw-gateway-run.log
|
||||
|
||||
If slash commands break again
|
||||
1. Confirm the bot is still in a team
|
||||
2. Confirm callbackUrl is reachable from the Mattermost server
|
||||
3. Check for old leftover manually installed overrides in ~/.openclaw/extensions/
|
||||
4. Restart gateway and re-read Mattermost startup logs
|
||||
|
||||
Button callback note
|
||||
- Native slash commands now work.
|
||||
- Button callback test initially failed with: mattermost interaction: rejected callback source remote=172.20.0.3
|
||||
- Updated channels.mattermost.interactions.allowedSourceIps to include:
|
||||
- 172.20.0.3
|
||||
- 172.20.0.0/16
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- After the allowlist update, button callbacks worked end-to-end.
|
||||
|
||||
Elevated exec from Mattermost
|
||||
- Added tools.elevated.enabled = true
|
||||
- Added tools.elevated.allowFrom.mattermost = ["63rzn4hbijnrmjbomxxugntg9h"]
|
||||
- This allows elevated exec only from the specified Mattermost source/account instead of opening it broadly.
|
||||
- Gateway restart is required for this change to take full effect.
|
||||
|
||||
Provider routing note for elevated exec
|
||||
- Elevated exec checks for this session path were evaluated under provider=webchat, not provider=mattermost.
|
||||
- Because of that, tools.elevated.allowFrom needed both:
|
||||
- mattermost: ["63rzn4hbijnrmjbomxxugntg9h"]
|
||||
- webchat: ["63rzn4hbijnrmjbomxxugntg9h"]
|
||||
- Gateway restart is required after changing these allowFrom rules.
|
||||
|
||||
Elevated exec verification
|
||||
- After adding both provider allowlists and restarting the gateway, elevated exec from the Mattermost session path worked.
|
||||
- Confirmed in-session test: sudo -n true -> SUDO_OK
|
||||
- This verified that elevated exec was allowed for the effective provider path used by that session.
|
||||
@@ -1,46 +0,0 @@
|
||||
# OpenClaw behind Traefik
|
||||
=======================
|
||||
|
||||
What I changed
|
||||
- Changed OpenClaw gateway bind in /home/user/.openclaw/openclaw.json from loopback to lan
|
||||
- Added Traefik dynamic config at /opt/traefik/dynamic/openclaw.yml
|
||||
- Target upstream is http://host.docker.internal:18789
|
||||
- Added gateway.controlUi.allowedOrigins entry for https://oc.an2.io
|
||||
|
||||
Why
|
||||
- Traefik runs in Docker.
|
||||
- The Traefik container already has host.docker.internal mapped to the Docker host gateway.
|
||||
- OpenClaw was only listening on 127.0.0.1:18789, which the container could not reach.
|
||||
- Binding OpenClaw to lan makes it listen on the host network so Traefik can reach it through the host gateway.
|
||||
- The Control UI loaded through Traefik uses browser origin https://oc.an2.io, so that exact origin must be listed in gateway.controlUi.allowedOrigins.
|
||||
|
||||
Traefik config summary
|
||||
- Router name: openclaw
|
||||
- Entry point: websecure
|
||||
- TLS cert resolver: letsencrypt
|
||||
- Host rule: oc.an2.io
|
||||
- Service upstream: http://host.docker.internal:18789
|
||||
- Dynamic file: /opt/traefik/dynamic/openclaw.yml
|
||||
|
||||
OpenClaw config summary
|
||||
- File: /home/user/.openclaw/openclaw.json
|
||||
- gateway.bind: lan
|
||||
- gateway.controlUi.allowedOrigins includes:
|
||||
- http://localhost:18789
|
||||
- http://127.0.0.1:18789
|
||||
- https://oc.an2.io
|
||||
|
||||
Notes
|
||||
- https://localhost:18789 is not the correct public URL because OpenClaw on port 18789 speaks plain HTTP.
|
||||
- TLS is terminated by Traefik on port 443 and then proxied to http://host.docker.internal:18789.
|
||||
- If the browser shows "origin not allowed", check gateway.controlUi.allowedOrigins first.
|
||||
|
||||
Useful checks
|
||||
- ss -ltnp | grep 18789
|
||||
- curl http://localhost:18789/
|
||||
- curl -k https://oc.an2.io/
|
||||
- sudo docker logs --tail 100 traefik
|
||||
|
||||
If needed
|
||||
- Restart OpenClaw gateway after config changes
|
||||
- Traefik dynamic config should hot-reload automatically, no Traefik restart is usually needed
|
||||
Reference in New Issue
Block a user