增加了全量部署脚本
parent
ae34bfc6a0
commit
9ad37d0aa6
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Public exposed port (only nginx is exposed)
|
||||||
|
NGINX_PORT=8080
|
||||||
|
|
||||||
|
# Required absolute host paths.
|
||||||
|
# They must exist or be creatable by the deployment user and writable by docker daemon.
|
||||||
|
HOST_DATA_ROOT=/opt/dashboard-nanobot/data
|
||||||
|
HOST_BOTS_WORKSPACE_ROOT=/opt/dashboard-nanobot/workspace/bots
|
||||||
|
|
||||||
|
# Optional custom image tags
|
||||||
|
BACKEND_IMAGE_TAG=latest
|
||||||
|
FRONTEND_IMAGE_TAG=latest
|
||||||
|
|
||||||
|
# Optional base images / mirrors
|
||||||
|
PYTHON_BASE_IMAGE=python:3.12-slim
|
||||||
|
NODE_BASE_IMAGE=node:22-alpine
|
||||||
|
NGINX_BASE_IMAGE=nginx:alpine
|
||||||
|
POSTGRES_IMAGE=postgres:16-alpine
|
||||||
|
REDIS_IMAGE=redis:7-alpine
|
||||||
|
|
||||||
|
# Python package index mirror (recommended in CN)
|
||||||
|
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
|
||||||
|
|
||||||
|
# Frontend package registry mirror (used by yarn, recommended in CN)
|
||||||
|
NPM_REGISTRY=https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# Container timezone
|
||||||
|
TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# PostgreSQL bootstrap account.
|
||||||
|
# These values are used by the postgres container itself.
|
||||||
|
POSTGRES_SUPERUSER=postgres
|
||||||
|
POSTGRES_SUPERPASSWORD=change_me_pg_super_password
|
||||||
|
POSTGRES_BOOTSTRAP_DB=postgres
|
||||||
|
|
||||||
|
# Dashboard application database account.
|
||||||
|
# deploy-full.sh will call scripts/init-full-db.sh to create/update these idempotently.
|
||||||
|
POSTGRES_APP_DB=dashboard
|
||||||
|
POSTGRES_APP_USER=dashboard
|
||||||
|
POSTGRES_APP_PASSWORD=change_me_dashboard_password
|
||||||
|
DATABASE_POOL_SIZE=20
|
||||||
|
DATABASE_MAX_OVERFLOW=40
|
||||||
|
DATABASE_POOL_TIMEOUT=30
|
||||||
|
DATABASE_POOL_RECYCLE=1800
|
||||||
|
|
||||||
|
# Redis cache (managed by docker-compose.full.yml)
|
||||||
|
REDIS_ENABLED=true
|
||||||
|
REDIS_DB=8
|
||||||
|
REDIS_PREFIX=nanobot
|
||||||
|
REDIS_DEFAULT_TTL=60
|
||||||
|
|
||||||
|
# Chat history page size for upward lazy loading (per request)
|
||||||
|
CHAT_PULL_PAGE_SIZE=60
|
||||||
|
COMMAND_AUTO_UNLOCK_SECONDS=10
|
||||||
|
DEFAULT_BOT_SYSTEM_TIMEZONE=Asia/Shanghai
|
||||||
|
|
||||||
|
# Panel access protection
|
||||||
|
PANEL_ACCESS_PASSWORD=change_me_panel_password
|
||||||
|
|
||||||
|
# Browser credential requests must use an explicit CORS allowlist.
|
||||||
|
# If frontend and backend are served under the same origin via nginx `/api` proxy,
|
||||||
|
# this can usually stay unset. Otherwise set the real dashboard origin(s).
|
||||||
|
# Example:
|
||||||
|
# CORS_ALLOWED_ORIGINS=https://dashboard.example.com
|
||||||
|
|
||||||
|
# Max upload size for backend validation (MB)
|
||||||
|
UPLOAD_MAX_MB=200
|
||||||
|
|
||||||
|
# Workspace files that should use direct download behavior in dashboard
|
||||||
|
WORKSPACE_DOWNLOAD_EXTENSIONS=.pdf,.doc,.docx,.xls,.xlsx,.xlsm,.ppt,.pptx,.odt,.ods,.odp,.wps,.stl,.scad,.zip,.rar
|
||||||
|
|
||||||
|
# Local speech-to-text (Whisper via whisper.cpp model file)
|
||||||
|
STT_ENABLED=true
|
||||||
|
STT_MODEL=ggml-small-q8_0.bin
|
||||||
|
STT_MODEL_DIR=${HOST_DATA_ROOT}/model
|
||||||
|
STT_DEVICE=cpu
|
||||||
|
STT_MAX_AUDIO_SECONDS=20
|
||||||
|
STT_DEFAULT_LANGUAGE=zh
|
||||||
|
STT_FORCE_SIMPLIFIED=true
|
||||||
|
STT_AUDIO_PREPROCESS=true
|
||||||
|
STT_AUDIO_FILTER=highpass=f=120,lowpass=f=7600,afftdn=nf=-20
|
||||||
|
STT_INITIAL_PROMPT=以下内容可能包含简体中文和英文术语。请优先输出简体中文,英文单词、缩写、品牌名和数字保持原文,不要翻译。
|
||||||
|
|
@ -49,6 +49,7 @@ frontend/coverage/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.full.example
|
||||||
!.env.prod.example
|
!.env.prod.example
|
||||||
backend/.env
|
backend/.env
|
||||||
frontend/.env
|
frontend/.env
|
||||||
|
|
|
||||||
50
README.md
50
README.md
|
|
@ -125,3 +125,53 @@ graph TD
|
||||||
- `HOST_BOTS_WORKSPACE_ROOT` 必须是宿主机绝对路径,并且在 `docker-compose.prod.yml` 中以“同路径”挂载到后端容器。
|
- `HOST_BOTS_WORKSPACE_ROOT` 必须是宿主机绝对路径,并且在 `docker-compose.prod.yml` 中以“同路径”挂载到后端容器。
|
||||||
原因:后端通过 Docker API 创建 Bot 容器时,使用的是宿主机可见的 bind 路径。
|
原因:后端通过 Docker API 创建 Bot 容器时,使用的是宿主机可见的 bind 路径。
|
||||||
- 语音识别当前基于 `pywhispercpp==1.3.1` + Whisper `.bin` 模型文件,不使用 `faster-whisper`。
|
- 语音识别当前基于 `pywhispercpp==1.3.1` + Whisper `.bin` 模型文件,不使用 `faster-whisper`。
|
||||||
|
|
||||||
|
## Docker 完整部署(内置 PostgreSQL / Redis)
|
||||||
|
|
||||||
|
这套方案和 `deploy-prod.sh` 并存,适合目标机器上直接把前端、后端、PostgreSQL、Redis 一起拉起。
|
||||||
|
|
||||||
|
### 文件
|
||||||
|
|
||||||
|
- `docker-compose.full.yml`
|
||||||
|
- `.env.full.example`
|
||||||
|
- `scripts/deploy-full.sh`
|
||||||
|
- `scripts/init-full-db.sh`
|
||||||
|
- `scripts/stop-full.sh`
|
||||||
|
- `scripts/sql/init-postgres-bootstrap.sql`
|
||||||
|
- `scripts/sql/init-postgres-app.sql`
|
||||||
|
|
||||||
|
### 启动步骤
|
||||||
|
|
||||||
|
1. 准备部署变量
|
||||||
|
- 复制 `.env.full.example` 为 `.env.full`
|
||||||
|
- 必填修改:
|
||||||
|
- `HOST_DATA_ROOT`
|
||||||
|
- `HOST_BOTS_WORKSPACE_ROOT`
|
||||||
|
- `POSTGRES_SUPERPASSWORD`
|
||||||
|
- `POSTGRES_APP_PASSWORD`
|
||||||
|
- `PANEL_ACCESS_PASSWORD`
|
||||||
|
- 如启用本地语音识别,请将 Whisper `.bin` 模型文件放到 `${HOST_DATA_ROOT}/model/`
|
||||||
|
2. 启动完整栈
|
||||||
|
- `./scripts/deploy-full.sh`
|
||||||
|
3. 访问
|
||||||
|
- `http://<host>:${NGINX_PORT}`(默认 `8080`)
|
||||||
|
|
||||||
|
### 初始化说明
|
||||||
|
|
||||||
|
- `scripts/deploy-full.sh` 会先启动 `postgres` / `redis`,然后自动调用 `scripts/init-full-db.sh`。
|
||||||
|
- `scripts/init-full-db.sh` 负责:
|
||||||
|
- 等待 PostgreSQL 就绪
|
||||||
|
- 创建或更新业务账号
|
||||||
|
- 创建业务库并授权
|
||||||
|
- 修正 `public` schema 权限
|
||||||
|
- Dashboard 业务表本身仍由后端启动时自动执行 `SQLModel.metadata.create_all(...)` 与补列/索引对齐。
|
||||||
|
|
||||||
|
### 停止
|
||||||
|
|
||||||
|
- `./scripts/stop-full.sh`
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `deploy-prod.sh` 和 `deploy-full.sh` 使用的是两套 compose 文件,但复用了相同容器名,不能同时在同一台机器上并行启动。
|
||||||
|
- PostgreSQL 数据默认落盘到 `${HOST_DATA_ROOT}/postgres`,Redis 数据默认落盘到 `${HOST_DATA_ROOT}/redis`。
|
||||||
|
- 如果你只想保留前后端容器,继续使用 `deploy-prod.sh`;如果希望把依赖也打包进来,使用 `deploy-full.sh`。
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
- 保持装配层足够薄
|
- 保持装配层足够薄
|
||||||
- 保持业务边界清晰
|
- 保持业务边界清晰
|
||||||
- 避免再次出现单文件多职责膨胀
|
- 避免出现单文件多职责膨胀
|
||||||
- 让后续迭代继续走低风险、小步验证路线
|
- 让后续迭代继续走低风险、小步验证路线
|
||||||
|
|
||||||
本文档自落地起作为**后续开发强制规范**执行。
|
本文档自落地起作为**后续开发强制规范**执行。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: ${POSTGRES_IMAGE:-postgres:16-alpine}
|
||||||
|
container_name: dashboard-nanobot-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ:-Asia/Shanghai}
|
||||||
|
POSTGRES_USER: ${POSTGRES_SUPERUSER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_SUPERPASSWORD:?POSTGRES_SUPERPASSWORD is required}
|
||||||
|
POSTGRES_DB: ${POSTGRES_BOOTSTRAP_DB:-postgres}
|
||||||
|
volumes:
|
||||||
|
- ${HOST_DATA_ROOT}/postgres:/var/lib/postgresql/data
|
||||||
|
expose:
|
||||||
|
- "5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 20s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "20m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: ${REDIS_IMAGE:-redis:7-alpine}
|
||||||
|
container_name: dashboard-nanobot-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ:-Asia/Shanghai}
|
||||||
|
command: ["redis-server", "--appendonly", "yes", "--save", "60", "1000"]
|
||||||
|
volumes:
|
||||||
|
- ${HOST_DATA_ROOT}/redis:/data
|
||||||
|
expose:
|
||||||
|
- "6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "20m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
args:
|
||||||
|
PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE:-python:3.12-slim}
|
||||||
|
PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.org/simple}
|
||||||
|
PIP_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-}
|
||||||
|
image: dashboard-nanobot/backend:${BACKEND_IMAGE_TAG:-latest}
|
||||||
|
container_name: dashboard-nanobot-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ:-Asia/Shanghai}
|
||||||
|
APP_HOST: 0.0.0.0
|
||||||
|
APP_PORT: 8000
|
||||||
|
APP_RELOAD: "false"
|
||||||
|
DATABASE_ECHO: "false"
|
||||||
|
DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-20}
|
||||||
|
DATABASE_MAX_OVERFLOW: ${DATABASE_MAX_OVERFLOW:-40}
|
||||||
|
DATABASE_POOL_TIMEOUT: ${DATABASE_POOL_TIMEOUT:-30}
|
||||||
|
DATABASE_POOL_RECYCLE: ${DATABASE_POOL_RECYCLE:-1800}
|
||||||
|
UPLOAD_MAX_MB: ${UPLOAD_MAX_MB:-100}
|
||||||
|
DATA_ROOT: ${HOST_DATA_ROOT}
|
||||||
|
BOTS_WORKSPACE_ROOT: ${HOST_BOTS_WORKSPACE_ROOT}
|
||||||
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_APP_DB}
|
||||||
|
REDIS_ENABLED: ${REDIS_ENABLED:-true}
|
||||||
|
REDIS_URL: redis://redis:6379/${REDIS_DB:-8}
|
||||||
|
REDIS_PREFIX: ${REDIS_PREFIX:-dashboard_nanobot}
|
||||||
|
REDIS_DEFAULT_TTL: ${REDIS_DEFAULT_TTL:-60}
|
||||||
|
CHAT_PULL_PAGE_SIZE: ${CHAT_PULL_PAGE_SIZE:-60}
|
||||||
|
COMMAND_AUTO_UNLOCK_SECONDS: ${COMMAND_AUTO_UNLOCK_SECONDS:-10}
|
||||||
|
DEFAULT_BOT_SYSTEM_TIMEZONE: ${DEFAULT_BOT_SYSTEM_TIMEZONE:-Asia/Shanghai}
|
||||||
|
PANEL_ACCESS_PASSWORD: ${PANEL_ACCESS_PASSWORD:-}
|
||||||
|
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-}
|
||||||
|
WORKSPACE_DOWNLOAD_EXTENSIONS: ${WORKSPACE_DOWNLOAD_EXTENSIONS:-}
|
||||||
|
STT_ENABLED: ${STT_ENABLED:-true}
|
||||||
|
STT_MODEL: ${STT_MODEL:-ggml-small-q8_0.bin}
|
||||||
|
STT_MODEL_DIR: ${STT_MODEL_DIR:-${HOST_DATA_ROOT}/model}
|
||||||
|
STT_DEVICE: ${STT_DEVICE:-cpu}
|
||||||
|
STT_MAX_AUDIO_SECONDS: ${STT_MAX_AUDIO_SECONDS:-20}
|
||||||
|
STT_DEFAULT_LANGUAGE: ${STT_DEFAULT_LANGUAGE:-zh}
|
||||||
|
STT_FORCE_SIMPLIFIED: ${STT_FORCE_SIMPLIFIED:-true}
|
||||||
|
STT_AUDIO_PREPROCESS: ${STT_AUDIO_PREPROCESS:-true}
|
||||||
|
STT_AUDIO_FILTER: ${STT_AUDIO_FILTER:-highpass=f=120,lowpass=f=7600,afftdn=nf=-20}
|
||||||
|
STT_INITIAL_PROMPT: ${STT_INITIAL_PROMPT:-以下内容可能包含简体中文和英文术语。请优先输出简体中文,英文单词、缩写、品牌名和数字保持原文,不要翻译。}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ${HOST_DATA_ROOT}:${HOST_DATA_ROOT}
|
||||||
|
- ${HOST_BOTS_WORKSPACE_ROOT}:${HOST_BOTS_WORKSPACE_ROOT}
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3).read()"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "20m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
NODE_BASE_IMAGE: ${NODE_BASE_IMAGE:-node:22-alpine}
|
||||||
|
NGINX_BASE_IMAGE: ${NGINX_BASE_IMAGE:-nginx:alpine}
|
||||||
|
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
|
||||||
|
VITE_API_BASE: /api
|
||||||
|
VITE_WS_BASE: /ws/monitor
|
||||||
|
image: dashboard-nanobot/nginx:${FRONTEND_IMAGE_TAG:-latest}
|
||||||
|
container_name: dashboard-nanobot-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ:-Asia/Shanghai}
|
||||||
|
UPLOAD_MAX_MB: ${UPLOAD_MAX_MB:-100}
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "${NGINX_PORT}:80"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "20m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: dashboard-nanobot-network
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
ENV_FILE="${1:-$ROOT_DIR/.env.full}"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker-compose.full.yml"
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
local path="$1"
|
||||||
|
local hint="$2"
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Missing file: $path"
|
||||||
|
if [[ -n "$hint" ]]; then
|
||||||
|
echo "$hint"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -n "${!name:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Missing required env: $name"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local line=""
|
||||||
|
local value=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
line="${line%$'\r'}"
|
||||||
|
[[ -z "${line//[[:space:]]/}" ]] && continue
|
||||||
|
[[ "${line#\#}" != "$line" ]] && continue
|
||||||
|
[[ "${line#export }" != "$line" ]] && line="${line#export }"
|
||||||
|
[[ "$line" == "$key="* ]] || continue
|
||||||
|
value="${line#*=}"
|
||||||
|
if [[ "$value" =~ ^\"(.*)\"$ ]]; then
|
||||||
|
value="${BASH_REMATCH[1]}"
|
||||||
|
elif [[ "$value" =~ ^\'(.*)\'$ ]]; then
|
||||||
|
value="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
printf '%s' "$value"
|
||||||
|
return 0
|
||||||
|
done < "$ENV_FILE"
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env_var() {
|
||||||
|
local name="$1"
|
||||||
|
local default_value="${2:-}"
|
||||||
|
local value=""
|
||||||
|
|
||||||
|
value="$(read_env_value "$name" || true)"
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
value="$default_value"
|
||||||
|
fi
|
||||||
|
printf -v "$name" '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_health() {
|
||||||
|
local container_name="$1"
|
||||||
|
local timeout_seconds="$2"
|
||||||
|
local elapsed=0
|
||||||
|
local status=""
|
||||||
|
|
||||||
|
while (( elapsed < timeout_seconds )); do
|
||||||
|
status="$(
|
||||||
|
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
if [[ "$status" == "healthy" || "$status" == "running" ]]; then
|
||||||
|
echo "[deploy-full] $container_name is $status"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
elapsed=$((elapsed + 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[deploy-full] timed out waiting for $container_name (last status: ${status:-unknown})"
|
||||||
|
docker logs --tail 80 "$container_name" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file "$ENV_FILE" "Create it from: $ROOT_DIR/.env.full.example"
|
||||||
|
require_file "$COMPOSE_FILE" ""
|
||||||
|
|
||||||
|
load_env_var HOST_DATA_ROOT
|
||||||
|
load_env_var HOST_BOTS_WORKSPACE_ROOT
|
||||||
|
load_env_var POSTGRES_SUPERUSER postgres
|
||||||
|
load_env_var POSTGRES_SUPERPASSWORD
|
||||||
|
load_env_var POSTGRES_BOOTSTRAP_DB postgres
|
||||||
|
load_env_var POSTGRES_APP_DB
|
||||||
|
load_env_var POSTGRES_APP_USER
|
||||||
|
load_env_var POSTGRES_APP_PASSWORD
|
||||||
|
load_env_var NGINX_PORT 8080
|
||||||
|
|
||||||
|
require_env HOST_DATA_ROOT
|
||||||
|
require_env HOST_BOTS_WORKSPACE_ROOT
|
||||||
|
require_env POSTGRES_SUPERUSER
|
||||||
|
require_env POSTGRES_SUPERPASSWORD
|
||||||
|
require_env POSTGRES_BOOTSTRAP_DB
|
||||||
|
require_env POSTGRES_APP_DB
|
||||||
|
require_env POSTGRES_APP_USER
|
||||||
|
require_env POSTGRES_APP_PASSWORD
|
||||||
|
require_env NGINX_PORT
|
||||||
|
|
||||||
|
echo "[deploy-full] using env: $ENV_FILE"
|
||||||
|
mkdir -p \
|
||||||
|
"$HOST_DATA_ROOT" \
|
||||||
|
"$HOST_DATA_ROOT/postgres" \
|
||||||
|
"$HOST_DATA_ROOT/redis" \
|
||||||
|
"$HOST_DATA_ROOT/model" \
|
||||||
|
"$HOST_BOTS_WORKSPACE_ROOT"
|
||||||
|
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config -q
|
||||||
|
|
||||||
|
echo "[deploy-full] starting postgres and redis"
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d postgres redis
|
||||||
|
|
||||||
|
wait_for_health "dashboard-nanobot-postgres" 120
|
||||||
|
wait_for_health "dashboard-nanobot-redis" 60
|
||||||
|
|
||||||
|
echo "[deploy-full] initializing application database"
|
||||||
|
"$ROOT_DIR/scripts/init-full-db.sh" "$ENV_FILE"
|
||||||
|
|
||||||
|
echo "[deploy-full] starting backend and nginx"
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d --build backend nginx
|
||||||
|
|
||||||
|
wait_for_health "dashboard-nanobot-backend" 180
|
||||||
|
wait_for_health "dashboard-nanobot-nginx" 120
|
||||||
|
|
||||||
|
echo "[deploy-full] service status"
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" ps
|
||||||
|
|
||||||
|
echo "[deploy-full] done"
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
ENV_FILE="${1:-$ROOT_DIR/.env.full}"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker-compose.full.yml"
|
||||||
|
BOOTSTRAP_SQL="$ROOT_DIR/scripts/sql/init-postgres-bootstrap.sql"
|
||||||
|
APP_SQL="$ROOT_DIR/scripts/sql/init-postgres-app.sql"
|
||||||
|
|
||||||
|
require_file() {
|
||||||
|
local path="$1"
|
||||||
|
local hint="$2"
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Missing file: $path"
|
||||||
|
if [[ -n "$hint" ]]; then
|
||||||
|
echo "$hint"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -n "${!name:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Missing required env: $name"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local line=""
|
||||||
|
local value=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
line="${line%$'\r'}"
|
||||||
|
[[ -z "${line//[[:space:]]/}" ]] && continue
|
||||||
|
[[ "${line#\#}" != "$line" ]] && continue
|
||||||
|
[[ "${line#export }" != "$line" ]] && line="${line#export }"
|
||||||
|
[[ "$line" == "$key="* ]] || continue
|
||||||
|
value="${line#*=}"
|
||||||
|
if [[ "$value" =~ ^\"(.*)\"$ ]]; then
|
||||||
|
value="${BASH_REMATCH[1]}"
|
||||||
|
elif [[ "$value" =~ ^\'(.*)\'$ ]]; then
|
||||||
|
value="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
printf '%s' "$value"
|
||||||
|
return 0
|
||||||
|
done < "$ENV_FILE"
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env_var() {
|
||||||
|
local name="$1"
|
||||||
|
local default_value="${2:-}"
|
||||||
|
local value=""
|
||||||
|
|
||||||
|
value="$(read_env_value "$name" || true)"
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
value="$default_value"
|
||||||
|
fi
|
||||||
|
printf -v "$name" '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_postgres() {
|
||||||
|
local timeout_seconds="${1:-120}"
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
while (( elapsed < timeout_seconds )); do
|
||||||
|
if docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \
|
||||||
|
-e PGPASSWORD="$POSTGRES_SUPERPASSWORD" \
|
||||||
|
postgres \
|
||||||
|
pg_isready -U "$POSTGRES_SUPERUSER" -d "$POSTGRES_BOOTSTRAP_DB" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
elapsed=$((elapsed + 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[init-full-db] timed out waiting for postgres"
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" logs --tail 100 postgres || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_file "$ENV_FILE" "Create it from: $ROOT_DIR/.env.full.example"
|
||||||
|
require_file "$COMPOSE_FILE" ""
|
||||||
|
require_file "$BOOTSTRAP_SQL" ""
|
||||||
|
require_file "$APP_SQL" ""
|
||||||
|
|
||||||
|
load_env_var POSTGRES_SUPERUSER postgres
|
||||||
|
load_env_var POSTGRES_SUPERPASSWORD
|
||||||
|
load_env_var POSTGRES_BOOTSTRAP_DB postgres
|
||||||
|
load_env_var POSTGRES_APP_DB
|
||||||
|
load_env_var POSTGRES_APP_USER
|
||||||
|
load_env_var POSTGRES_APP_PASSWORD
|
||||||
|
|
||||||
|
require_env POSTGRES_SUPERUSER
|
||||||
|
require_env POSTGRES_SUPERPASSWORD
|
||||||
|
require_env POSTGRES_BOOTSTRAP_DB
|
||||||
|
require_env POSTGRES_APP_DB
|
||||||
|
require_env POSTGRES_APP_USER
|
||||||
|
require_env POSTGRES_APP_PASSWORD
|
||||||
|
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d postgres >/dev/null
|
||||||
|
|
||||||
|
wait_for_postgres 120
|
||||||
|
|
||||||
|
echo "[init-full-db] ensuring role/database exist"
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \
|
||||||
|
-e PGPASSWORD="$POSTGRES_SUPERPASSWORD" \
|
||||||
|
postgres \
|
||||||
|
psql \
|
||||||
|
-v ON_ERROR_STOP=1 \
|
||||||
|
-v app_db="$POSTGRES_APP_DB" \
|
||||||
|
-v app_user="$POSTGRES_APP_USER" \
|
||||||
|
-v app_password="$POSTGRES_APP_PASSWORD" \
|
||||||
|
-U "$POSTGRES_SUPERUSER" \
|
||||||
|
-d "$POSTGRES_BOOTSTRAP_DB" \
|
||||||
|
-f - < "$BOOTSTRAP_SQL"
|
||||||
|
|
||||||
|
echo "[init-full-db] ensuring schema privileges in $POSTGRES_APP_DB"
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \
|
||||||
|
-e PGPASSWORD="$POSTGRES_SUPERPASSWORD" \
|
||||||
|
postgres \
|
||||||
|
psql \
|
||||||
|
-v ON_ERROR_STOP=1 \
|
||||||
|
-v app_user="$POSTGRES_APP_USER" \
|
||||||
|
-U "$POSTGRES_SUPERUSER" \
|
||||||
|
-d "$POSTGRES_APP_DB" \
|
||||||
|
-f - < "$APP_SQL"
|
||||||
|
|
||||||
|
echo "[init-full-db] done"
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
SELECT format('ALTER SCHEMA public OWNER TO %I', :'app_user')\gexec
|
||||||
|
SELECT format('GRANT ALL ON SCHEMA public TO %I', :'app_user')\gexec
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'app_user', :'app_password')
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_catalog.pg_roles
|
||||||
|
WHERE rolname = :'app_user'
|
||||||
|
)\gexec
|
||||||
|
|
||||||
|
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'app_user', :'app_password')\gexec
|
||||||
|
|
||||||
|
SELECT format('CREATE DATABASE %I OWNER %I', :'app_db', :'app_user')
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_database
|
||||||
|
WHERE datname = :'app_db'
|
||||||
|
)\gexec
|
||||||
|
|
||||||
|
SELECT format('ALTER DATABASE %I OWNER TO %I', :'app_db', :'app_user')\gexec
|
||||||
|
SELECT format('GRANT ALL PRIVILEGES ON DATABASE %I TO %I', :'app_db', :'app_user')\gexec
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
ENV_FILE="${1:-$ROOT_DIR/.env.full}"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker-compose.full.yml"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "Missing env file: $ENV_FILE"
|
||||||
|
echo "Create it from: $ROOT_DIR/.env.full.example"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down
|
||||||
Loading…
Reference in New Issue