Docker Compose
The whole compose file, nothing to assemble
One service, the published ghcr.io image, and every environment variable Studio reads — active ones set,
rarely-used ones sitting commented where you can find them. It is the same file that lives in the repository; this
page just saves you the clone.
Quick start
Four lines from an empty directory to a login screen. The third writes the two secrets the file refuses to start without.
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 [email protected] and the password
you just wrote, then point it at a database on the same network.
The full file
docker-compose.example.yml — 142 lines. Read the comments; they carry the parts that are easy
to get wrong.
# =============================================================================
# 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
#
# Every supported environment variable is listed below. Commonly-used ones are
# active; less-frequently-used ones are shown 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 (without it
# the login screen shows a clear config error); USER_* is optional.
# -----------------------------------------------------------------------
ADMIN_EMAIL: ${ADMIN_EMAIL:[email protected]} # 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:[email protected]}
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} # ollama/custom only, 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}
# -----------------------------------------------------------------------
# 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:24.16.0-trixie-slim (no curl/wget) — use Node's built-in fetch.
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/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:
# - pgdata:/var/lib/postgresql/data
# 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 environment variable
Grouped as the file groups them. 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. Studio will not start a usable login without ADMIN_PASSWORD and JWT_SECRET.
| Variable | Default | In the file | What it does |
|---|---|---|---|
ADMIN_EMAIL | [email protected] | set | Admin login — full access plus the maintenance tools. |
ADMIN_PASSWORD | required | set | Admin password. Without it the login screen shows a configuration error instead of a form. |
USER_EMAIL | [email protected] | 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. Tested against Auth0, Keycloak, Okta, Microsoft Entra ID and Zitadel.
| 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, so a fresh container starts empty on purpose.
| 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 | — | 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 | Base URL for ollama and custom, e.g. http://host:11434/v1. |
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. |
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. |