Business Intelligence
    Dashboards

    Deploy Apache Superset on a VPS

    Self-host Apache Superset BI dashboards on a RamNode VPS — PostgreSQL metadata, Redis + Celery, Gunicorn, and Nginx TLS, no Docker required.

    Apache Superset is an open source business intelligence platform that connects to your existing SQL databases and turns them into interactive dashboards. It handles ad hoc SQL exploration through SQL Lab, chart building through a no-code explorer, and dashboard sharing across teams. Unlike hosted BI tools, Superset keeps your query results and dashboard definitions on infrastructure you control.

    This guide walks through a production install on a RamNode VPS using Python virtual environments rather than Docker, with PostgreSQL as the metadata database, Redis for caching and the Celery queue, Gunicorn as the application server, and Nginx terminating TLS in front.

    What You Will Need

    • A RamNode VPS running Ubuntu 24.04 LTS with at least 4 GB RAM and 2 vCPU. Superset with Celery workers and a headless browser for reports will not fit comfortably in 2 GB. If you plan to run alert reports, choose 8 GB.
    • Root or sudo access.
    • A domain or subdomain with an A record pointing at your VPS IP address.
    • Basic familiarity with systemd and Nginx.

    Superset stores dashboard definitions, user accounts, and query history in its metadata database. It does not store your analytics data. Size your VPS around concurrent users and Celery workers, not around the size of the warehouse you are querying.

    Step 1: Prepare the System

    Update packages and install the build dependencies Superset needs to compile its Python wheels.

    shell
    apt update && apt upgrade -y
    apt install -y build-essential libssl-dev libffi-dev libsasl2-dev libldap2-dev \
      default-libmysqlclient-dev libpq-dev pkg-config git curl unzip

    Create a dedicated system user so Superset never runs as root.

    shell
    useradd --system --create-home --home-dir /opt/superset --shell /bin/bash superset

    Add swap if your plan has 4 GB or less. Superset's Python processes spike during chart rendering and package installs.

    shell
    fallocate -l 2G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    echo '/swapfile none swap sw 0 0' >> /etc/fstab

    Step 2: Install Python 3.11

    Ubuntu 24.04 ships Python 3.12. Superset 4.x and 5.x target Python 3.10 and 3.11, and several of its pinned dependencies do not build cleanly on 3.12. Install 3.11 from the deadsnakes PPA and leave the system Python alone.

    shell
    add-apt-repository -y ppa:deadsnakes/ppa
    apt update
    apt install -y python3.11 python3.11-dev python3.11-venv

    Confirm the version.

    shell
    python3.11 --version

    Step 3: Install and Configure PostgreSQL

    Superset defaults to SQLite for metadata, which cannot handle concurrent writes from the web workers and the Celery daemon. Use PostgreSQL.

    shell
    apt install -y postgresql postgresql-contrib
    systemctl enable --now postgresql

    Create the database and role.

    shell
    sudo -u postgres psql <<'SQL'
    CREATE USER superset WITH PASSWORD 'ReplaceWithAStrongPassword';
    CREATE DATABASE superset OWNER superset;
    GRANT ALL PRIVILEGES ON DATABASE superset TO superset;
    SQL

    Superset writes frequently to its metadata tables during query execution. Tune PostgreSQL for a small VPS by editing /etc/postgresql/16/main/postgresql.conf:

    shell
    shared_buffers = 512MB
    work_mem = 16MB
    maintenance_work_mem = 128MB
    effective_cache_size = 2GB
    max_connections = 100

    Restart PostgreSQL.

    shell
    systemctl restart postgresql

    Step 4: Install Redis

    Redis serves three roles in Superset: the Celery broker, the results backend for async queries, and the cache layer for chart data and filter state.

    shell
    apt install -y redis-server

    Edit /etc/redis/redis.conf and set these values:

    shell
    bind 127.0.0.1 ::1
    maxmemory 512mb
    maxmemory-policy allkeys-lru
    supervised systemd

    The allkeys-lru policy matters. Superset uses Redis for cache data that is safe to evict. Without an eviction policy, Redis will refuse writes once it hits the memory ceiling and Superset will throw errors on every cached chart load.

    shell
    systemctl restart redis-server
    systemctl enable redis-server

    Step 5: Create the Virtual Environment and Install Superset

    Switch to the superset user and build the environment.

    shell
    sudo -u superset -i
    python3.11 -m venv /opt/superset/venv
    source /opt/superset/venv/bin/activate
    pip install --upgrade pip setuptools wheel

    Install Superset along with the drivers and extras you need.

    shell
    pip install apache-superset
    pip install psycopg2-binary redis celery gevent Pillow

    Add database drivers for whatever you plan to query. Common additions:

    shell
    pip install pymysql            # MySQL and MariaDB
    pip install clickhouse-connect # ClickHouse
    pip install trino              # Trino and Starburst
    pip install snowflake-sqlalchemy

    Install each driver into this same virtual environment. Superset discovers database engines at import time, so a driver installed system wide will not be visible.

    Step 6: Write the Superset Configuration

    Superset reads its configuration from a Python file named superset_config.py found on the PYTHONPATH. Create a dedicated config directory.

    shell
    mkdir -p /opt/superset/config /opt/superset/data /opt/superset/logs

    Generate a secret key. Superset refuses to start with the default key, and rotating it later invalidates every saved database connection password.

    shell
    openssl rand -base64 42

    Create /opt/superset/config/superset_config.py:

    shell
    import os
    from celery.schedules import crontab
    from cachelib.redis import RedisCache
    
    SECRET_KEY = "PASTE_YOUR_GENERATED_KEY_HERE"
    
    SQLALCHEMY_DATABASE_URI = (
        "postgresql+psycopg2://superset:ReplaceWithAStrongPassword@localhost:5432/superset"
    )
    
    REDIS_HOST = "localhost"
    REDIS_PORT = 6379
    
    # Chart and dashboard metadata cache
    CACHE_CONFIG = {
        "CACHE_TYPE": "RedisCache",
        "CACHE_DEFAULT_TIMEOUT": 86400,
        "CACHE_KEY_PREFIX": "superset_",
        "CACHE_REDIS_HOST": REDIS_HOST,
        "CACHE_REDIS_PORT": REDIS_PORT,
        "CACHE_REDIS_DB": 1,
    }
    
    # Query result cache
    DATA_CACHE_CONFIG = {
        "CACHE_TYPE": "RedisCache",
        "CACHE_DEFAULT_TIMEOUT": 3600,
        "CACHE_KEY_PREFIX": "superset_data_",
        "CACHE_REDIS_HOST": REDIS_HOST,
        "CACHE_REDIS_PORT": REDIS_PORT,
        "CACHE_REDIS_DB": 2,
    }
    
    FILTER_STATE_CACHE_CONFIG = {
        "CACHE_TYPE": "RedisCache",
        "CACHE_DEFAULT_TIMEOUT": 86400,
        "CACHE_KEY_PREFIX": "superset_filter_",
        "CACHE_REDIS_HOST": REDIS_HOST,
        "CACHE_REDIS_PORT": REDIS_PORT,
        "CACHE_REDIS_DB": 3,
    }
    
    EXPLORE_FORM_DATA_CACHE_CONFIG = {
        "CACHE_TYPE": "RedisCache",
        "CACHE_DEFAULT_TIMEOUT": 86400,
        "CACHE_KEY_PREFIX": "superset_explore_",
        "CACHE_REDIS_HOST": REDIS_HOST,
        "CACHE_REDIS_PORT": REDIS_PORT,
        "CACHE_REDIS_DB": 4,
    }
    
    # Async query results
    RESULTS_BACKEND = RedisCache(
        host=REDIS_HOST, port=REDIS_PORT, db=5, key_prefix="superset_results_"
    )
    
    
    class CeleryConfig:
        broker_url = f"redis://{REDIS_HOST}:{REDIS_PORT}/0"
        result_backend = f"redis://{REDIS_HOST}:{REDIS_PORT}/0"
        imports = ("superset.sql_lab", "superset.tasks.scheduler")
        worker_prefetch_multiplier = 1
        task_acks_late = True
        beat_schedule = {
            "reports.scheduler": {
                "task": "reports.scheduler",
                "schedule": crontab(minute="*", hour="*"),
            },
            "reports.prune_log": {
                "task": "reports.prune_log",
                "schedule": crontab(minute=10, hour=0),
            },
        }
    
    
    CELERY_CONFIG = CeleryConfig
    
    FEATURE_FLAGS = {
        "ALERT_REPORTS": True,
        "DASHBOARD_RBAC": True,
        "EMBEDDED_SUPERSET": False,
    }
    
    SQLLAB_CTAS_NO_LIMIT = True
    SUPERSET_WEBSERVER_TIMEOUT = 120
    ROW_LIMIT = 50000
    SQL_MAX_ROW = 100000
    
    # Behind Nginx with TLS
    ENABLE_PROXY_FIX = True
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"
    
    WEBDRIVER_BASEURL = "http://127.0.0.1:8088/"
    WEBDRIVER_BASEURL_USER_FRIENDLY = "https://superset.example.com/"

    Lock down the file. It holds your database password and secret key.

    shell
    chmod 600 /opt/superset/config/superset_config.py

    Step 7: Initialize the Database and Create an Admin

    Export the environment Superset expects, then run the migrations.

    shell
    export PYTHONPATH=/opt/superset/config:$PYTHONPATH
    export FLASK_APP=superset
    
    superset db upgrade
    superset fab create-admin
    superset init

    superset db upgrade builds the metadata schema. superset init creates the default roles, Admin, Alpha, Gamma, and Public, and assigns permissions. Run superset init again after every version upgrade, since new features add new permissions that existing roles will not have.

    Add these exports to /opt/superset/.bashrc so future shell sessions pick them up automatically.

    Step 8: Create the systemd Services

    Exit back to root. Create /etc/systemd/system/superset.service:

    shell
    [Unit]
    Description=Apache Superset
    After=network.target postgresql.service redis-server.service
    Requires=postgresql.service redis-server.service
    
    [Service]
    Type=simple
    User=superset
    Group=superset
    WorkingDirectory=/opt/superset
    Environment="PYTHONPATH=/opt/superset/config"
    Environment="FLASK_APP=superset"
    Environment="SUPERSET_CONFIG_PATH=/opt/superset/config/superset_config.py"
    ExecStart=/opt/superset/venv/bin/gunicorn \
      --workers 4 \
      --worker-class gthread \
      --threads 20 \
      --timeout 120 \
      --limit-request-line 0 \
      --limit-request-field_size 0 \
      --bind 127.0.0.1:8088 \
      "superset.app:create_app()"
    Restart=on-failure
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target

    Set worker count to roughly two times your vCPU count plus one. On a 2 vCPU plan, four workers with 20 threads each is a reasonable starting point. Each worker is a full Python process holding its own copy of Superset, so watch memory before increasing this.

    Create the Celery worker at /etc/systemd/system/superset-worker.service:

    shell
    [Unit]
    Description=Superset Celery Worker
    After=network.target redis-server.service postgresql.service
    Requires=redis-server.service
    
    [Service]
    Type=simple
    User=superset
    Group=superset
    WorkingDirectory=/opt/superset
    Environment="PYTHONPATH=/opt/superset/config"
    Environment="FLASK_APP=superset"
    ExecStart=/opt/superset/venv/bin/celery \
      --app=superset.tasks.celery_app:app worker \
      --pool=prefork \
      --concurrency=2 \
      -O fair \
      --loglevel=INFO
    Restart=on-failure
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target

    Create the Celery beat scheduler at /etc/systemd/system/superset-beat.service:

    shell
    [Unit]
    Description=Superset Celery Beat
    After=network.target redis-server.service
    Requires=redis-server.service
    
    [Service]
    Type=simple
    User=superset
    Group=superset
    WorkingDirectory=/opt/superset
    Environment="PYTHONPATH=/opt/superset/config"
    Environment="FLASK_APP=superset"
    ExecStart=/opt/superset/venv/bin/celery \
      --app=superset.tasks.celery_app:app beat \
      --pidfile=/opt/superset/data/celerybeat.pid \
      --schedule=/opt/superset/data/celerybeat-schedule \
      --loglevel=INFO
    Restart=on-failure
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target

    Run exactly one beat process. Two beat schedulers pointed at the same Redis broker will fire every scheduled report twice.

    Enable and start everything.

    shell
    chown -R superset:superset /opt/superset
    systemctl daemon-reload
    systemctl enable --now superset superset-worker superset-beat
    systemctl status superset --no-pager

    Step 9: Configure Nginx and TLS

    shell
    apt install -y nginx certbot python3-certbot-nginx

    Create /etc/nginx/sites-available/superset:

    shell
    upstream superset_app {
        server 127.0.0.1:8088 fail_timeout=0;
    }
    
    server {
        listen 80;
        server_name superset.example.com;
    
        client_max_body_size 100M;
    
        location / {
            proxy_pass http://superset_app;
            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-Proto $scheme;
            proxy_redirect off;
    
            proxy_connect_timeout 300;
            proxy_send_timeout 300;
            proxy_read_timeout 300;
        }
    }

    The long read timeout matters. SQL Lab queries against slow warehouses will otherwise hit a 504 from Nginx while the query is still running. If you routinely run queries longer than five minutes, enable async query execution through Celery instead of raising this further.

    Enable the site and request a certificate.

    shell
    ln -s /etc/nginx/sites-available/superset /etc/nginx/sites-enabled/
    rm -f /etc/nginx/sites-enabled/default
    nginx -t
    systemctl reload nginx
    certbot --nginx -d superset.example.com

    Step 10: Lock Down the Firewall

    Superset should never be reachable on port 8088 from outside the box.

    shell
    ufw allow OpenSSH
    ufw allow 'Nginx Full'
    ufw --force enable

    Confirm Gunicorn is bound to loopback only.

    shell
    ss -tlnp | grep 8088

    You should see 127.0.0.1:8088 and not 0.0.0.0:8088.

    Step 11: Enable Alert Reports

    Superset renders scheduled reports by driving a headless browser against its own UI. Install Chromium and the matching driver.

    shell
    apt install -y chromium-browser chromium-chromedriver

    On Ubuntu 24.04 the Chromium packages are snap wrappers, which do not work well from a systemd service. Install the Debian build instead:

    shell
    apt install -y wget
    wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
    apt install -y ./google-chrome-stable_current_amd64.deb

    Then install the matching chromedriver into the virtual environment:

    shell
    sudo -u superset /opt/superset/venv/bin/pip install chromedriver-binary-auto

    Add the webdriver block to superset_config.py:

    shell
    from selenium.webdriver.chrome.options import Options
    
    WEBDRIVER_TYPE = "chrome"
    WEBDRIVER_OPTION_ARGS = [
        "--headless=new",
        "--disable-gpu",
        "--disable-dev-shm-usage",
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--window-size=1600,1000",
    ]
    
    THUMBNAIL_SELENIUM_USER = "admin"
    ALERT_REPORTS_NOTIFICATION_DRY_RUN = False
    
    SMTP_HOST = "smtp.example.com"
    SMTP_PORT = 587
    SMTP_STARTTLS = True
    SMTP_SSL = False
    SMTP_USER = "reports@example.com"
    SMTP_PASSWORD = "your-smtp-password"
    SMTP_MAIL_FROM = "reports@example.com"

    Note that RamNode blocks outbound SMTP on standard ports for abuse reasons. Route report email through an external provider API relay on port 587 or 2525, or send reports to Slack using SLACK_API_TOKEN instead.

    Restart the worker and beat services after changing the config.

    shell
    systemctl restart superset superset-worker superset-beat

    Step 12: Connect Your First Database

    Log in at your domain with the admin account you created. Go to Data, then Database Connections, then plus Database.

    For a database on the same VPS, use localhost. For a remote warehouse, allowlist your RamNode VPS IP address on the warehouse side first. Superset connects outbound from the VPS, so the source IP your warehouse sees is the VPS public IP.

    Under the Advanced tab of the connection dialog, enable these where they apply:

    • Allow DML if users need to run INSERT or UPDATE from SQL Lab. Leave off for read only analytics.
    • Asynchronous query execution if you configured Celery. This hands long queries to the worker instead of blocking a Gunicorn thread.
    • Chart cache timeout to control how long chart results stay in Redis.

    Verifying the Install

    Check that all three services are running.

    shell
    systemctl is-active superset superset-worker superset-beat

    Confirm Celery sees the worker.

    shell
    sudo -u superset /opt/superset/venv/bin/celery \
      --app=superset.tasks.celery_app:app inspect ping

    Watch the logs while you load a dashboard.

    shell
    journalctl -u superset -f

    Troubleshooting

    Superset will not start and the log mentions the default SECRET_KEY. You did not set SECRET_KEY in superset_config.py, or PYTHONPATH is not pointing at the config directory. Confirm the service environment includes PYTHONPATH=/opt/superset/config.

    Charts load but never finish, spinner stays forever. Async queries are enabled on the database connection but the Celery worker is not running or cannot reach Redis. Check systemctl status superset-worker and confirm RESULTS_BACKEND is set.

    "Database connection failed" for a database that works from psql. Superset connects through SQLAlchemy inside the venv. Confirm the driver package is installed in /opt/superset/venv and not system wide.

    Reports generate blank images. Chrome cannot reach WEBDRIVER_BASEURL. It must be the internal address Gunicorn listens on, not the public HTTPS URL, because the headless browser runs on the same box and the certificate hostname will not resolve to loopback.

    502 from Nginx after upgrade. Run superset db upgrade and superset init after any version bump, then restart. Migrations that have not run will crash the app on boot.

    Memory climbs until OOM kill. Reduce Gunicorn workers or Celery concurrency. A single Superset worker holds roughly 400 to 600 MB resident once warm.

    Upgrading

    Stop the services, upgrade the package, run migrations, then restart.

    shell
    systemctl stop superset superset-worker superset-beat
    sudo -u superset /opt/superset/venv/bin/pip install --upgrade apache-superset
    sudo -u superset -i
    export PYTHONPATH=/opt/superset/config FLASK_APP=superset
    source /opt/superset/venv/bin/activate
    superset db upgrade
    superset init
    exit
    systemctl start superset superset-worker superset-beat

    Back up the metadata database before every upgrade. Superset migrations are not reliably reversible.

    shell
    sudo -u postgres pg_dump superset > /root/superset-$(date +%F).sql

    Hardening Notes

    • Change the default role assignment. New users land in the Gamma role by default. Review PUBLIC_ROLE_LIKE and leave it unset unless you intend anonymous dashboard access.
    • Set TALISMAN_ENABLED = True in production to get security headers and a content security policy.
    • Rotate the Postgres password out of the config file into an environment file loaded by systemd with EnvironmentFile= and chmod 600.
    • Enable DASHBOARD_RBAC so dashboard access is granted per dashboard rather than per underlying dataset.
    • Take regular metadata backups. Losing the Superset database means losing every chart, dashboard, and saved query.

    Where to Go Next

    Once Superset is running, the natural next steps are wiring it to a proper warehouse and putting a scheduler in front of your data pipelines. Superset visualizes tables, but something has to build those tables on a schedule. Pair this install with a dedicated orchestration VPS running Dagster or Prefect, and point Superset at the outputs.