Skip to main content
This is the checklist for running a production-grade, self-hosted Lightdash deployment with the official Helm chart (lightdash/helm-charts). Audience: platform/infra engineers deploying Lightdash on Kubernetes.

Tier 1 — Evaluation / PoC

Follow the self-hosting guide (or docker compose for a local spin-up) for a minimum production setup suitable for evaluating Lightdash. Prerequisites Checklist

Tier 2 — Scalable production deployment

None of these steps are required but are recommended for running Lightdash beyond a PoC.

Operations

Workers and scaling

Infrastructure dependencies

Security and authentication

Tier 3 — Optional features and observability

Enterprise features

Integrations (enable what you use)

Observability

Architecture: what you’re deploying

ComponentChart valueWhat it does
Backend(always on)API + UI. Scale horizontally, 2+ replicas
Scheduler workerscheduler.enabledScheduled deliveries, Slack/email sends, exports, syncs
NATSnats.enabledJetStream message bus for async query execution
Warehouse NATS workerwarehouseNatsWorker.enabledExecutes warehouse queries + streams results to S3
Pre-aggregate NATS workerpreAggregateNatsWorker.enabledBuilds pre-aggregated materializations (Enterprise)
Headless browserbrowserless-chrome.enabledChromium pool for screenshots/PDFs
Migration jobmigrationJob.enabledPre-upgrade Helm hook that runs database migrations exactly once
PostgreSQLexternal (postgresql.enabled: false)Application state. Don’t use the bundled subchart, even for a PoC
S3 bucket(s)externalQuery results, downloads, pre-agg materializations, data apps
The chart wires up environment variables for you in three buckets:
  • configMap.* — non-sensitive env vars, applied to backend and all workers
  • secrets.* — sensitive env vars, rendered into a Kubernetes Secret (or bring your own via existingSecret)
  • extraEnv / schedulerExtraEnv — raw env entries, including valueFrom.secretKeyRef
The full list of supported environment variables lives in the environment variables reference.

Core configuration

image:
  repository: lightdash/lightdash
  tag: "0.xxxx.x"        # ALWAYS pin. Upgrade deliberately (see Upgrades & operations)

configMap:
  SITE_URL: https://lightdash.yourcompany.com
  SECURE_COOKIES: "true"
  TRUST_PROXY: "true"          # you are behind a TLS-terminating LB/ingress
  LIGHTDASH_MODE: default
  LIGHTDASH_MAX_PAYLOAD: "40mb"   # default 5mb is too small for large dbt manifests

existingSecret: lightdash-secrets   # contains LIGHTDASH_SECRET, S3 keys, SMTP password, ...
Key points:
  • SITE_URL is required for invite emails, OAuth redirect URIs, Slack unfurls, and scheduled-delivery links. Changing it later means reconfiguring every OAuth integration.
  • LIGHTDASH_SECRET must be changed and stored securely. It signs session cookies and encrypts data at rest in Postgres (warehouse credentials, tokens). You must set this
    • SECURE_COOKIES + TRUST_PROXY are both "false" by default — production behind HTTPS needs both "true" (see Secure Lightdash with HTTPS).
  • Session length is controlled by COOKIES_MAX_AGE_HOURS (unset = session cookie). Set it to your security policy, e.g. "24".
  • If backend pods can’t reach SITE_URL from inside the cluster (hairpin/NAT issues), set INTERNAL_LIGHTDASH_HOST to the in-cluster service URL — the headless browser and internal calls use it.

Secrets management

secrets.* values live in plaintext in your Helm values. For enterprise deployments, prefer existingSecret populated by External Secrets Operator / a CSI driver from your secret manager, and keep Helm values (and git) free of credentials.

Headless browser

Enabled by default in the chart — keep it on, and tune it like Lightdash Cloud does:
browserless-chrome:
  enabled: true
  replicaCount: 1              # scale with scheduled-delivery volume
  image:
    repository: ghcr.io/browserless/chromium
    tag: v2.49.0               # pin it; this is what Lightdash Cloud runs
  resources:
    requests: { cpu: "2", memory: 4Gi }
    limits: { memory: 4Gi }    # the memory-pressure guard below needs a limit to exist
  env:
    CONNECTION_TIMEOUT: "300000"   # 5 min; the chart default 180s truncates big dashboards
    TIMEOUT: "120000"              # per-session; raising this is the most common screenshot fix
    HEALTH: "true"                 # reject new sessions under memory pressure (HTTP 429)…
    MAX_MEMORY_PERCENT: "85"       # …instead of OOM-killing mid-screenshot
