Skip to content

Docker Deployment and Upgrades

lqdflying edited this page Oct 4, 2026 · 28 revisions

Docker Deployment and Upgrades

ChatHub ships as a single Docker image targeting PostgreSQL deployments.


Docker Compose (Quick Start)

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

Background 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 OpenResty Reverse Proxy

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:

  1. Use 1Panel's Save and reload action so OpenResty validates the complete generated configuration before applying it.
  2. Confirm no CDN, application gateway, or load balancer in front of 1Panel retains a shorter timeout.
  3. Repeat a long or large MCP request. A successful proxy correction removes the OpenResty HTML 504 at approximately 60 seconds and allows mcp.callTool to complete without mcp.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.


1Panel Compose env files

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.


Environment Variables

Required

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

Auth (at least one required)

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.

LLM API Keys

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 RAG embeddings

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/v1

The 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.

Credentials Login (optional)

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

Sandbox (optional)

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

S3 (Picbed and file storage)

S3_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.


Volume Management

The PostgreSQL data directory should be mounted to a persistent volume:

volumes:
  - ./data/postgres:/var/lib/postgresql/data

Back 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.


Database Migrations

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.

Migration Safety Net

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.

Chat-group membership ownership repair

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 CASCADE and ON 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.


Upgrade Procedure

Moving from a retired browser-local edition

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.

  1. Back up your PostgreSQL volume before upgrading.
  2. Pull the new image: docker pull docker.io/lqdflying/chathub:latest
  3. Update your docker-compose.yml if needed (check release notes for env var changes).
  4. Restart: docker compose up -d
  5. Check logs: docker compose logs chathub -- verify migrations ran without errors.

Migrations run on every container start, so schema changes are applied automatically.

Version Pinning (Recommended)

Pin to a specific version for production:

image: docker.io/lqdflying/chathub:v1.0.0

Then upgrade by changing the tag and restarting. This avoids surprises from :latest.


Migration Troubleshooting

Migration failed / partial state

If migrations fail mid-way:

  1. Check docker compose logs chathub for the Drizzle error.
  2. The ensure* helpers run after migrations and will create any missing tables/columns regardless of journal state.
  3. Restart the container -- the safety-net queries are idempotent.

openai_codex_oauth_tokens_user_id_unique already exists (42P07)

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.

Container won't start

Check:

  • DATABASE_URL is 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.

Docker build failures (dev)

If you encounter "No space left on device" during Docker builds, free runner disk before building:

docker system prune -af

Healthcheck

healthcheck:
  test: ['CMD-SHELL', 'pg_isready -U user -d postgres']
  interval: 5s
  timeout: 5s
  retries: 5

GitHub Actions (CI)

Docker images are built by .github/workflows/docker-release.yml on tag pushes:

  • v*.*.* → pushes lqdflying/chathub:X.Y.Z and :latest (GA only)
  • v*.*.*-canary.* → pushes lqdflying/chathub:X.Y.Z-canary.N (does not touch :latest)

The workflow frees unused runner disk before building to avoid space issues.

Clone this wiki locally