Files
hermes/start-docker-cn.sh
2026-04-13 14:56:51 +08:00

240 lines
8.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# One-click local Docker deployment using China-friendly mirrors.
# Run from workspace root: /Users/dannier/Desktop/living/hh
#
# 配置:编辑同目录下的 hermes-docker.env可由 hermes-docker.env.example 复制)
# Usage:
# bash start-docker-cn.sh
WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${WORKSPACE_DIR}/hermes-docker.env"
ENV_EXAMPLE="${WORKSPACE_DIR}/hermes-docker.env.example"
if [ ! -f "${ENV_FILE}" ] && [ -f "${ENV_EXAMPLE}" ]; then
cp "${ENV_EXAMPLE}" "${ENV_FILE}"
echo "==> Created ${ENV_FILE} from example — edit API keys if needed"
fi
if [ -f "${ENV_FILE}" ]; then
set -a
# shellcheck disable=SC1090
source "${ENV_FILE}"
set +a
fi
HERMES_PROJECT_DIR="${HERMES_PROJECT_DIR:-${WORKSPACE_DIR}/hermes-agent}"
HERMES_DATA_DIR="${HERMES_DATA_DIR:-$HOME/.hermes}"
IMAGE_NAME="${IMAGE_NAME:-hermes-agent:cn-local}"
CONTAINER_NAME="${CONTAINER_NAME:-hermes}"
COMPOSE_FILE="${WORKSPACE_DIR}/docker-compose.cn.yml"
API_PORT="${API_PORT:-8642}"
HERMES_API_KEY="${HERMES_API_KEY:-change-me-local-dev}"
WEBUI_PORT="${WEBUI_PORT:-3000}"
ENABLE_WEBUI="${ENABLE_WEBUI:-true}"
WEBUI_SECRET_KEY="${WEBUI_SECRET_KEY:-dev-change-me-in-production}"
OPEN_WEBUI_IMAGE="${OPEN_WEBUI_IMAGE:-ghcr.io/open-webui/open-webui:main}"
OPEN_WEBUI_DATA_DIR="${OPEN_WEBUI_DATA_DIR:-${WORKSPACE_DIR}/open-webui-data}"
# macOS 下启动完成后是否自动打开浏览器OPEN_BROWSER=false 可关闭)
OPEN_BROWSER="${OPEN_BROWSER:-true}"
# China mirrors (can be overridden by env vars or hermes-docker.env)
BASE_IMAGE="${BASE_IMAGE:-}"
BASE_IMAGE_CANDIDATES="${BASE_IMAGE_CANDIDATES:-docker.m.daocloud.io/library/debian:13.4}"
APT_MIRROR_CANDIDATES="${APT_MIRROR_CANDIDATES:-mirrors.aliyun.com,mirrors.ustc.edu.cn,mirrors.huaweicloud.com,mirrors.tuna.tsinghua.edu.cn}"
PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}"
NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmmirror.com}"
PLAYWRIGHT_DOWNLOAD_HOSTS="${PLAYWRIGHT_DOWNLOAD_HOSTS:-https://npmmirror.com/mirrors/playwright,https://registry.npmmirror.com/-/binary/playwright}"
INSTALL_PLAYWRIGHT_BROWSER="${INSTALL_PLAYWRIGHT_BROWSER:-0}"
PLAYWRIGHT_ONLY_SHELL="${PLAYWRIGHT_ONLY_SHELL:-0}"
# Hermes 在容器内会 load ~/.hermes/.env 且 override=True会覆盖 Docker env_file 注入的密钥。
# 将 hermes-docker.env 里已解析的 LLM 变量写入 ~/.hermes/.env避免「Docker 有 key、运行时仍 401」。
sync_llm_keys_to_hermes_dotenv() {
local h="${HERMES_DATA_DIR}/.env"
local keys=(
OPENAI_API_KEY OPENROUTER_API_KEY GOOGLE_API_KEY GEMINI_API_KEY
GLM_API_KEY KIMI_API_KEY ANTHROPIC_API_KEY MINIMAX_API_KEY HF_TOKEN
DASHSCOPE_API_KEY DASHSCOPE_BASE_URL
)
local key val any=0
touch "$h"
for key in "${keys[@]}"; do
val="${!key:-}"
[ -z "$val" ] && continue
any=1
if [ -s "$h" ]; then
grep -v "^${key}=" "$h" > "${h}.tmp.$$" && mv "${h}.tmp.$$" "$h"
fi
printf '%s=%s\n' "$key" "$val" >> "$h"
done
if [ "$any" -eq 1 ]; then
echo "==> Synced LLM keys from ${ENV_FILE} -> ${h} (required: Hermes overrides Docker env with this file)"
fi
}
echo "==> Checking project path: ${HERMES_PROJECT_DIR}"
if [ ! -f "${HERMES_PROJECT_DIR}/Dockerfile" ]; then
echo "ERROR: Dockerfile not found under ${HERMES_PROJECT_DIR}"
exit 1
fi
echo "==> Checking Docker availability"
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: docker not found. Please install Docker first."
exit 1
fi
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose)
else
echo "ERROR: docker compose not found (docker compose or docker-compose)."
exit 1
fi
echo "==> Preparing data directories"
mkdir -p "${HERMES_DATA_DIR}"
mkdir -p "${OPEN_WEBUI_DATA_DIR}"
sync_llm_keys_to_hermes_dotenv
echo "==> Building image with China mirrors: ${IMAGE_NAME}"
if [ -n "${BASE_IMAGE}" ]; then
BUILD_BASE_IMAGES=("${BASE_IMAGE}")
else
IFS=',' read -r -a BUILD_BASE_IMAGES <<< "${BASE_IMAGE_CANDIDATES}"
fi
BUILD_OK=0
for base in "${BUILD_BASE_IMAGES[@]}"; do
echo "==> Trying BASE_IMAGE: ${base}"
if docker build \
--build-arg BASE_IMAGE="${base}" \
--build-arg APT_MIRROR_CANDIDATES="${APT_MIRROR_CANDIDATES}" \
--build-arg PIP_INDEX_URL="${PIP_INDEX_URL}" \
--build-arg NPM_REGISTRY="${NPM_REGISTRY}" \
--build-arg PLAYWRIGHT_DOWNLOAD_HOSTS="${PLAYWRIGHT_DOWNLOAD_HOSTS}" \
--build-arg INSTALL_PLAYWRIGHT_BROWSER="${INSTALL_PLAYWRIGHT_BROWSER}" \
--build-arg PLAYWRIGHT_ONLY_SHELL="${PLAYWRIGHT_ONLY_SHELL}" \
-t "${IMAGE_NAME}" \
"${HERMES_PROJECT_DIR}"; then
BUILD_OK=1
break
fi
echo "WARN: Build failed with BASE_IMAGE=${base}, trying next candidate..."
done
if [ "${BUILD_OK}" -ne 1 ]; then
echo "ERROR: image build failed for all BASE_IMAGE candidates."
echo "You can manually specify one, e.g.:"
echo " BASE_IMAGE=docker.m.daocloud.io/library/debian:13.4 bash start-docker-cn.sh"
exit 1
fi
echo "==> Writing compose file: ${COMPOSE_FILE}"
CORS_ORIGINS="http://127.0.0.1:${API_PORT},http://localhost:${API_PORT},http://localhost:${WEBUI_PORT},http://127.0.0.1:${WEBUI_PORT}"
{
cat <<EOF
services:
hermes:
image: ${IMAGE_NAME}
container_name: ${CONTAINER_NAME}
restart: unless-stopped
command: gateway run
ports:
- "${API_PORT}:8642"
volumes:
- ${HERMES_DATA_DIR}:/opt/data
shm_size: "1gb"
env_file:
- ${ENV_FILE}
environment:
- TZ=Asia/Shanghai
- API_SERVER_ENABLED=true
- API_SERVER_HOST=0.0.0.0
- "API_SERVER_KEY=${HERMES_API_KEY}"
- "API_SERVER_CORS_ORIGINS=${CORS_ORIGINS}"
EOF
if [ "${ENABLE_WEBUI}" = "true" ]; then
cat <<EOF
open-webui:
image: ${OPEN_WEBUI_IMAGE}
container_name: hermes-open-webui
restart: unless-stopped
depends_on:
- hermes
ports:
- "${WEBUI_PORT}:8080"
environment:
- "OPENAI_API_BASE_URL=http://hermes:8642/v1"
- "OPENAI_API_KEY=${HERMES_API_KEY}"
- "WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}"
- ENABLE_SIGNUP=true
- ENABLE_OLLAMA_API=false
volumes:
- ${OPEN_WEBUI_DATA_DIR}:/app/backend/data
EOF
fi
echo ""
} > "${COMPOSE_FILE}"
echo "==> Starting containers"
if [ "${ENABLE_WEBUI}" = "true" ]; then
echo "==> Pulling Open WebUI image if needed: ${OPEN_WEBUI_IMAGE}"
docker pull "${OPEN_WEBUI_IMAGE}" || true
fi
"${COMPOSE_CMD[@]}" -f "${COMPOSE_FILE}" up -d --remove-orphans
if [ "${ENABLE_WEBUI}" = "true" ]; then
echo "==> Waiting for Web UI to listen on port ${WEBUI_PORT}..."
for _i in $(seq 1 90); do
if curl -sf "http://127.0.0.1:${WEBUI_PORT}/" >/dev/null 2>&1; then
echo "==> Web UI is responding."
break
fi
sleep 1
done
if [ "${OPEN_BROWSER}" = "true" ] && [ "$(uname -s)" = "Darwin" ]; then
open "http://127.0.0.1:${WEBUI_PORT}" 2>/dev/null || true
fi
fi
echo "==> Done"
echo "Config file : ${ENV_FILE}"
echo "Hermes data : ${HERMES_DATA_DIR}"
echo "Container : ${CONTAINER_NAME}"
echo
echo "Hermes API (OpenAI-compatible):"
echo " http://127.0.0.1:${API_PORT}/v1"
echo " API key: ${HERMES_API_KEY}"
echo
if [ "${ENABLE_WEBUI}" = "true" ]; then
echo "Web UI (Open WebUI) — 在浏览器地址栏手动打开(须 http不要用 https"
echo " http://127.0.0.1:${WEBUI_PORT}"
echo " 首次启动可能要 13 分钟(下载嵌入模型等),白页请多等一会或看日志。"
echo " 自检: curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:${WEBUI_PORT}/"
echo " 首次进入: 注册本地账号(数据在 ${OPEN_WEBUI_DATA_DIR}"
echo
fi
echo "Quick test:"
echo " curl -sS http://127.0.0.1:${API_PORT}/v1/models -H \"Authorization: Bearer ${HERMES_API_KEY}\""
echo
echo "Useful commands:"
echo " ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} logs -f hermes"
echo " ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} logs -f open-webui"
echo " ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} down"
echo
echo "LLM 密钥(必填,否则 Open WebUI 对话会报 No inference provider:"
echo " 编辑 ${ENV_FILE} 取消注释并填写 OPENROUTER_API_KEY=(或见同文件说明)"
echo " 然后: ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} up -d"
echo " 亦可写入 ${HERMES_DATA_DIR}/.env 后: ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} restart hermes"
echo "Optional: docker exec -it ${CONTAINER_NAME} hermes model"
echo " docker exec -it ${CONTAINER_NAME} hermes setup"