The chart auto-wires HEADLESS_BROWSER_HOST / HEADLESS_BROWSER_PORT. Remember: the browser renders dashboards by calling SITE_URL — it must be able to resolve and reach that URL from inside the cluster (use INTERNAL_LIGHTDASH_HOST if it can’t). For timeout/retry troubleshooting and internal-HTTPS setups, see enable headless browser.

Upgrades and operations

Upgrade mechanics are in update Lightdash and the version policy in versioning. Operational best practice:
  • Pin image.tag and upgrade deliberately. There is no LTS tag. Lightdash versioning: patch = routine, minor = check release notes (may be backwards-incompatible), major = read the upgrade guide
  • Upgrade cadence: at least monthly. Lightdash ships continuously; falling many minor versions behind makes the eventual migration jump riskier
  • Upgrade path: bump image.taghelm upgrade → the migrationJob hook migrates the database → rolling restart. Roll back the image only if the release notes say the migrations are backwards-compatible; otherwise restore the database backup
  • Keep a UAT instance that mirrors production config, and rehearse upgrades there against a refreshed copy of the production database
  • DR runbook: you need three things to rebuild an instance from scratch — the Postgres backup, the LIGHTDASH_SECRET, and your Helm values. Test that you can

Scheduler worker

By default the backend runs scheduled jobs in-process. In production, run a dedicated scheduler worker so a heavy dashboard export can’t starve the API:
scheduler:
  enabled: true
  replicas: 1            # scale up if deliveries queue at peak times
  concurrency: 3         # jobs per replica (sets SCHEDULER_CONCURRENCY)
  resources:
    requests:
      cpu: 500m
      memory: 1425Mi
      ephemeral-storage: 1Gi
When scheduler.enabled: true the chart automatically sets SCHEDULER_ENABLED: "false" on the backend — don’t set it yourself. Tuning: SCHEDULER_JOB_TIMEOUT (default 600000 ms), SCHEDULER_SCREENSHOT_TIMEOUT, and scheduler.tasks.include / scheduler.tasks.exclude if you want to shard task types across worker groups. For async warehouse queries, see the NATS workers overview and warehouse workers — including the critical rule: never enable nats.enabled without warehouseNatsWorker.enabled.

Sizing and availability

Baseline per-component requests for a standard instance (see also recommended resources):
ComponentCPUMemoryEphemeralReplicas
Backend500m–11.5–4 Gi1–2 Gi2+
Scheduler worker500m1425 Mi1 Gi1
Warehouse NATS worker500m1.5 Gi9 Gi1
Pre-agg NATS worker650m4 Gi9 Gi1
Browserless24 Gi1 Gi1
NATS100m256 Mi–1 Gi1
NATS workers buffer large result sets on ephemeral disk before uploading to S3 — the 9Gi ephemeral-storage request is not a typo.
replicaCount: 2

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 60   # backend only; workers scale via replicas

podAntiAffinity:
  enabled: true        # spreads each component across nodes (hard) and zones (soft)

podDisruptionBudget:
  enabled: true
  minAvailable: 1

PostgreSQL

Setup is covered in Configure Lightdash to use an external database. This section is what to run on the database side, matching Lightdash Cloud:
  • Managed Postgres (Cloud SQL / RDS / Azure Database) — version 14+ recommended (12 is the documented minimum)
  • Extensions: uuid-ossp (required) and pgvector, required for Enterprise AI features (AI Analyst embeddings, verified answers)
  • HA: regional/multi-AZ primary. Lightdash Cloud runs regional HA primaries with a cross-region replica for disaster recovery
  • Backups: daily automated backups, point-in-time recovery enabled, ≥ 7 days of transaction logs, 7–31 retained backups
  • SSD storage with auto-resize
  • Sizing: 2 vCPU / 8 GB RAM is a solid single-org baseline; 4 vCPU / 16 GB+ for large orgs or heavy scheduler usage
  • Connection budget: every backend/worker pod opens its own pool. Set PGMAXCONNECTIONS: "50" and PGMINCONNECTIONS: "0" in configMap, then make sure Postgres max_connections covers pods × PGMAXCONNECTIONS with headroom
  • Query insights / slow-query logging enabled on the database side
  • TLS to the database:
