Creating a Panel App
Open My Panel Apps. The toolbar offers two ways to publish, one per runtime:
- New Server App — build and run an app from a git repository.
- Add WASM App — publish a static, in-browser app from a file.
Creating a Server App
Section titled “Creating a Server App”A server app is built from a git repository and served at https://<your-tenant-host>/serve/<slug>/. You point PlaidCloud at a branch of a repository; it builds the code into a container, runs panel serve on it, and rebuilds automatically whenever you push to that branch.
Prepare Your App Repository
Section titled “Prepare Your App Repository”Your repository needs two things: an entry point — a Python file that builds a Panel app and marks it .servable() — and, optionally, a requirements.txt at the repository root listing any extra packages.
my-panel-app/├── app.py # entry point — calls .servable()└── requirements.txt # optional; extra dependencies, at the repo rootHello World
Section titled “Hello World”Create app.py:
import panel as pn
pn.extension()
pn.pane.Markdown("# Hello, PlaidCloud 👋\n\nYour first server-side Panel app is running.").servable()That’s a complete app. The .servable() call is what PlaidCloud serves — anything you want rendered must be marked servable (directly, or by being inside a servable layout).
A Slightly Richer Example
Section titled “A Slightly Richer Example”Panel apps are interactive. This one adds a slider that updates a table live:
import pandas as pdimport panel as pn
pn.extension()
multiplier = pn.widgets.IntSlider(name="Multiplier", start=1, end=10, value=3)
@pn.depends(multiplier)def times_table(m): df = pd.DataFrame({"n": range(1, 6)}) df["result"] = df["n"] * m return pn.pane.DataFrame(df, index=False)
pn.Column( "# Times Table", multiplier, times_table,).servable()Add Dependencies
Section titled “Add Dependencies”This second example imports pandas, which isn’t part of the base image, so add a requirements.txt at the repository root:
pandasPlaidCloud installs these with pip when it builds the image.
What the platform provides — and expects. Keep these assumptions in mind when you write your app:
panelandbokehare pre-installed by the platform base image. Don’t list them inrequirements.txt, and avoid pinning an older Panel version — the live-update reconnect relies on a recent one.- The PlaidCloud client libraries (
plaidcloud-rpc,plaidcloud-utilities) are pre-installed. Importplaidcloud.rpcorplaidcloud.utilitiesdirectly — don’t list them inrequirements.txt.requirements.txtmust be at the repository root. A file in a subdirectory is ignored, even if your entry point lives in one.- The app runs as a non-root user on a read-only filesystem. Only
/tmpis writable, so write any temporary files there (most libraries already honor$TMPDIR).- Don’t hardcode the port, URL prefix, or host. PlaidCloud assigns them and injects them at deploy time — just call
.servable().
Set the Theme: Light, Dark, or a Toggle
Section titled “Set the Theme: Light, Dark, or a Toggle”Server apps render in PlaidCloud’s modern Fast design by default — a clean, Fluent-style look applied platform-wide. In the publish dialog, Design controls that platform layer:
- FAST — the PlaidCloud default. Use it for new apps and apps built with Fast templates.
- Default — Panel’s default styling. Use it when an existing app’s CSS or visuals break under FAST.
What you control in your app code is the theme: light, dark, or a switch the viewer can flip.
The theme is a property of the template you serve. The Fast templates — FastListTemplate (a single column) and FastGridTemplate (a responsive grid) — take two settings for it:
theme—"default"for light or"dark". Setting it pins the app to that theme, even if a viewer adds?theme=darkto the URL.theme_toggle—Trueshows a light/dark switch in the header;Falsehides it. It defaults toTrue. Hiding the switch doesn’t fix the theme on its own — pin it withtheme(above); settheme_toggle=Falseonce you have, so viewers don’t see a switch that can’t change anything.
Pick the pattern that fits your app.
Light only — best for dense reports and financial tables, which are usually hand-styled for a light background:
import panel as pn
pn.extension("tabulator")
pn.template.FastListTemplate( title="Sales Dashboard", theme="default", # light theme_toggle=False, # hide the switch main=[...],).servable()Dark only:
pn.template.FastListTemplate( title="Ops Monitor", theme="dark", theme_toggle=False, main=[...],).servable()Let the viewer choose — defaults to light, with a switch in the header. Omit theme so it follows the toggle:
pn.template.FastListTemplate( title="Explorer", theme_toggle=True, main=[...],).servable()If you allow dark, make your tables dark-aware. A
Tabulatorpinned to a light table theme (theme="bootstrap"), or custom CSS with hardcoded light colors likebackground: #fff, renders as an unreadable white block on a dark page. Use a table theme that follows the app —pn.widgets.Tabulator(..., theme="fast")— and avoid hardcoded light colors. If a report is heavily hand-styled for light, pin it to light instead (the first pattern above).
A plain app with no template — pn.Column(...).servable() — follows Panel’s default and isn’t affected by these settings; reach for a template when you want explicit control.
Your repository must be reachable through a git connection in PlaidCloud. If your code lives in PlaidCloud Git, create a managed PlaidCloud Git connection under Tools > Connections — it needs no server URL or credentials. For the whole path from an empty repository to a served app, see Deploy a Panel App From PlaidCloud Git.
Publish the App
Section titled “Publish the App”- Click New Server App to open the Publish Server Panel App dialog. Its settings are grouped into sections.
- Under Directory Settings:
- App Name — the display name shown in the list.
- URL Slug — both the app’s URL and its internal name, so it must be a valid DNS label: lowercase letters, digits, and hyphens, starting with a letter, 40 characters or fewer (for example,
sales-dashboard). The dialog checks this before you publish. - Memo — an optional description.
- Under Git Connection and Publish Watcher:
- Git Connection — the connection that holds your app’s repository.
- Branch — the branch to build from. Pushes to this branch rebuild and redeploy the app automatically.
- Entry Point — the
.pyfile Panel should serve (yourapp.py).
- Under Runtime:
-
CPU and Memory the app’s container should request.
Fewer resources (lower CPU / memory) let the app schedule and cold-start faster.
-
Design — choose FAST for PlaidCloud’s modern design, or Default if the app’s CSS expects Panel’s default styling.
-
Idle (minutes) — how long the app stays warm with no traffic before it scales back to zero. The default is 5 minutes.
-
Allow Public Access — tick to allow unauthenticated access; otherwise viewers must sign in to your tenant.
-
- Under Advanced (Embedded Serving) — optional. By default the app serves only from your PlaidCloud tenant. To embed it in another site, click Add Domain and enter each domain allowed to load it, one per row — a host only, no scheme or path (for example,
example.comorapp.example.com:8443). Leave the list empty unless you’re embedding the app elsewhere. - Click Publish.
After you publish, the app builds — its Status shows in the list and updates automatically. See Using a Panel App for what the statuses mean, how to open the app once it is ready, and how to trigger a rebuild without unpublishing.
The app’s URL works only once its build status is Ready, and the first request after it has been idle spins it up (~15 seconds).
Advanced: Match PlaidCloud Locally
Section titled “Advanced: Match PlaidCloud Locally”Technical users can build and test the app with the same inputs PlaidCloud uses. This is the closest local check before publishing or clicking Rebuild.
PlaidCloud builds from the selected git branch with this generated Dockerfile. Use the same content locally as Dockerfile.plaidcloud-panel:
FROM us-docker.pkg.dev/plaidcloud-build/panel-base/platform-panel-base:0.5.1USER rootCOPY . /appRUN if [ -f /app/requirements.txt ]; then pip install --no-cache-dir -r /app/requirements.txt; fiUSER 10001To match the server build:
- Check out the same branch you select in Branch.
- Run the build from the repository root, so
COPY . /appmatches PlaidCloud. - Put optional dependencies in
requirements.txtat the repository root. - Use the same entry-point path you select in Entry Point.
docker build \ -f Dockerfile.plaidcloud-panel \ -t local-panel-app:plaidcloud \ .The deployed image does not override the base image entry point. It runs panel serve from environment variables injected by PlaidCloud. Test the built image with the same runtime contract:
docker run --rm \ --cpus 0.5 \ --memory 512m \ -p 5006:5006 \ -e APP_PATH=/app/app.py \ -e PANEL_PREFIX=/serve/local \ -e PANEL_PORT=5006 \ -e PANEL_ORIGIN=localhost:5006 \ -e PANEL_KEEP_ALIVE_MS=25000 \ -e PLAID_PANEL_DESIGN=fast \ local-panel-app:plaidcloudOpen http://localhost:5006/serve/local/.
Match the publish dialog settings when you test:
- Entry Point maps to
APP_PATH=/app/<entry-point>. - CPU and Memory map to Docker’s
--cpusand--memorylimits. - Design: FAST maps to
PLAID_PANEL_DESIGN=fast. - Design: Default means omit
PLAID_PANEL_DESIGN. - Idle (minutes) maps to the deployed scale-down window. It is not enforced by local Docker, but the app still uses the same Panel websocket keepalive and session cleanup settings.
- Embedded Serving domains map to
PANEL_ORIGIN; use a space-separated list if you need to test more than one host.
If you do not need a container, you can still test the serve layer from a local Python environment. Install the same requirements.txt, set the platform environment variables, then run panel serve with the same flags the deployed image uses.
export APP_PATH=app.pyexport PANEL_PREFIX=/serve/localexport PANEL_PORT=5006export PANEL_ORIGIN=localhost:5006export PANEL_KEEP_ALIVE_MS=25000export PLAID_PANEL_DESIGN=fast
panel serve "$APP_PATH" \ --address 0.0.0.0 \ --port "$PANEL_PORT" \ --prefix "$PANEL_PREFIX" \ --allow-websocket-origin "$PANEL_ORIGIN" \ --num-procs 1 \ --keep-alive "$PANEL_KEEP_ALIVE_MS" \ --unused-session-lifetime 15000 \ --index "$(basename "$APP_PATH" .py)"Open http://localhost:5006/serve/local/.
Use PLAID_PANEL_DESIGN=fast to match the default deployed look. To test the publish dialog’s Default design option, leave PLAID_PANEL_DESIGN unset and run the same command. Keep PANEL_PREFIX, PANEL_ORIGIN, --keep-alive, and --unused-session-lifetime in place; those are part of the deployed runtime contract.
Creating a WASM App
Section titled “Creating a WASM App”A WASM app runs in the browser from a pre-built HTML file.
- Click Add WASM App to open the Publish Serverless Panel App dialog.
- Enter an App Name and Version.
- Choose the App HTML Location — the HTML file in a document account.
- Enter an App URL Slug and an optional Memo.
- Click Publish. A WASM app has no build step and is served immediately.