first commit
This commit is contained in:
239
start-docker-cn.sh
Executable file
239
start-docker-cn.sh
Executable file
@@ -0,0 +1,239 @@
|
||||
#!/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 " 首次启动可能要 1~3 分钟(下载嵌入模型等),白页请多等一会或看日志。"
|
||||
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"
|
||||
Reference in New Issue
Block a user