Skip to content

Docker Compose

The whole compose file, nothing to assemble

One service, the published ghcr.io image, and the variables most deployments touch — active ones set, rarely-used ones sitting commented where you can find them. It is the file that lives in the repository, with a few comments corrected where they were wrong for this release; this page just saves you the clone. Studio reads more than the file lists; the full set is in .env.example.

Quick start

Four lines from an empty directory to a login screen. The third writes the two secrets the file refuses to start without.

bash
curl -O https://libredb.org/docker-compose.example.yml
mv docker-compose.example.yml docker-compose.yml
printf 'ADMIN_PASSWORD=change-me\nJWT_SECRET=%s\n' "$(openssl rand -base64 32)" > .env
docker compose up -d

Studio is now on http://localhost:3000. Sign in with admin@libredb.org and the password you just wrote, then point it at a database on the same network.

The full file

docker-compose.example.yml — 164 lines. Read the comments; they carry the parts that are easy to get wrong.

docker-compose.example.yml
download
# =============================================================================
# LibreDB Studio — Ready-to-use Docker Compose
# =============================================================================
# Pulls the published image (ghcr.io/libredb/libredb-studio:latest) — no source
# build required. Works on any Docker host and PaaS that consumes a plain
# docker-compose.yml (Dokploy, Coolify, Portainer, etc.).
#
# Quick start:
#   1. cp docker-compose.example.yml docker-compose.yml
#   2. cp .env.example .env        # then set at least JWT_SECRET / passwords
#   3. docker compose up -d
#   4. open http://localhost:3000
#
# The variables most deployments use are listed below; .env.example has the rest.
# Commonly-used ones are active; others are commented out — uncomment as needed.
# Secrets are read from the .env file via ${VAR} interpolation and are never
# hardcoded in this file.
# =============================================================================