ssl:
  enabled: true                          # injects PGSSLMODE=verify-full + NODE_EXTRA_CA_CERTS
  configMapName: lightdash-db-ca         # your CA cert
  certFileName: ca.pem
Migrations run automatically at startup. Once you run multiple backend replicas, enable the migration job so replicas don’t race on the migration lock — it runs migrations as a Helm pre-install,pre-upgrade hook and the backend then starts without migrating:
migrationJob:
  enabled: true
(If a deploy is ever interrupted mid-migration, the lock lives in the knex_migrations_lock table.)

Object storage

Setup (env vars, per-provider instructions, IAM roles) is covered in Configure Lightdash to use external object storage. Bucket strategy is where enterprise deployments differ from the basic setup — this is how Lightdash Cloud sets up storage:
  • Dedicated bucket per purpose, each with a scoped credential that can only touch its own bucket:
    • results/cache bucket — add a lifecycle rule deleting objects after 1 day (results are ephemeral; this keeps the bucket small and cheap)
    • pre-aggregations bucket (if using pre-aggs) — lifecycle delete after ~30 days
    • data-apps bucket (if using data apps) — persistent, no delete lifecycle
  • Block all public access on every bucket
  • Prefer IAM roles (IRSA on EKS, Workload Identity on GKE) over static keys — but note signed URLs are then capped at 7 days
  • If users share download links that must outlive the signed-URL TTL (S3_EXPIRATION_TIME, default 3 days), enable PERSISTENT_DOWNLOAD_URLS_ENABLED: "true"

Email deliverability

Env vars are in the SMTP reference. Best practice on top:
  • Use a transactional provider (SES, Postmark, SendGrid) — Lightdash Cloud sends through Postmark
  • Set up SPF/DKIM for the sender domain so scheduled deliveries don’t land in spam
  • Internal open relay without auth: set EMAIL_SMTP_USE_AUTH: "false"

Load balancer and networking

Ingress and TLS setup is covered in Secure Lightdash with HTTPS. Match what Lightdash Cloud configures at the load balancer:
  • Redirect HTTP → HTTPS, minimum TLS 1.2
  • Backend/LB timeout ≥ 300s — long-running exports and queries will be killed by the common 30–60s defaults
  • Health check on GET /api/v1/health
  • Request body size limit at the ingress ≥ your LIGHTDASH_MAX_PAYLOAD (dbt manifests for big projects are tens of MB)
  • Optional: IP allowlisting / WAF / rate limiting at the load balancer
  • On GKE specifically, the chart can create a BackendConfig for you (backendConfig.create: true, backendConfig.spec) — this is how timeouts, CDN and Cloud Armor policies are attached on Lightdash Cloud

Authentication policy

Per-provider SSO setup (Okta, Azure AD, Google, generic OIDC — env vars and redirect URIs) is covered in use SSO login for self-hosted Lightdash. Enterprise deployments should be SSO-only:
configMap:
  AUTH_DISABLE_PASSWORD_AUTHENTICATION: "true"
  AUTH_ENABLE_OIDC_LINKING: "true"            # let existing accounts link an SSO identity
  AUTH_ENABLE_OIDC_TO_EMAIL_LINKING: "true"   # required for SCIM+SSO and LD_SETUP admin login
Also consider:
  • PAT policy: PAT_ALLOWED_ORG_ROLES, PAT_MAX_EXPIRATION_TIME_IN_DAYS, or DISABLE_PAT
  • Keep ALLOW_MULTIPLE_ORGS: "false" (default) for a single-company instance

Security hardening

  • CSP enforcement: LIGHTDASH_CSP_REPORT_ONLY: "false" (default is report-only; enforce in production), plus LIGHTDASH_CSP_ALLOWED_DOMAINS for any extra origins you load from
  • CORS: leave disabled unless embedding; if embedding, LIGHTDASH_CORS_ENABLED: "true" with an explicit LIGHTDASH_CORS_ALLOWED_DOMAINS list — never *
  • Egress policy: Lightdash needs your warehouse, S3, SMTP, api.keygen.sh (license), your IdP, and any AI provider endpoints — everything else can be blocked
  • NetworkPolicies: the chart only ships one for NATS (keep nats.networkPolicy.enabled: true, the default); add your own default-deny + allow rules for backend ↔ postgres/browserless/S3 if your cluster uses them
  • podSecurityContext / securityContext: the chart sets none by default — add runAsNonRoot, drop capabilities per your Pod Security Standards baseline
  • Soft delete for content recovery: SOFT_DELETE_ENABLED: "true" (+ SOFT_DELETE_RETENTION_DAYS, default 30)

