How to share a Plotly Dash app

Dash sits in an awkward spot. It looks like a dashboard you could export — the layout is declarative, the charts are Plotly, and Plotly figures serialize to standalone HTML happily. But the interactivity is not in the figures. It is in the callbacks.

Why there is no static Dash export

Every @callback runs in the Flask process. When a reader changes the dropdown, the browser POSTs to /_dash-update-component and waits for Python to return new component props. Remove the server and you get a page that renders its initial state and then ignores every control — which is a worse artifact than an honest static report, because it looks alive and is not.

So there are two real paths, and they serve different readers.

Path one: deploy it

Any host that runs a Python web process works — Cloud Run, Fly, Render, an instance behind gunicorn, Dash Enterprise if you want the managed version with auth and deployment tooling attached. Put authentication in front of it, because a dashboard wired to your warehouse is a query surface.

Do this when readers genuinely drive the controls. If two people a week change the date filter and everyone else looks at the default view, you are operating a service for an audience of two.

Path two: publish the figures

Most Dash apps are, in practice, a fixed set of charts with a few filters nobody touches. That version renders to a single file, and the charts stay interactive, because Plotly's zoom, hover, and legend behaviour is client-side JavaScript — only the Python round-trip disappears.

import plotly.io as pio
from app import build_figures          # reuse the app's own figure builders

figs = build_figures(load_data())
body = "".join(
    pio.to_html(f, full_html=False, include_plotlyjs="inline" if i == 0 else False)
    for i, f in enumerate(figs)
)

html = f"""<!doctype html><meta charset="utf-8">
<style>body{{font:15px/1.6 system-ui;margin:2rem auto;max-width:70rem}}
h1{{font-size:1.5rem}}</style>
<h1>Operations dashboard — {date.today():%d %b %Y}</h1>{body}"""

open("dashboard.html", "w").write(html)

Two details carry the whole thing. include_plotlyjs="inline" on the first figure embeds the library so the page fetches nothing at view time — a linked CDN is the single most common reason a shared chart renders blank on someone else's network. And False on the rest avoids shipping the library four times, which is the difference between a 4 MB file and a 20 MB one.

Then publish it, and keep publishing to one id:

curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html dashboard.html \
        --arg title "Operations — $(date +%F)" '{title: $title, html: $html}')"

Comma runs report HTML verbatim with scripts enabled, so the Plotly charts behave exactly as they did in the app. A routine can run the refresh on a schedule if you would rather not own a cron.

What you get that the app never had

A URL with no uptime. Nothing to keep running, nothing to wake up, no cold start for the person who opens it at 11pm.

Access without accounts. Visibility is a setting on the report: private, team, domain-gated or link.

Anchored discussion. A reader highlights the throughput dip and pins "this is the maintenance window, expected." That thread persists across every revision, so the same question does not get asked next month. A live dashboard has nowhere to keep that — the state it shows is always now, and the reasoning about last Tuesday lives in someone's memory.

Which to build

If the filters are the product, deploy the app and put auth in front of it. If the charts are the product and the filters are decoration, publish the figures on a schedule and delete the deployment. Most teams discover they are in the second case once they look at who actually clicks anything.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history.

Create your first report →

Related