services:
  libredb-studio:
    image: ghcr.io/libredb/libredb-studio:latest
    container_name: libredb-studio
    restart: unless-stopped
    ports:
      - "3000:3000"
    # Uncomment if you enable the bundled PostgreSQL service below:
    # depends_on:
    #   libredb-postgres:
    #     condition: service_healthy
    environment:
      # -----------------------------------------------------------------------
      # AUTHENTICATION — local provider. ADMIN_PASSWORD is required here (without
      # it docker compose refuses to start); USER_* is optional.
      # -----------------------------------------------------------------------
      ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@libredb.org}        # admin: full access + maintenance tools
      ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
      # Optional lower-privilege account (query execution only). Omit USER_PASSWORD
      # to run admin-only — no default user password is ever assumed.
      USER_EMAIL: ${USER_EMAIL:-user@libredb.org}
      USER_PASSWORD: ${USER_PASSWORD:-}
      # JWT signing secret — min 32 chars. Generate: openssl rand -base64 32
      JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env (min 32 chars)}

      # Auth provider: "local" (default, email/password) or "oidc" (SSO)
      NEXT_PUBLIC_AUTH_PROVIDER: ${NEXT_PUBLIC_AUTH_PROVIDER:-local}

      # -----------------------------------------------------------------------
      # OIDC SSO  (only when NEXT_PUBLIC_AUTH_PROVIDER=oidc)
      # Auth0 / Keycloak / Okta / Azure AD / Zitadel
      # -----------------------------------------------------------------------
      # OIDC_ISSUER: ${OIDC_ISSUER}                # must serve /.well-known/openid-configuration
      # OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
      # OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
      # OIDC_SCOPE: ${OIDC_SCOPE:-openid profile email}
      # OIDC_ROLE_CLAIM: ${OIDC_ROLE_CLAIM:-}      # e.g. realm_access.roles (Keycloak), groups (Okta)
      # OIDC_ADMIN_ROLES: ${OIDC_ADMIN_ROLES:-admin}

      # -----------------------------------------------------------------------
      # STORAGE — where connections/config are persisted (server-side)
      #   "local"    (default) browser localStorage, zero config
      #   "sqlite"   server file, single-node persistent (uncomment volume below)
      #   "postgres" multi-node persistent (uncomment the postgres service below)
      # -----------------------------------------------------------------------
      STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local}
      # STORAGE_SQLITE_PATH: ${STORAGE_SQLITE_PATH:-/app/data/libredb-storage.db}
      # STORAGE_POSTGRES_URL: ${STORAGE_POSTGRES_URL:-postgresql://postgres:postgres@libredb-postgres:5432/libredb_storage?sslmode=disable}

      # -----------------------------------------------------------------------
      # AI / LLM  (optional) — provider: gemini | openai | ollama | custom
      # -----------------------------------------------------------------------
      # LLM_PROVIDER: ${LLM_PROVIDER:-gemini}
      # LLM_API_KEY: ${LLM_API_KEY}                # required for gemini/openai
      # LLM_MODEL: ${LLM_MODEL:-gemini-2.5-flash}
      # LLM_API_URL: ${LLM_API_URL}                # required for custom, read by every provider, e.g. http://host:11434/v1

      # -----------------------------------------------------------------------
      # AGENT RUNTIME — standalone only, and there is no flag to turn it on.
      # It appears once a model is configured through the LLM_* settings above
      # (there is no second place for an API key) AND the ledger path below is
      # writable. Uncomment the line below to keep the AI configuration and have
      # no agent — including on an upgrade, where an LLM_API_KEY that used to
      # power NL2SQL now brings an agent with it.
      # -----------------------------------------------------------------------
      # LIBREDB_AGENT_ENABLED: "false"
      # Durable backend for run state. "local" (default) keeps it on disk behind
      # file locks — SINGLE INSTANCE ONLY. More than one replica needs
      # "@workflow/world-postgres" plus WORKFLOW_POSTGRES_URL.
      # WORKFLOW_TARGET_WORLD: ${WORKFLOW_TARGET_WORLD:-local}
      # Where the "local" backend writes, and the second half of the availability
      # answer above: if it cannot be created and written, the agent reports
      # itself absent. The image already defaults this to /app/data/workflow, so
      # uncomment it only to put the ledger elsewhere — and mount the data volume
      # below either way, or runs vanish when the container is recreated.
      # WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/data/workflow}

      # -----------------------------------------------------------------------
      # MCP SERVER: off unless LIBREDB_MCP_ENABLED says on. An AI client of your
      # own reads schemas and runs read-only SQL at /api/mcp with a token minted on
      # the settings screen. The URL is the address clients use, with your
      # BASE_PATH, and every token is bound to it; changing the label revokes
      # every token at once. See docs/MCP.md.
      # -----------------------------------------------------------------------
      # LIBREDB_MCP_ENABLED: "true"
      # LIBREDB_MCP_URL: ${LIBREDB_MCP_URL:-http://localhost:3000/api/mcp}
      # LIBREDB_MCP_TOKEN_LABEL: ${LIBREDB_MCP_TOKEN_LABEL}
      # LIBREDB_MCP_TOKEN_TTL_DAYS: ${LIBREDB_MCP_TOKEN_TTL_DAYS:-30}

      # -----------------------------------------------------------------------
      # PASSKEYS: off unless PASSKEY_ORIGIN is set; also needs STORAGE_PROVIDER
      # sqlite or postgres. Open Studio at exactly this address. See docs/PASSKEYS.md.
      # -----------------------------------------------------------------------
      # PASSKEY_ORIGIN: ${PASSKEY_ORIGIN:-http://localhost:3000}

      # -----------------------------------------------------------------------
      # SEED CONNECTIONS (optional) — pre-configure databases on boot.
      # Uncomment the seed volume mount below, then provide seed-connections.yaml.
      # Credentials referenced in that file via ${VAR} are read from this .env.
      # -----------------------------------------------------------------------
      # SEED_CONFIG_PATH: /app/config/seed-connections.yaml
      # SEED_CACHE_TTL_MS: ${SEED_CACHE_TTL_MS:-60000}

    # volumes:
    #   # SQLite storage persistence (STORAGE_PROVIDER=sqlite):
    #   - libredb-data:/app/data
    #   # Seed connections file (read-only):
    #   - ./seed-connections.yaml:/app/config/seed-connections.yaml:ro

    # Image runs as node:26.10.0-trixie-slim (no curl/wget) — use Node's built-in fetch.
    # BASE_PATH must match a custom image built with that prefix (docs/SUBPATH.md).
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://localhost:3000${BASE_PATH:-}/api/db/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

  # ---------------------------------------------------------------------------
  # Optional: PostgreSQL backend for STORAGE_PROVIDER=postgres
  # To enable: uncomment this service, the STORAGE_POSTGRES_URL env above,
  # the depends_on block above, and the pgdata volume below.
  # ---------------------------------------------------------------------------
  # libredb-postgres:
  #   image: postgres:18
  #   container_name: libredb-postgres
  #   restart: unless-stopped
  #   environment:
  #     POSTGRES_USER: postgres
  #     POSTGRES_PASSWORD: postgres
  #     POSTGRES_DB: libredb_storage
  #   volumes:
  #     # PostgreSQL 18+ images expect the mount at /var/lib/postgresql, not /var/lib/postgresql/data.
  #     # A pgdata volume from the old layout has to be removed once: run docker compose down, then
  #     # docker volume rm on the volume whose name ends in _pgdata (find it with docker volume ls).
  #     - pgdata:/var/lib/postgresql
  #   healthcheck:
  #     test: ["CMD-SHELL", "pg_isready -U postgres"]
  #     interval: 10s
  #     timeout: 5s
  #     retries: 5

# volumes:
#   libredb-data:   # SQLite storage persistence
#   pgdata:         # PostgreSQL storage persistence

Every variable the file sets or lists

Grouped as the file groups them. This is a subset of what Studio reads; .env.example documents the rest. Set means the line ships uncommented and is already in force; commented means it is there for you to uncomment. Secrets come from .env through ${VAR} interpolation and are never written into the compose file.

Authentication