Enterprise features

License key setup and validation is covered in enterprise license keys — remember the key is validated against https://api.keygen.sh on every server start, so allowlist that domain in your egress policy.

Enterprise feature flags

configMap:
  # Caching
  RESULTS_CACHE_ENABLED: "true"       # warehouse results cache in S3
  AUTOCOMPLETE_CACHE_ENABLED: "true"  # filter autocomplete cache
  CACHE_STALE_TIME_SECONDS: "86400"

  # Governance
  SERVICE_ACCOUNT_ENABLED: "true"
  CUSTOM_ROLES_ENABLED: "true"

  # Embedding (if embedding dashboards in your product)
  EMBEDDING_ENABLED: "true"
  LIGHTDASH_IFRAME_EMBEDDING_DOMAINS: "https://app.yourcompany.com"

AI Analyst

configMap:
  AI_COPILOT_ENABLED: "true"
  AI_DEFAULT_PROVIDER: "anthropic"     # openai | azure | anthropic | openrouter | bedrock
  AI_EMBEDDING_ENABLED: "true"         # verified answers (requires pgvector in Postgres)
  MCP_ENABLED: "true"                  # MCP endpoint at /api/v1/mcp with built-in OAuth

secrets:
  ANTHROPIC_API_KEY: ...               # or OPENAI_API_KEY / AZURE_AI_* / BEDROCK_*
  • Azure OpenAI: AZURE_AI_API_KEY, AZURE_AI_ENDPOINT, AZURE_AI_API_VERSION, AZURE_AI_DEPLOYMENT_NAME; Bedrock: BEDROCK_REGION + credentials
  • Routing through an internal LLM gateway (LiteLLM etc.): AI_DEFAULT_PROVIDER: openai + OPENAI_BASE_URL
  • Guardrails: AI_COPILOT_MAX_QUERY_LIMIT (default 500), AI_COPILOT_ALLOWED_PROJECT_UUID to pilot on one project first
  • MCP details: Configure MCP for Lightdash

Data apps

Sandbox providers (E2B, AWS Lambda MicroVMs, Azure) and their security model are covered in sandboxes; model configuration in self-hosting data apps. Production checklist on top:
configMap:
  APPS_RUNTIME_ENABLED: "true"
  SANDBOX_PROVIDER: e2b
  APP_RUNTIME_PREVIEW_ORIGIN: https://lightdash-apps.yourcompany.com   # separate origin!

secrets:
  E2B_API_KEY: ...
  APPS_S3_BUCKET: yourco-lightdash-apps    # persistent bucket, no delete lifecycle
  APPS_S3_REGION: eu-west-1
Serve app previews from a separate domain (Lightdash Cloud uses *.lightdash.app next to *.lightdash.cloud) so untrusted app content never shares an origin with your Lightdash session cookies, and restrict that hostname to /api/apps/* paths at the ingress.

Observability

The metric catalogue, alerting guidance, and Prometheus server setup are covered in Prometheus metrics; log configuration in configure logging. Run what Lightdash Cloud runs on every instance:
configMap:
  # Structured logs for your log pipeline
  LIGHTDASH_LOG_LEVEL: info          # "audit" adds an audit trail of user actions
  LIGHTDASH_LOG_FORMAT: json
  LIGHTDASH_LOG_OUTPUTS: console

  # Prometheus metrics on :9090/metrics (all pods, backend + workers)
  LIGHTDASH_PROMETHEUS_ENABLED: "true"
  LIGHTDASH_PROMETHEUS_HTTP_METRICS_ENABLED: "true"
  • Scrape port 9090 on all pods labelled app.kubernetes.io/name=lightdash (the chart doesn’t ship a ServiceMonitor/PodMonitoring — create one; Lightdash Cloud does exactly this, 30s interval)
  • Metrics worth alerting on: HTTP p95/error rate (http_server_request_duration_seconds), queue depth / scheduler job failures, pg pool saturation, event-loop lag
  • If you run NATS, its Prometheus exporter is on port 7777 (nats.promExporter.enabled: true)