fix(prod): production startup fixes — health endpoint, serveStatic path, entrypoint, docker config
- Add /api/health endpoint for Docker healthchecks - Fix serveStatic path: dist/public instead of ../public - Fix entrypoint.sh: DB wait check, npx drizzle-kit migrate, add netcat - Fix Dockerfile: add bash/netcat, fix COPY order, add tsconfig.node.json - Fix docker-compose.yml: add OLLAMA/LLM env vars for Node.js fallback - Fix docker-stack.yml: remove template vars, use env vars instead of secrets - Fix drizzle.config.ts: add migrations prefix - Update .env.example with full LLM provider documentation
This commit is contained in:
44
.env.example
44
.env.example
@@ -3,21 +3,41 @@
|
||||
# Скопируйте этот файл в .env и заполните значения
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
# Ollama API (обязательно для работы чата и списка моделей)
|
||||
# ── LLM Provider ───────────────────────────────
|
||||
# Option 1: Ollama Cloud (default)
|
||||
LLM_BASE_URL=https://ollama.com/v1
|
||||
LLM_API_KEY=your_api_key_here
|
||||
|
||||
# Option 2: Direct Ollama (legacy aliases)
|
||||
OLLAMA_BASE_URL=https://ollama.com/v1
|
||||
OLLAMA_API_KEY=your_ollama_api_key_here
|
||||
OLLAMA_API_KEY=
|
||||
|
||||
# База данных MySQL/TiDB
|
||||
DATABASE_URL=mysql://goclaw:password@localhost:3306/goclaw
|
||||
# Option 3: OpenAI-compatible
|
||||
# LLM_BASE_URL=https://api.openai.com/v1
|
||||
# LLM_API_KEY=sk-...
|
||||
|
||||
# JWT Secret — случайная строка для подписи сессионных токенов
|
||||
# Сгенерировать: openssl rand -hex 32
|
||||
JWT_SECRET=change_me_to_random_secret
|
||||
# Default model for orchestrator (when DB config unavailable)
|
||||
DEFAULT_MODEL=qwen2.5:7b
|
||||
|
||||
# Telegram Bot (опционально)
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_WEBHOOK_URL=
|
||||
# ── Database ───────────────────────────────────
|
||||
# MySQL/TiDB connection string
|
||||
DATABASE_URL=mysql://goclaw:goClawPass123@localhost:3306/goclaw
|
||||
|
||||
# GoClaw Gateway (опционально)
|
||||
# ── Authentication ─────────────────────────────
|
||||
# JWT Secret — generate: openssl rand -hex 32
|
||||
JWT_SECRET=change_me_to_a_random_64_char_string
|
||||
|
||||
# ── GoClaw Gateway ─────────────────────────────
|
||||
# URL of the Go Gateway (blank = fallback to Node.js orchestrator)
|
||||
GATEWAY_URL=http://localhost:18789
|
||||
GATEWAY_API_KEY=
|
||||
|
||||
# ── Manus OAuth (optional) ─────────────────────
|
||||
VITE_APP_ID=
|
||||
OAUTH_SERVER_URL=
|
||||
VITE_OAUTH_PORTAL_URL=
|
||||
|
||||
# ── Manus Built-in APIs (optional) ─────────────
|
||||
BUILT_IN_FORGE_API_URL=
|
||||
BUILT_IN_FORGE_API_KEY=
|
||||
VITE_FRONTEND_FORGE_API_KEY=
|
||||
VITE_FRONTEND_FORGE_API_URL=
|
||||
@@ -11,14 +11,14 @@ COPY package.json pnpm-lock.yaml ./
|
||||
COPY patches/ ./patches/
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code (exclude gateway/ and docker/)
|
||||
# Copy all source code needed for build
|
||||
COPY client/ ./client/
|
||||
COPY server/ ./server/
|
||||
COPY shared/ ./shared/
|
||||
COPY drizzle/ ./drizzle/
|
||||
COPY drizzle.config.ts tsconfig.json vite.config.ts ./
|
||||
COPY drizzle.config.ts tsconfig.json tsconfig.node.json vite.config.ts vitest.config.ts ./
|
||||
|
||||
# Build frontend and backend
|
||||
# Build frontend (Vite) and backend (esbuild)
|
||||
RUN pnpm build
|
||||
|
||||
# ── Runtime Stage ─────────────────────────────────────────────────────────────
|
||||
@@ -28,12 +28,10 @@ FROM node:22-alpine
|
||||
RUN apk add --no-cache \
|
||||
wget \
|
||||
curl \
|
||||
bash \
|
||||
netcat-openbsd \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 goclaw && \
|
||||
adduser -u 1001 -G goclaw -s /bin/sh -D goclaw
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm
|
||||
@@ -42,22 +40,24 @@ RUN npm install -g pnpm@latest
|
||||
# Copy package files and install production deps only
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY patches/ ./patches/
|
||||
# Install all deps (vite is needed at runtime for SSR/proxy, drizzle-kit for migrations)
|
||||
# Install all deps (drizzle-kit needed for migrations at runtime)
|
||||
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
# Copy built artifacts
|
||||
# Copy built artifacts from builder
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/drizzle ./drizzle
|
||||
|
||||
# Copy drizzle config for migrations (needed by drizzle-kit at runtime)
|
||||
# Copy drizzle config and migrations for runtime migration
|
||||
COPY drizzle.config.ts ./drizzle.config.ts
|
||||
COPY drizzle/ ./drizzle/
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Set ownership
|
||||
RUN chown -R goclaw:goclaw /app
|
||||
# Run as non-root user
|
||||
RUN addgroup -g 1001 goclaw && \
|
||||
adduser -u 1001 -G goclaw -s /bin/sh -D goclaw && \
|
||||
chown -R goclaw:goclaw /app
|
||||
|
||||
USER goclaw
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ volumes:
|
||||
# ollama-data: # Uncomment when using local Ollama service below
|
||||
|
||||
services:
|
||||
|
||||
# ── MySQL 8 ──────────────────────────────────────────────────────────────
|
||||
db:
|
||||
image: mysql:8.0
|
||||
@@ -52,7 +51,17 @@ services:
|
||||
networks:
|
||||
- goclaw-net
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-goClawRoot123}"]
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"mysqladmin",
|
||||
"ping",
|
||||
"-h",
|
||||
"localhost",
|
||||
"-u",
|
||||
"root",
|
||||
"-p${MYSQL_ROOT_PASSWORD:-goClawRoot123}",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -99,7 +108,7 @@ services:
|
||||
# ── LLM Provider ─────────────────────────────────────────────────────
|
||||
# Cloud default (Ollama Cloud, OpenAI-compatible):
|
||||
LLM_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}"
|
||||
LLM_API_KEY: "${LLM_API_KEY:-${OLLAMA_API_KEY:-}}"
|
||||
LLM_API_KEY: "${LLM_API_KEY:-${OLLAMA_API_KEY:-}}"
|
||||
# Legacy alias (still supported):
|
||||
OLLAMA_API_KEY: "${OLLAMA_API_KEY:-${LLM_API_KEY:-}}"
|
||||
# ── To use local Ollama on GPU node, set in .env: ─────────────────────
|
||||
@@ -145,6 +154,8 @@ services:
|
||||
DATABASE_URL: "mysql://${MYSQL_USER:-goclaw}:${MYSQL_PASSWORD:-goClawPass123}@db:3306/${MYSQL_DATABASE:-goclaw}"
|
||||
GATEWAY_URL: "http://gateway:18789"
|
||||
JWT_SECRET: "${JWT_SECRET:-change-me-in-production}"
|
||||
OLLAMA_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}"
|
||||
OLLAMA_API_KEY: "${LLM_API_KEY:-}"
|
||||
VITE_APP_ID: "${VITE_APP_ID:-}"
|
||||
OAUTH_SERVER_URL: "${OAUTH_SERVER_URL:-}"
|
||||
VITE_OAUTH_PORTAL_URL: "${VITE_OAUTH_PORTAL_URL:-}"
|
||||
|
||||
@@ -40,31 +40,15 @@ volumes:
|
||||
# ollama-data: # Uncomment when using local Ollama service below
|
||||
# driver: local
|
||||
|
||||
secrets:
|
||||
mysql-root-password:
|
||||
external: true
|
||||
mysql-password:
|
||||
external: true
|
||||
jwt-secret:
|
||||
external: true
|
||||
llm-api-key:
|
||||
external: true
|
||||
|
||||
services:
|
||||
|
||||
# ── MySQL 8 ──────────────────────────────────────────────────────────────
|
||||
db:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password
|
||||
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-goClawRoot123}"
|
||||
MYSQL_DATABASE: goclaw
|
||||
MYSQL_USER: goclaw
|
||||
MYSQL_PASSWORD_FILE: /run/secrets/mysql-password
|
||||
secrets:
|
||||
- mysql-root-password
|
||||
- mysql-password
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
MYSQL_PASSWORD: "${MYSQL_PASSWORD:-goClawPass123}"
|
||||
networks:
|
||||
- goclaw-net
|
||||
deploy:
|
||||
@@ -123,23 +107,18 @@ services:
|
||||
environment:
|
||||
PORT: "18789"
|
||||
# ── LLM Provider ─────────────────────────────────────────────────────
|
||||
# Default: Ollama Cloud (requires llm-api-key secret below)
|
||||
# Default: Ollama Cloud (requires LLM_API_KEY)
|
||||
LLM_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}"
|
||||
LLM_API_KEY: "${LLM_API_KEY:-}"
|
||||
DEFAULT_MODEL: "${DEFAULT_MODEL:-qwen2.5:7b}"
|
||||
# LLM_API_KEY is injected via /run/secrets/llm-api-key (see below)
|
||||
# ── To switch to local GPU Ollama, set: ──────────────────────────────
|
||||
# LLM_BASE_URL: "http://ollama:11434"
|
||||
# (and uncomment the ollama service above)
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
DATABASE_URL: "goclaw:{{MYSQL_PASSWORD}}@tcp(db:3306)/goclaw?parseTime=true"
|
||||
DATABASE_URL: "goclaw:goClawPass123@tcp(db:3306)/goclaw?parseTime=true"
|
||||
PROJECT_ROOT: "/app"
|
||||
GATEWAY_REQUEST_TIMEOUT_SECS: "120"
|
||||
GATEWAY_MAX_TOOL_ITERATIONS: "10"
|
||||
LOG_LEVEL: "info"
|
||||
secrets:
|
||||
- mysql-password
|
||||
- source: llm-api-key
|
||||
target: /run/secrets/llm-api-key
|
||||
networks:
|
||||
- goclaw-net
|
||||
ports:
|
||||
@@ -183,11 +162,11 @@ services:
|
||||
image: git.softuniq.eu/uniqai/goclaw/control-center:latest
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DATABASE_URL: "mysql://goclaw:{{MYSQL_PASSWORD}}@db:3306/goclaw"
|
||||
DATABASE_URL: "mysql://goclaw:goClawPass123@db:3306/goclaw"
|
||||
GATEWAY_URL: "http://gateway:18789"
|
||||
secrets:
|
||||
- mysql-password
|
||||
- jwt-secret
|
||||
JWT_SECRET: "${JWT_SECRET:-change-me-in-production}"
|
||||
LLM_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}"
|
||||
LLM_API_KEY: "${LLM_API_KEY:-}"
|
||||
networks:
|
||||
- goclaw-net
|
||||
ports:
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
#!/bin/sh
|
||||
# GoClaw Control Center entrypoint
|
||||
# Runs Drizzle migrations before starting the server
|
||||
# Runs database migrations before starting the server
|
||||
|
||||
set -e
|
||||
|
||||
echo "[Entrypoint] Running database migrations..."
|
||||
echo "[Entrypoint] Waiting for database connectivity..."
|
||||
|
||||
# Wait for database to be reachable (max 30 attempts = 60 seconds)
|
||||
DB_READY=false
|
||||
ATTEMPT=0
|
||||
MAX_ATTEMPTS=30
|
||||
|
||||
# Parse DATABASE_URL for connection check
|
||||
# Supports: mysql://user:pass@host:port/dbname
|
||||
if [ -n "$DATABASE_URL" ]; then
|
||||
# Extract host and port from DATABASE_URL
|
||||
DB_HOST=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:/]+)(:[0-9]+)?/.*|\1|')
|
||||
DB_PORT=$(echo "$DATABASE_URL" | sed -E 's|.*@[^:/]+:([0-9]+)/.*|\1|')
|
||||
if [ -z "$DB_PORT" ]; then DB_PORT=3306; fi
|
||||
|
||||
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
if nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null || timeout 2 sh -c "echo > /dev/tcp/$DB_HOST/$DB_PORT" 2>/dev/null; then
|
||||
DB_READY=true
|
||||
echo "[Entrypoint] Database is reachable (attempt $ATTEMPT/$MAX_ATTEMPTS)"
|
||||
break
|
||||
fi
|
||||
echo "[Entrypoint] Waiting for database at $DB_HOST:$DB_PORT... (attempt $ATTEMPT/$MAX_ATTEMPTS)"
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$DB_READY" = "false" ] && [ -n "$DATABASE_URL" ]; then
|
||||
echo "[Entrypoint] WARNING: Database not reachable after $MAX_ATTEMPTS attempts. Continuing anyway..."
|
||||
fi
|
||||
|
||||
echo "[Entrypoint] Running database migrations (via drizzle-kit)..."
|
||||
|
||||
# Run drizzle-kit migrate — applies all pending migrations idempotently
|
||||
# drizzle.config.ts and drizzle/migrations/ are copied into /app during build
|
||||
cd /app && node_modules/.bin/drizzle-kit migrate 2>&1
|
||||
cd /app && npx drizzle-kit migrate 2>&1
|
||||
|
||||
MIGRATE_EXIT=$?
|
||||
if [ $MIGRATE_EXIT -ne 0 ]; then
|
||||
echo "[Entrypoint] WARNING: Migration failed (exit $MIGRATE_EXIT). Starting server anyway..."
|
||||
echo "[Entrypoint] WARNING: Migration failed (exit $MIGRATE_EXIT). Starting server anyway — seed will handle schema..."
|
||||
else
|
||||
echo "[Entrypoint] Migrations applied successfully."
|
||||
fi
|
||||
|
||||
@@ -12,4 +12,7 @@ export default defineConfig({
|
||||
dbCredentials: {
|
||||
url: connectionString,
|
||||
},
|
||||
migrations: {
|
||||
prefix: "timestamp",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,6 +34,16 @@ async function startServer() {
|
||||
// Configure body parser with larger size limit for file uploads
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||
|
||||
// Health check endpoint for Docker HEALTHCHECK and load balancers
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
uptime: Math.floor(process.uptime()),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// OAuth callback under /api/oauth/callback
|
||||
registerOAuthRoutes(app);
|
||||
// tRPC API
|
||||
|
||||
@@ -48,10 +48,9 @@ export async function setupVite(app: Express, server: Server) {
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath =
|
||||
process.env.NODE_ENV === "development"
|
||||
? path.resolve(import.meta.dirname, "../..", "dist", "public")
|
||||
: path.resolve(import.meta.dirname, "public");
|
||||
// dist/index.js is the server bundle, dist/public/ is the client build
|
||||
// import.meta.dirname = .../dist → public is at dist/public
|
||||
const distPath = path.resolve(import.meta.dirname, "public");
|
||||
if (!fs.existsSync(distPath)) {
|
||||
console.error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`
|
||||
|
||||
Reference in New Issue
Block a user