The local email/password provider. This compose file refuses to start until ADMIN_PASSWORD and JWT_SECRET are set in .env. Studio on its own generates both on first start and prints the admin password to the log once; the file asks for them so the values are yours.

Variable Default In the file What it does
ADMIN_EMAIL admin@libredb.org set Admin login — full access plus the maintenance tools.
ADMIN_PASSWORD required set Admin password. Unset, docker compose up stops with an error before the container starts.
USER_EMAIL user@libredb.org set Optional lower-privilege account — query execution only, no maintenance.
USER_PASSWORD empty set Leave it unset to run admin-only. No default user password is ever assumed.
JWT_SECRET required set Signing secret, minimum 32 characters. Generate one with openssl rand -base64 32.
NEXT_PUBLIC_AUTH_PROVIDER local set local for email/password, oidc to hand sign-in to your identity provider.

OIDC single sign-on

Only read when NEXT_PUBLIC_AUTH_PROVIDER=oidc. Studio's OIDC guide has setup examples for Auth0, Keycloak, Okta, Microsoft Entra ID and Zitadel. The Keycloak steps are verified on Keycloak 26.4 and ship as a local demo stack; the others are configuration examples, and Okta's logout has not been tried against a live tenant.

Variable Default In the file What it does
OIDC_ISSUER — commented Issuer URL. It must serve /.well-known/openid-configuration.
OIDC_CLIENT_ID — commented Client ID from your identity provider.
OIDC_CLIENT_SECRET — commented Client secret for that client.
OIDC_SCOPE openid profile email commented Scopes requested at sign-in.
OIDC_ROLE_CLAIM empty commented The claim that carries roles — realm_access.roles on Keycloak, groups on Okta.
OIDC_ADMIN_ROLES admin commented Which of those roles get admin access.

Storage

Where saved connections and configuration live. The default keeps them in the browser. A first start also adds two editable sample connections, on by default; .env.example has the switches that turn them off.

Variable Default In the file What it does
STORAGE_PROVIDER local set local (browser storage, zero config), sqlite (one server file), or postgres (shared across replicas).
STORAGE_SQLITE_PATH /app/data/libredb-storage.db commented Where the SQLite file goes. Mount the data volume too, or it dies with the container.
STORAGE_POSTGRES_URL postgresql://postgres:postgres@libredb-postgres:5432/libredb_storage?sslmode=disable commented Connection string for the postgres provider. The file ships a matching optional postgres:18 service.

AI model

Optional. Configure a model here and natural-language querying appears; leave it out and Studio is a plain editor.

Variable Default In the file What it does
LLM_PROVIDER gemini commented gemini, openai, ollama or custom.
LLM_API_KEY — commented Required for gemini and openai. This is the only place Studio reads a model key from.
LLM_MODEL gemini-2.5-flash commented Model name.
LLM_API_URL — commented Required for custom, and read by every provider: an Ollama that is not on localhost:11434, e.g. http://host:11434/v1, or a proxy or regional endpoint for gemini.

Agent runtime

There is no switch that turns the agent on. It appears once a model is configured above and the ledger path is writable — which means an upgrade can hand an agent to a key you added for natural-language querying.

Variable Default In the file What it does
LIBREDB_AGENT_ENABLED unset commented Set to "false" to keep the AI configuration and have no agent.
WORKFLOW_TARGET_WORLD local commented Durable backend for run state. local is file-locked and single-instance; more than one replica needs @workflow/world-postgres.
WORKFLOW_LOCAL_DATA_DIR /app/data/workflow commented Where the local backend writes. If it cannot be created and written, the agent reports itself absent.

MCP server

Off unless switched on. An AI client of your own reads schemas and runs read-only SQL at /api/mcp, with a token minted on the settings screen.

Variable Default In the file What it does
LIBREDB_MCP_ENABLED unset commented true, on or 1 turns the MCP endpoint on; unset leaves it off.
LIBREDB_MCP_URL http://localhost:3000/api/mcp commented The address clients use, with your BASE_PATH. Every token is bound to it, so changing it invalidates them all.
LIBREDB_MCP_TOKEN_LABEL — commented Any non-empty value. Changing it revokes every MCP token at once.
LIBREDB_MCP_TOKEN_TTL_DAYS 30 commented How many days a minted token stays valid, from 1 to 365.

Passkeys

Off unless PASSKEY_ORIGIN is set. Needs local sign-in and STORAGE_PROVIDER sqlite or postgres, since the server store keeps the passkey records.

Variable Default In the file What it does
PASSKEY_ORIGIN http://localhost:3000 commented The exact address people open Studio at: scheme, host and port, no path. https is required except on localhost.

Seed connections

Optional. Ship the team a Studio that already knows the databases, instead of asking everyone to add them by hand.

Variable Default In the file What it does
SEED_CONFIG_PATH /app/config/seed-connections.yaml commented A mounted YAML file of connections to register on boot. Credentials inside it interpolate from the same .env.
SEED_CACHE_TTL_MS 60000 commented How long seeded connections are cached.