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:
¨NW¨
2026-04-08 23:09:28 +01:00
parent cee297b4db
commit 322cebf475
8 changed files with 119 additions and 67 deletions

View File

@@ -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=

View File

@@ -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

View File

@@ -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:-}"

View File

@@ -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:

View File

@@ -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

View File

@@ -12,4 +12,7 @@ export default defineConfig({
dbCredentials: {
url: connectionString,
},
migrations: {
prefix: "timestamp",
},
});

View File

@@ -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

View File

@@ -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`