# Apache Superset Docker Deployment

> Deploy Apache Superset with Docker for full-featured, self-hosted BI. No third-party required.

By Zsolt Bizderi · Published 2025-06-10
Canonical: https://ambientnode.uk/apache-superset-docker-deployment

[Superset](https://superset.apache.org/) is a powerful, open-source data visualization and business intelligence platform maintained by the Apache Foundation. It’s ideal for teams who want modern dashboards without handing data over to third parties. After [experimenting with tools like Metabase](/metabase/), I landed on Superset for its extensibility, better dashboard customisation, and alerting/reporting features.

I struggled finding a comprehensive guide for Docker-based deployments beyond the basic setup, so I'll walk through my deployment process for Superset, from setting up the host and containers, to branding and email alerts.

---

## Why Superset?

Before diving into deployment, a quick comparison.

| Feature | **Superset** | **Metabase** |
| --- | --- | --- |
| Data engine support | Wide (SQLAlchemy-compatible) | Limited but growing |
| Chart/dash flexibility | Advanced (custom CSS, D3, JS plugins) | Simple and opinionated |
| Alerts & reports | Built-in with scheduling (via Celery) | Available on paid plans |
| Embedding options | Fine-grained (auth, iframe, SSO) | Basic iframe embedding |
| Community | Apache-backed, active dev scene | Also solid, with a user-friendly vibe |

Metabase is simpler and easier to get started with, especially for business users. But if you're comfortable with SQL and want granular control, Superset offers a lot more power.

---

## Deploying Superset with Docker Compose

I’m running this on a remote Linux server via Docker. We’ll use a persistent folder for Superset configs and database volumes.

### Step 1: Prepare the Host

SSH into your server and run:

```
sudo mkdir -p /opt/superset/{home,db}
sudo chown -R 1000:1000 /opt/superset
```

This creates persistent storage for Superset's configuration and the PostgreSQL database.

---

### Step 2: Docker Compose Setup (Full Version with Commands)

The complete Compose stack includes Superset, Postgres, Redis, a Celery worker for async tasks, and a beat scheduler for scheduled reports.

```
version: "3.8"

services:
  superset:
    image: apache/superset:latest
    container_name: superset
    ports:
      - "8088:8088"
    environment:
      - SUPERSET_ENV=production
      - SUPERSET_LOAD_EXAMPLES=no
      - SUPERSET_SECRET_KEY=your-secret-key
      - SUPERSET_REDIS_HOST=redis
      - SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset
      - CELERY_BROKER_URL=redis://redis:6379/0
    volumes:
      - /opt/superset/home:/app/superset_home
      - /opt/superset/home/superset_config.py:/app/pythonpath/superset_config.py
    depends_on:
      - db
      - redis
    restart: always
    networks:
      - shared_network
    command: >
      /bin/bash -c "
        pip install --no-cache-dir --no-warn-script-location psycopg2-binary prophet gevent openpyxl pandas-gbq pymysql elasticsearch-dbapi snowflake-connector-python cryptography flask-mail &&
        export FLASK_APP=superset &&
        superset db upgrade &&
        if ! superset fab list-users | grep -q admin; then
          superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@example.com --password 'YourSecurePassword';
        fi &&
        superset init &&
        gunicorn --workers 4 --worker-class gthread --timeout 120 -b 0.0.0.0:8088 'superset.app:create_app()'
      "

  db:
    image: postgres:13
    container_name: superset_db
    environment:
      - POSTGRES_DB=superset
      - POSTGRES_USER=superset
      - POSTGRES_PASSWORD=superset
    ports:
      - "6125:5432"
    volumes:
      - /opt/superset/db:/var/lib/postgresql/data
    restart: always
    networks:
      - shared_network

  redis:
    image: redis:latest
    container_name: superset_redis
    restart: always
    networks:
      - shared_network

  worker:
    image: apache/superset:latest-dev
    container_name: superset_worker
    command: celery --app=superset.tasks.celery_app:app worker --pool=gevent --concurrency=4
    environment:
      - SUPERSET_ENV=production
      - SUPERSET_SECRET_KEY=your-secret-key
      - SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=db+postgresql://superset:superset@db:5432/superset
    volumes:
      - /opt/superset/home:/app/superset_home
      - /opt/superset/home/superset_config.py:/app/pythonpath/superset_config.py
    depends_on:
      - redis
      - db
    restart: always
    networks:
      - shared_network

  beat:
    image: apache/superset:latest-dev
    container_name: superset_beat
    command: >
      /bin/bash -c "mkdir -p /app/superset_home && chmod -R a+rwX /app/superset_home && celery \
      --app=superset.tasks.celery_app:app beat \
      --pidfile= \
      --schedule=/app/superset_home/celerybeat-schedule \
      --loglevel=info"
    environment:
      - SUPERSET_ENV=production
      - SUPERSET_SECRET_KEY=your-secret-key
      - SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=db+postgresql://superset:superset@db:5432/superset
    volumes:
      - /opt/superset/home:/app/superset_home
      - /opt/superset/home/superset_config.py:/app/pythonpath/superset_config.py
    depends_on:
      - redis
      - db
    restart: always
    networks:
      - shared_network

networks:
  shared_network:
    external: true
```

`docker-compose.yml

If running this in production, use secrets and do not embed the credentials in the compose file.

---

## Custom Branding

Tweak your `/opt/superset/home/superset_config.py` to rebrand the UI:

```
APP_NAME = "My BI Platform"  # Top-left title and browser tab title

# Where clicking the logo takes users (e.g., your dashboard homepage)
LOGO_TARGET_PATH = "https://yourdomain.com"

# Replace the Superset logo (top-left) with your own
APP_ICON = "https://yourdomain.com/static/logo.svg"

# Favicon (browser tab icon)
FAVICONS = [
    {
        "rel": "icon",
        "type": "image/png",
        "sizes": "32x32",
        "href": "https://yourdomain.com/static/favicon-32x32.png",
    },
    {
        "rel": "icon",
        "type": "image/png",
        "sizes": "16x16",
        "href": "https://yourdomain.com/static/favicon-16x16.png",
    },
]
```

---

## Enabling Email Reports

Add this to the same config, this will enable the report/alert feature on Superset:

```
EMAIL_NOTIFICATIONS = True
SMTP_HOST = "smtp.yourprovider.com"
SMTP_PORT = 587
SMTP_STARTTLS = True
SMTP_SSL = False
SMTP_USER = "alerts@example.com"
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
SMTP_MAIL_FROM = "alerts@example.com"

ALERT_REPORTS_SUPERSET_WEBDRIVER = {
    "auth_type": "AUTH_FORM",
    "auth_user": "admin",
    "auth_password": os.getenv("SUPERSET_ADMIN_PW", ""),
    "login_url": "https://yourdomain.com/login/",
}
```

---

## What’s Next?

* Hook up your databases
* Build dashboards and slice charts
* Embed dashboards internally or in apps
* Define roles and access rules

---

## Final Thoughts

Superset can be a bit heavy out the gate, but once it’s running, it becomes a versatile and professional-grade BI platform, all while keeping your data under your control.

---

## UPDATE 11/08/2025

Since writing the post above, I’ve upgraded my Superset stack to do two things:

1. Real-time async queries via WebSockets  
   Charts render the moment they’re ready (no browser polling).
2. Headless screenshots & email reports without a custom Dockerfile  
   The Celery worker uses the `latest-dev` image which bundles a headless browser/driver.

## What changed

* **WebSockets for GAQ (browser <-> WS <-> Redis):**  
  Previously, the browser polled `/api/v1/async_query/...` every X ms. With WS, the server pushes “query finished” events the instant Celery writes status to Redis. Lower latency, fewer HTTP requests, faster dashboards.
* **Split images: stable web, -dev worker:**  
  The main app stays on the small, stable image. Only the worker uses `*-dev`, which bundles headless Firefox + geckodriver. Reports/thumbnails work without baking a custom image; web container remains lean and safer.
* **Correct webdriver scoping:**
  + `WEBDRIVER_BASEURL` points to **`http://superset:8088/`** (Docker-internal) so Selenium can log in and carry cookies reliably.
  + `WEBDRIVER_BASEURL_USER_FRIENDLY` (and email links) use your external HTTPS URL.  
    Avoids cookie domain mismatches and "can’t login" screenshot failures.
* **Celery wired up:**  
  `imports=("superset.sql_lab", "superset.tasks",)` ensures all Superset tasks (reports, thumbnails, cache-warmup) are registered. Beat has the same GAQ secret, so it can start without exploding.
* **Performance headroom:**
  + Gunicorn on **gevent** with higher worker connections = better I/O concurrency.
  + Redis caches for results, form state, thumbnails.

---

### `docker-compose.yml`

```
version: "3.8"

services:
  superset:
    image: apache/superset:latest
    container_name: superset
    ports: ["8088:8088"]
    environment:
      - SUPERSET_ENV=production
      - SUPERSET_LOAD_EXAMPLES=no
      - SUPERSET_REDIS_HOST=redis
      - JWT_COOKIE_NAME=async-token
      - CACHE_CONFIG={"CACHE_TYPE":"RedisCache","CACHE_DEFAULT_TIMEOUT":3600,"CACHE_KEY_PREFIX":"superset_","CACHE_REDIS_HOST":"redis","CACHE_REDIS_PORT":6379}
      - DATA_CACHE_CONFIG={"CACHE_TYPE":"RedisCache","CACHE_DEFAULT_TIMEOUT":3600,"CACHE_KEY_PREFIX":"superset_","CACHE_REDIS_HOST":"redis","CACHE_REDIS_PORT":6379}
      - RATELIMIT_ENABLED=false
      - RATELIMIT_STORAGE_URL=redis://redis:6379
      - SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/1
      - GLOBAL_ASYNC_QUERIES_JWT_SECRET=${GLOBAL_ASYNC_QUERIES_JWT_SECRET}
      - ASYNC_QUERIES_JWT_ALGO=HS256
      - SUPERSET_SECRET_KEY=${SUPERSET_SECRET_KEY}
      # SMTP + webdriver creds used by “Test email” and thumbnails
      - SMTP_PASSWORD=${SMTP_PASSWORD}
      - SUPERSET_ADMIN_USER=${SUPERSET_ADMIN_USER}
      - SUPERSET_ADMIN_PW=${SUPERSET_ADMIN_PW}
      # Browsers connect via public WSS
      - GAQ_WS_URL=wss://superset.example.com/ws/
    volumes:
      - /opt/superset/home:/app/superset_home
      - /opt/superset/home/superset_config.py:/app/pythonpath/superset_config.py
      - /opt/superset/logo.png:/app/superset/static/assets/images/logo.png
      - /opt/superset/logo.png:/app/superset/static/assets/images/favicon.png
    restart: always
    depends_on: [db, redis]
    networks: [shared_network]
    deploy:
      resources:
        limits:
          cpus: "4.0"
          memory: 6G
        reservations:
          cpus: "2.0"
          memory: 2G
    command: >
      /bin/bash -c "
        pip install --no-cache-dir --no-warn-script-location psycopg2-binary pillow prophet gevent openpyxl pandas-gbq pymysql elasticsearch-dbapi snowflake-connector-python cryptography flask-mail &&
        export FLASK_APP=superset &&
        superset db upgrade &&
        if ! superset fab list-users | grep -q ${SUPERSET_ADMIN_USER}; then
          superset fab create-admin --username ${SUPERSET_ADMIN_USER} --firstname Superset --lastname Admin --email admin@example.com --password '${SUPERSET_ADMIN_PW}';
        fi &&
        superset init &&
        gunicorn --workers 4 --threads 6 --worker-class gthread --timeout 180 -b 0.0.0.0:8088 'superset.app:create_app()'
      "

  db:
    image: postgres:13
    container_name: superset_db
    environment:
      - POSTGRES_DB=superset
      - POSTGRES_USER=superset
      - POSTGRES_PASSWORD=superset
    ports: ["6125:5432"]
    volumes:
      - /opt/superset/db:/var/lib/postgresql/data
    restart: always
    networks: [shared_network]

  redis:
    image: redis:latest
    container_name: superset_redis
    restart: always
    networks: [shared_network]

  worker:
    image: apache/superset:latest-dev
    container_name: superset_worker
    working_dir: /app/superset_home
    command: >
      /bin/bash -c "
        pip install --no-cache-dir pillow &&
        celery --app=superset.tasks.celery_app:app worker --pool=gevent --concurrency=4
      "
    environment:
      - SUPERSET_ENV=production
      - SUPERSET_REDIS_HOST=redis
      - SUPERSET_SECRET_KEY=${SUPERSET_SECRET_KEY}
      - SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/1
      - GLOBAL_ASYNC_QUERIES_JWT_SECRET=${GLOBAL_ASYNC_QUERIES_JWT_SECRET}
      - ASYNC_QUERIES_JWT_ALGO=HS256
      - SMTP_PASSWORD=${SMTP_PASSWORD}
      - SUPERSET_ADMIN_USER=${SUPERSET_ADMIN_USER}
      - SUPERSET_ADMIN_PW=${SUPERSET_ADMIN_PW}
    volumes:
      - /opt/superset/home:/app/superset_home
      - /opt/superset/home/superset_config.py:/app/pythonpath/superset_config.py
    depends_on: [redis, db, superset]
    restart: always
    networks: [shared_network]
    deploy:
      resources:
        limits:
          cpus: "3.0"
          memory: 3G
        reservations:
          cpus: "1.0"
          memory: 1G

  beat:
    image: apache/superset:latest
    container_name: superset_beat
    command: >
      /bin/bash -c "mkdir -p /app/superset_home && chmod -R a+rwX /app/superset_home && celery
      --app=superset.tasks.celery_app:app beat
      --pidfile=
      --schedule=/app/superset_home/celerybeat-schedule
      --loglevel=info"
    environment:
      - SUPERSET_ENV=production
      - SUPERSET_REDIS_HOST=redis
      - SUPERSET_SECRET_KEY=${SUPERSET_SECRET_KEY}
      - SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/1
      - GLOBAL_ASYNC_QUERIES_JWT_SECRET=${GLOBAL_ASYNC_QUERIES_JWT_SECRET}
      - ASYNC_QUERIES_JWT_ALGO=HS256
      - SMTP_PASSWORD=${SMTP_PASSWORD}
      - GAQ_WS_URL=wss://superset.example.com/ws/
    volumes:
      - /opt/superset/home:/app/superset_home
      - /opt/superset/home/superset_config.py:/app/pythonpath/superset_config.py
    depends_on: [redis, db]
    restart: always
    networks: [shared_network]

  websocket:
    image: apache/superset:latest-websocket
    container_name: superset_websocket
    environment:
      - JWT_SECRET=${GLOBAL_ASYNC_QUERIES_JWT_SECRET}
      - JWT_COOKIE_NAME=async-token
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_DB=0
    depends_on: [redis]
    restart: always
    networks: [shared_network]

networks:
  shared_network:
    external: true
```

> Put secrets in a **`.env`** next to the compose file:

---

### `superset_config.py`

```
import os
from celery.schedules import crontab
from cachelib.redis import RedisCache

# ───────────────────────── Celery ─────────────────────────
class CeleryConfig:
    broker_url = "redis://redis:6379/0"
    result_backend = "redis://redis:6379/1"
    imports = ("superset.sql_lab", "superset.tasks")
    worker_prefetch_multiplier = 1
    task_acks_late = True
    task_soft_time_limit = 300
    task_time_limit = 360
    beat_schedule = {
        "reports.scheduler": {
            "task": "reports.scheduler",
            "schedule": crontab(minute="*", hour="*"),
        },
        "reports.prune_log": {
            "task": "reports.prune_log",
            "schedule": crontab(minute=0, hour=0),
        },
    }

CELERY_CONFIG = CeleryConfig

# ───────────────────────── Branding ───────────────────────
APP_NAME = "Superset BI"
APP_ICON = "/static/assets/images/logo.png"
APP_ICON_WIDTH = 200
LOGO_TARGET_PATH = "https://superset.example.com"
FAVICON = "/static/assets/images/logo.png"

# ───────────────────────── Core security ──────────────────
SECRET_KEY = os.getenv("SUPERSET_SECRET_KEY")
WTF_CSRF_ENABLED = True
PREFERRED_URL_SCHEME = "https"

SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"

# ───────────────────────── Reverse proxy ──────────────────
ENABLE_PROXY_FIX = True
PROXY_FIX_CONFIG = {"x_for": 2, "x_proto": 2, "x_host": 2, "x_port": 2, "x_prefix": 1}

# ───────────────────────── Metadata DB ────────────────────
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://superset:superset@db:5432/superset"
SQLALCHEMY_ENGINE_OPTIONS = {
    "pool_size": 25,
    "max_overflow": 50,
    "pool_pre_ping": True,
    "pool_recycle": 300,
}

# ───────────────────────── Caching ────────────────────────
RESULTS_BACKEND = RedisCache(host="redis", port=6379, key_prefix="superset_results_")
CACHE_CONFIG = {
    "CACHE_TYPE": "RedisCache",
    "CACHE_DEFAULT_TIMEOUT": 3600,
    "CACHE_KEY_PREFIX": "superset_",
    "CACHE_REDIS_HOST": "redis",
    "CACHE_REDIS_PORT": 6379,
}
DATA_CACHE_CONFIG = dict(CACHE_CONFIG)

FILTER_STATE_CACHE_CONFIG = {
    "CACHE_TYPE": "RedisCache",
    "CACHE_DEFAULT_TIMEOUT": 86400,
    "CACHE_KEY_PREFIX": "filter_state_",
    "CACHE_REDIS_HOST": "redis",
    "CACHE_REDIS_PORT": 6379,
}

EXPLORE_FORM_DATA_CACHE_CONFIG = {
    "CACHE_TYPE": "RedisCache",
    "CACHE_KEY_PREFIX": "explore_",
    "CACHE_REDIS_HOST": "redis",
    "CACHE_REDIS_PORT": 6379,
    "CACHE_DEFAULT_TIMEOUT": 300,
}
THUMBNAIL_CACHE_CONFIG = {
    "CACHE_TYPE": "RedisCache",
    "CACHE_KEY_PREFIX": "thumb_",
    "CACHE_REDIS_HOST": "redis",
    "CACHE_REDIS_PORT": 6379,
    "CACHE_DEFAULT_TIMEOUT": 3600,
}

# ───────────────────────── Rate limiting ──────────────────
RATELIMIT_ENABLED = False
RATELIMIT_STORAGE_URI = "redis://redis:6379/3"

# ───────────────────────── Timeouts / limits ──────────────
SUPERSET_WEBSERVER_TIMEOUT = 180
ENABLE_TIME_ROTATE = True
ROW_LIMIT = 10000

SQLLAB_TIMEOUT = 600
SQLLAB_ASYNC_TIME_LIMIT_SEC = 600
SQLLAB_CTAS_NO_LIMIT = True

# ───────────────────────── Email / Alerts & Reports ───────
EMAIL_NOTIFICATIONS = True
SMTP_HOST = "smtp.office365.com"
SMTP_PORT = 587
SMTP_STARTTLS = True
SMTP_SSL = False
SMTP_USER = "reports@example.com"
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
SMTP_MAIL_FROM = "reports@example.com"

WEBDRIVER_BASEURL = "https://superset.example.com/"
WEBDRIVER_BASEURL_USER_FRIENDLY = "https://superset.example.com/"

EMAIL_REPORTS_CTA = "Explore in Superset BI"
EMAIL_REPORTS_SUBJECT_PREFIX = "[BI REPORT] "
ALERT_REPORTS_NOTIFICATION_DRY_RUN = False

ALERT_REPORTS_SUPERSET_WEBDRIVER = {
    "auth_type": "AUTH_FORM",
    "auth_user": os.getenv("SUPERSET_ADMIN_USER", "admin"),
    "auth_password": os.getenv("SUPERSET_ADMIN_PW", ""),
    "login_url": "https://superset.example.com/login/",
}

# ───────────────────────── Feature flags ───────────────────
FEATURE_FLAGS = {
    "ALERT_REPORTS": True,
    "EMBEDDED_SUPERSET": True,
    "PLAYWRIGHT_REPORTS_AND_THUMBNAILS": False,
    "ALLOW_FULL_CSV_EXPORT": True,
    "DASHBOARD_VIRTUALIZATION": False,
    "DASHBOARD_LAZY_RENDERING": False,
    "DASHBOARD_NATIVE_FILTERS": True,
    "DASHBOARD_NATIVE_FILTERS_SET": True,
    "SHARE_QUERIES_VIA_KV_STORE": True,
    "EMBEDDABLE_CHARTS": True,
    "GLOBAL_ASYNC_QUERIES": True,
}

SCREENSHOT_SELENIUM_DRIVER = "firefox"
WEBDRIVER_TYPE = "firefox"
WEBDRIVER_OPTION_ARGS = ["--headless", "--disable-dev-shm-usage"]
WEBDRIVER_CONFIGURATION = {"service_log_path": "/app/superset_home/geckodriver.log"}

SCREENSHOT_LOAD_WAIT = 120
SCREENSHOT_SELENIUM_ANIMATION_WAIT = 30
SCREENSHOT_SELENIUM_HEADSTART = 10
SCREENSHOT_LOCATE_WAIT = 120
EMAIL_PAGE_RENDER_WAIT = 45
SCREENSHOT_SELENIUM_WAIT = 45

WEBDRIVER_WINDOW = {"dashboard": (1300, 2000), "slice": (1300, 1200), "pixel_density": 1}

# ───────────────────────── GAQ (Async Queries) ────────────
GLOBAL_ASYNC_QUERIES_JWT_SECRET = os.getenv("GLOBAL_ASYNC_QUERIES_JWT_SECRET")
ASYNC_QUERIES_JWT_ALGO = os.getenv("ASYNC_QUERIES_JWT_ALGO", "HS256")
GLOBAL_ASYNC_QUERIES_CACHE_BACKEND = {
    "CACHE_TYPE": "RedisCache",
    "CACHE_KEY_PREFIX": "gaq_",
    "CACHE_REDIS_HOST": "redis",
    "CACHE_REDIS_PORT": 6379,
    "CACHE_DEFAULT_TIMEOUT": 0,
}
GLOBAL_ASYNC_QUERIES_REDIS_CONFIG = {"host": "redis", "port": 6379, "db": 0, "password": None, "ssl": False}
GLOBAL_ASYNC_QUERIES_TRANSPORT = "ws"
GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = "wss://superset.example.com/ws/"
GLOBAL_ASYNC_QUERIES_POLLING_DELAY = 300
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME = "async-token"
GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = True

# ───────────────────────── Embedding / CSP / CORS ─────────
GUEST_ROLE_NAME = "Gamma"
GUEST_TOKEN_JWT_SECRET = os.getenv("GUEST_TOKEN_JWT_SECRET")
GUEST_TOKEN_JWT_ALGO = "HS256"
EMBEDDED_SUPERSET_CONFIG = {"guest_token_jwt_audience": "superset", "allowed_domains": ["https://superset.example.com"]}

TALISMAN_ENABLED = True
TALISMAN_CONFIG = {
    "force_https": False,
    "content_security_policy": {"frame-ancestors": ["'self'", "https://superset.example.com"]},
}

ENABLE_CORS = False
CORS_OPTIONS = {"supports_credentials": True, "resources": [r"/api/.*"], "origins": ["https://superset.example.com"]}

MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY")
LOG_LEVEL = "INFO"
```

---

# Reverse proxy (NPM)

Keep your main proxy host pointing to `http://superset:8088`.  
Add a Custom Location:

* Location: `/ws/`
* Forward to: `http://websocket:8080`
* Enable Websockets Support

Advanced tab:

```
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
```

Make sure NPM is on the same Docker network as this stack.

---

# How to confirm it works

* **WebSocket:** open DevTools > Network > filter **WS** > you should see `wss://bi.yourdomain.tld/ws/` with 101 Switching Protocols and messages flowing.
* **Reports:** create a test Report > "Run now" > you'll see activity in `superset_worker` logs; you should receive an email.
* **Speed:** charts appear as soon as each query finishes (no synchronized pop after a poll tick).
