Repository navigation
Docker Deployment and Upgrades
ChatHub ships as a single Docker image targeting PostgreSQL deployments.
services:
chathub:
image: docker.io/lqdflying/chathub:latest
depends_on:
chathub-db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://user:password@chathub-db:5432/postgres
- DATABASE_DRIVER=node
- KEY_VAULTS_SECRET=<your-secret>
- NEXTAUTH_SECRET=<your-secret>
- NEXTAUTH_URL=https://your-domain.com
- APP_URL=https://your-domain.com
# Optional: override server-to-server async dispatch origin.
# Docker defaults to container-local http://127.0.0.1:3210 when possible.
# INTERNAL_APP_URL=http://127.0.0.1:3210
# LLM API keys:
# OPENAI_API_KEY=...
# ANTHROPIC_API_KEY=...
# MOONSHOT_API_KEY=...
# MOONSHOT_PROXY_URL=https://your-moonshot-compatible-host/v1
# MINIMAX_API_KEY=...
# DEEPSEEK_API_KEY=...
# MIMO_API_KEY=...
# MIMO_PROXY_URL=https://api.xiaomimimo.com/v1
# OpenAI-compatible gateway:
# OPENAICOMPATIBLE_API_KEY=...
# OPENAICOMPATIBLE_PROXY_URL=https://your-host/v1
# Anthropic-compatible gateway:
# ANTHROPICCOMPATIBLE_API_KEY=...
# ANTHROPICCOMPATIBLE_PROXY_URL=https://your-host
# ANTHROPICCOMPATIBLE_AUTH_MODE=bearer # or 'api-key' (default)
# Optional: disable background conversation generation
# FEATURE_FLAGS=-durable_conversation_generation
# CONVERSATION_WORKER_CONCURRENCY=4
# DISABLE_CONVERSATION_WORKER=1
# Dedicated Knowledge Base embeddings (all three are required):
# RAG_EMBEDDING_PROVIDER=openai
# RAG_EMBEDDING_MODEL=text-embedding-3-small
# RAG_EMBEDDING_API_KEY=...
# Optional Sandbox tool, on an OpenSandbox server you run separately:
# OPENSANDBOX_SERVER_URL=http://opensandbox:8090
# OPENSANDBOX_API_KEY=<your-opensandbox-api-key>
# OPENSANDBOX_IMAGE=chathub-sandbox:1
ports:
- '3210:3210'
chathub-db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: postgres
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U user -d postgres']
interval: 5s
timeout: 5s
retries: 5Background conversation generation is on by default and uses the same server
LLM keys shown above (container env or Settings vault). Keys that exist only as
“fetch in the browser” fall back to the connected-tab runtime for that send.
See Background Conversation Generation.
The Sandbox tool runs on an OpenSandbox server you start separately. Setup is
Sandbox (OpenSandbox). Leave
OPENSANDBOX_SERVER_URL and OPENSANDBOX_IMAGE unset if you do not need it.
If an older Compose file still has a code-interpreter service, remove that
service and its depends_on entry together.
| Variable | Purpose |
|---|---|
FEATURE_FLAGS=-durable_conversation_generation |
Disable background conversation generation |
CONVERSATION_WORKER_CONCURRENCY |
Graphile worker concurrency (default 4) |
DISABLE_CONVERSATION_WORKER=1 |
Do not start the conversation worker in this process |
NODE_OPTIONS |
Node flags for the ChatHub process. Images from this change ship --max-old-space-size=4096 --dns-result-order=ipv4first --use-openssl-ca. Compose/env_file replaces the whole string (it does not merge). Keep one line only; a later duplicate in lobe.env wins. Raise the MiB value only when the host has spare RAM. |
OPENSANDBOX_SERVER_URL |
OpenSandbox lifecycle server (example http://opensandbox:8090). See Sandbox (OpenSandbox)
|
OPENSANDBOX_API_KEY |
Must equal the server's OPENSANDBOX_SERVER_API_KEY
|
OPENSANDBOX_IMAGE |
Sandbox image built from docker/opensandbox (example chathub-sandbox:1) |
APP_URL should remain the public browser/OAuth/webhook origin. Background
async jobs use INTERNAL_APP_URL only for server-to-server /trpc/async
dispatch. If INTERNAL_APP_URL is unset, the Docker image uses the
container-local loopback route when local rewrite is enabled, then falls back to
APP_URL. Set INTERNAL_APP_URL to a root HTTP(S) origin only, for example
http://127.0.0.1:3210. Do not include credentials, a path, query parameters,
or a fragment. ChatHub rejects those forms and uses the fallback route instead.
1Panel stores each site's generated reverse-proxy locations under
/www/sites/<domain>/proxy/*.conf and includes them from the site's HTTPS
server {} block. For a site whose ChatHub container is published on host
port 33210, replace the complete contents of its per-site
proxy/root.conf with the following configuration.
Do not add another location / or location ^~ / block elsewhere for the
same site. A duplicate location prevents OpenResty from reloading.
# Complete content for 1Panel's per-site proxy/root.conf.
location ^~ / {
proxy_pass http://127.0.0.1:33210;
proxy_http_version 1.1;
# This affects this OpenResty hop only.
# Any CDN, gateway, or load balancer in front must allow the same duration.
proxy_connect_timeout 15s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
send_timeout 600s;
# Stream MCP, SSE, and other incremental responses without body buffering.
proxy_buffering off;
proxy_buffer_size 32k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
# Do not cache or retry state-changing MCP requests.
proxy_cache off;
proxy_next_upstream off;
proxy_intercept_errors off;
# Forward request information.
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header REMOTE-HOST $remote_addr;
# Match 1Panel's self-contained WebSocket header pattern.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}$http_connection is an Nginx built-in variable containing the incoming
Connection request header, so this per-site file does not depend on a custom
$connection_upgrade variable. Nginx recommends a controlled
map for deployments
that can edit the global http {} context. If such a map already exists,
replace $http_connection with $connection_upgrade; never declare map
inside server {} or location {}.
With proxy_buffering off, response bodies are passed to the client as they
arrive. proxy_buffer_size remains the initial upstream response/header
buffer. proxy_buffers and proxy_busy_buffers_size apply only if buffering
is later enabled, so they are retained as explicit fallback sizing and do not
turn buffering back on. See the
Nginx proxy module documentation
for the directive contexts and timeout semantics.
Keep Strict-Transport-Security and Alt-Svc in 1Panel's HTTPS
server {} configuration. Do not duplicate those response headers in
proxy/root.conf.
After saving the file:
- Use 1Panel's Save and reload action so OpenResty validates the complete generated configuration before applying it.
- Confirm no CDN, application gateway, or load balancer in front of 1Panel retains a shorter timeout.
- Repeat a long or large MCP request. A successful proxy correction removes
the OpenResty HTML
504at approximately 60 seconds and allowsmcp.callToolto complete withoutmcp.recoverToolResult.
The timeout increase prevents this OpenResty hop from closing an idle upstream read at Nginx's default 60-second boundary. It does not repair a timeout enforced by another proxy layer or an application failure.
If ChatHub already uses split env files (lobe.env, markitdown.env,
pgdb.env, redis.env), put Sandbox keys in lobe.env with the other
ChatHub settings. Do not leave them only in Compose environment: while
every other service uses env_file.
The server, image, and variable list are on
Sandbox (OpenSandbox). ChatHub does not
depends_on that server. Leave OPENSANDBOX_SERVER_URL and
OPENSANDBOX_IMAGE unset if you do not need Sandbox. ChatHub still boots;
the tool returns a configuration error.
Upgrading from the Dify sidecar: remove the code-interpreter service and
its depends_on entry in the same edit, and delete SANDBOX_PROVIDER,
CODE_INTERPRETER_SANDBOX_URL, and CODE_INTERPRETER_SANDBOX_API_KEY from
lobe.env. depends_on must stay all mappings. Mixing - lobe-db with a
mapping entry fails with did not find expected '-' indicator.
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
DATABASE_DRIVER |
Always node
|
KEY_VAULTS_SECRET |
Encryption key for stored secrets; preserve the same value when moving or restoring a deployment |
NEXTAUTH_SECRET |
NextAuth session encryption key |
| Variable | Description |
|---|---|
NEXT_PUBLIC_ENABLE_NEXT_AUTH |
Enable NextAuth (set to 1) |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
Enable Clerk auth |
AUTH_TOKEN |
API Bearer token |
The container refuses to start without at least one of these configured.
| Provider | Env |
|---|---|
| OpenAI (GPT) | OPENAI_API_KEY |
| Anthropic (Claude) | ANTHROPIC_API_KEY |
| Google (Gemini) | GOOGLE_API_KEY |
| Moonshot (Kimi) |
MOONSHOT_API_KEY + optional MOONSHOT_PROXY_URL
|
| MiniMax | MINIMAX_API_KEY |
| DeepSeek | DEEPSEEK_API_KEY |
| Xiaomi MiMo |
MIMO_API_KEY + optional MIMO_PROXY_URL
|
| OpenAI-compatible |
OPENAICOMPATIBLE_API_KEY + OPENAICOMPATIBLE_PROXY_URL
|
| Anthropic-compatible |
ANTHROPICCOMPATIBLE_API_KEY + ANTHROPICCOMPATIBLE_PROXY_URL + optional ANTHROPICCOMPATIBLE_AUTH_MODE
|
| Ollama | auto-discovered (set ENABLED_OLLAMA=0 to disable) |
For the complete provider/env map (Azure, Bedrock, OpenRouter, and 40+ others), see src/envs/llm.ts.
Knowledge Base vectors use a dedicated provider, separate from the LLM keys above. Set all three required values together:
RAG_EMBEDDING_PROVIDER=openai
RAG_EMBEDDING_MODEL=text-embedding-3-small
RAG_EMBEDDING_API_KEY=sk-...
# Optional HTTP(S) proxy or compatible endpoint:
# RAG_EMBEDDING_BASE_URL=https://api.openai.com/v1The optional base URL must be a secret-free API root without URL credentials,
query parameters, or fragments. Keep authentication in
RAG_EMBEDDING_API_KEY.
Supported provider values are openai, cohere, and voyage. A deployment
may omit these variables and let each account configure an encrypted override
under Settings -> RAG Provider, but RAG indexing and retrieval remain
unavailable until one complete configuration exists. Chat provider keys and
DEFAULT_FILES_CONFIG are not fallbacks. See
Knowledge Base and RAG.
NEXT_PUBLIC_ENABLE_NEXT_AUTH=1
NEXT_AUTH_SSO_PROVIDERS=credentials
AUTH_CREDENTIALS_USERNAME=admin
AUTH_CREDENTIALS_PASSWORD=your-strong-password
AUTH_TOKEN=your-secret-token
AUTH_SESSION_MAX_AGE_DAYS=7
ChatHub keys belong in the ChatHub env file (lobe.env on 1Panel). See
Sandbox (OpenSandbox).
OPENSANDBOX_SERVER_URL=http://opensandbox:8090
OPENSANDBOX_API_KEY=<your-opensandbox-api-key>
OPENSANDBOX_IMAGE=chathub-sandbox:1S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_ENDPOINT=...
S3_BUCKET=...
S3_REGION=...
New authenticated file uploads receive a server-selected object key; clients
provide a filename and purpose but cannot choose the presigned PUT path. Normal
files use <file-root>/<account-hash>/<hour>/<random>.<extension>, while RAG
evaluation imports use a separate ragEval/<account-hash>/... namespace. The
hash prevents raw account IDs from appearing in object names; authorization is
still enforced by the application and database. Existing legacy object keys
remain readable and are not renamed during upgrade.
After upgrading a deployment that accepted older unscoped upload keys, this read-only PostgreSQL query identifies global file rows where one object URL is associated with distinct content hashes:
SELECT
url,
count(*) AS global_file_rows,
count(DISTINCT hash_id) AS distinct_hashes,
array_agg(hash_id ORDER BY hash_id) AS hashes
FROM global_files
GROUP BY url
HAVING count(DISTINCT hash_id) > 1
ORDER BY distinct_hashes DESC, url;An empty result needs no action. Treat non-empty rows as an audit finding, not an automatic deletion list: inspect the current object content, PostgreSQL references, and backups before repairing anything. ChatHub does not delete or rewrite these historical rows automatically because the database alone cannot determine which overwritten object version is correct.
The PostgreSQL data directory should be mounted to a persistent volume:
volumes:
- ./data/postgres:/var/lib/postgresql/dataBack up this directory before major upgrades. The application image is stateless. Relational data such as conversations, settings, file records, and OAuth tokens lives in PostgreSQL; uploaded binary objects live in the configured S3-compatible storage.
ChatHub also provides a user-level JSON backup under Settings → Storage. That workflow covers core settings and conversations but intentionally excludes binary/object-storage and authentication data. It is not a replacement for a PostgreSQL volume backup.
JSON backups retain provider credentials as encrypted ciphertext. Preserve the
original KEY_VAULTS_SECRET in a secure secret manager: a deployment using a
different value cannot restore those credentials, and ChatHub will block the
import before changing database data. See
Data Backup and Restore for the complete scope and
restore procedure.
Background conversation generation is on by default. Closing a tab does not
cancel a reply. Disable with FEATURE_FLAGS=-durable_conversation_generation.
See Background Conversation Generation.
Migrations run automatically on container startup via scripts/migrateServerDB/docker.cjs. Drizzle ORM applies pending migrations from packages/database/migrations/.
Knowledge Base vector storage requires the PostgreSQL vector extension. Use
pgvector/pgvector:pg16 for the bundled Compose database, or install pgvector
on an external/managed PostgreSQL service before starting ChatHub. Migration
0005_pgvector.sql enables the extension and later migrations create the
1,024-dimensional vector column and HNSW cosine index. If an upgrade reports
that the extension is missing, install pgvector and restart the application so
the normal migration entrypoint can retry; do not edit the migration journal.
Migration 0053_add_rag_embedding_indexes.sql builds the HNSW index with
regular CREATE INDEX inside Drizzle's migration transaction. PostgreSQL keeps
reads available but blocks writes to embeddings for the duration of the
build; CREATE INDEX CONCURRENTLY cannot run in that transaction. For a large
embedding table, estimate the index-build time and schedule a maintenance
window before starting the upgrade.
Every migration that adds a table or column also has an idempotent ensure* helper. These CREATE IF NOT EXISTS / ALTER TABLE ADD COLUMN IF NOT EXISTS queries protect against journal drift -- deployments where the __drizzle_migrations table records a migration as applied but the actual DDL never executed (failed build, restored DB backup, partial transaction).
Relevant files:
-
scripts/migrateServerDB/docker.cjs-- orchestrates migrate + ensure calls -
scripts/migrateServerDB/ensure*.cjs-- individual safety-net helpers -
Dockerfile-- copies ensure helpers into/app/
These helpers are safe to run on every startup regardless of migration journal state.
Upgrades containing the chat-group ownership migration run an idempotent repair before enforcing the new database constraints:
- A membership with a stale stored owner is normalized only when its referenced group and agent already have the same owner.
- A membership with a missing user, group, or agent parent is removed, as is a membership linking a group and agent owned by different accounts. The deployment cannot infer the intended owner safely.
- The repair inspects the composite parent indexes and all five membership foreign keys structurally. Missing, unvalidated, or semantically incorrect definitions are replaced and validated.
- Every membership foreign key converges to
ON DELETE CASCADEandON UPDATE NO ACTION, so later user, group, or agent deletion removes the junction row instead of being blocked by drifted membership data.
The same repair runs after Drizzle migrations on every container startup, so it
also covers restored databases, previously applied migration variants, and
migration-journal drift. Back up PostgreSQL before upgrading and review
migration errors in docker compose logs chathub. Restarting is safe after
correcting a database availability issue because normalization, orphan cleanup,
and constraint convergence are transactional and idempotent.
Current images require PostgreSQL and do not open browser PGlite or IndexedDB/Dexie databases. Before replacing an old local edition, create its JSON export from the original browser profile. After deploying ChatHub and signing in, restore the file from Settings -> Storage. There is no automatic container migration from browser database files. See Data Backup and Restore for compatibility and credential-key requirements.
- Back up your PostgreSQL volume before upgrading.
- Pull the new image:
docker pull docker.io/lqdflying/chathub:latest - Update your
docker-compose.ymlif needed (check release notes for env var changes). - Restart:
docker compose up -d - Check logs:
docker compose logs chathub-- verify migrations ran without errors.
Migrations run on every container start, so schema changes are applied automatically.
Pin to a specific version for production:
image: docker.io/lqdflying/chathub:v1.0.0Then upgrade by changing the tag and restarting. This avoids surprises from :latest.
If migrations fail mid-way:
- Check
docker compose logs chathubfor the Drizzle error. - The
ensure*helpers run after migrations and will create any missing tables/columns regardless of journal state. - Restart the container -- the safety-net queries are idempotent.
If startup loops on relation "openai_codex_oauth_tokens_user_id_unique" already exists from ensureOpenAICodexOAuthTokens.cjs after the ChatGPT / Codex tables exist, the image's safety-net helper tried to add a UNIQUE constraint whose index Drizzle already created. PostgreSQL reports that as 42P07 (duplicate_table), not a duplicate constraint. Upgrade to an image that skips an existing unique index. Do not drop the table or edit __drizzle_migrations.
Check:
-
DATABASE_URLis reachable from the container - PostgreSQL healthcheck passes (
pg_isready) - At least one auth method is configured (container refuses to start without it)
If logs loop on Failed to prepare server and Cannot find module 'tslib'
from /app/node_modules/graphile-worker, the image is missing Graphile
Worker's runtime helpers. That is an image packaging bug, not a Compose
misconfiguration. DISABLE_CONVERSATION_WORKER=1 does not skip that import.
Upgrade to an image that overlays the full /deps node_modules (including
tslib) onto the standalone tree. See
Background Conversation Generation.
If logs show FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory then Process exited after SIGABRT, V8 hit --max-old-space-size.
Confirm the running value:
docker inspect lobe-chat --format '{{range .Config.Env}}{{println .}}{{end}}' | grep NODE_OPTIONS
docker exec lobe-chat /bin/node -p "Math.round(require('v8').getHeapStatistics().heap_size_limit/1024/1024)+' MiB'"A 4096 MiB old-space limit prints about 4288 MiB. About 2240 MiB means
the 2 GiB Node default is still in effect (missing heap flag, or a second
NODE_OPTIONS line in lobe.env without --max-old-space-size). Recreate
the container after editing env; docker compose restart does not reload
env_file. The host must have more free RAM than the heap plus PostgreSQL
and sidecars.
If you encounter "No space left on device" during Docker builds, free runner disk before building:
docker system prune -afhealthcheck:
test: ['CMD-SHELL', 'pg_isready -U user -d postgres']
interval: 5s
timeout: 5s
retries: 5Docker images are built by .github/workflows/docker-release.yml on tag pushes:
-
v*.*.*→ pusheslqdflying/chathub:X.Y.Zand:latest(GA only) -
v*.*.*-canary.*→ pusheslqdflying/chathub:X.Y.Z-canary.N(does not touch:latest)
The workflow frees unused runner disk before building to avoid space issues.
- Home
- Quick Start
- Docker Deployment and Upgrades
- Authentication
- Model Providers
- Knowledge Base and RAG
- MarkItDown Sidecar
- Sandbox (OpenSandbox)
- OpenAI Compatible Cache Matrix
- Model Extension Options
- Memory & Context Compaction
- Background Conversation Generation
- Pasted Text
- Context Export
- Data Backup and Restore
- Image Generation
- Artifacts
- Inline SVG Diagrams
- Tools Hub
- MCP OAuth
- MCP Management
- Agent Skills
- Database
- Development
- Debug and Diagnostics
- Architecture Overview
- Docker Release
- Release Workflow