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