Back to Blog
Tutorial6 min read2026-07-11

How to Deploy Gradio Apps on PandaStack

Move a Gradio app off share=True and into production: server binding, queue tuning, model loading, and logging predictions to Postgres.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Gradio is how most ML demos get their first users: wrap a function in gr.Interface, call launch(share=True), send the link. That tunnel link is also where a lot of "production" Gradio apps quietly live, which is a problem — share links are temporary, route through a third-party tunnel, and die when your laptop sleeps. Moving to a real deployment takes about twenty minutes. Here's the whole thing, including the parts the quickstart skips: queue tuning, model loading, and persisting predictions to a database.

From share link to server

The core change is telling Gradio to bind like a server instead of a desktop toy:

# app.py
import os
import gradio as gr

def predict(text: str) -> str:
    # your model call here
    return text[::-1]

demo = gr.Interface(
    fn=predict,
    inputs=gr.Textbox(label="Input"),
    outputs=gr.Textbox(label="Output"),
    title="Demo",
)

if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0",
        server_port=int(os.environ.get("PORT", 7860)),
    )

server_name="0.0.0.0" is the critical line. Gradio defaults to binding localhost, which works on your machine and silently fails in a container — the build succeeds, the process runs, and the platform's router gets connection refused. Port 7860 is Gradio's default; the PORT fallback lets the platform override it without a code change. Both settings also have environment-variable forms (GRADIO_SERVER_NAME, GRADIO_SERVER_PORT) if you prefer to keep the code untouched.

Drop share=True entirely. Under the hood Gradio runs on FastAPI and uvicorn, so what you're deploying is a normal ASGI web service — it deserves a normal URL.

If your app shouldn't be public while you iterate, Gradio has built-in basic auth:

demo.launch(server_name="0.0.0.0", auth=(os.environ["DEMO_USER"], os.environ["DEMO_PASS"]))

Credentials from the environment, never hardcoded — you'll see why this is free in a minute.

The queue is your capacity plan

Recent Gradio versions route events through a queue by default, and the queue's settings are effectively your capacity plan. Two knobs matter:

demo.queue(max_size=32, default_concurrency_limit=2)

default_concurrency_limit is how many requests run your function simultaneously. For a CPU-bound model, set it to roughly your CPU count — more just means requests fighting over the same cores and everyone's latency going up. max_size caps how many requests can wait; beyond it, users get an immediate "queue full" instead of an infinite spinner. An honest rejection beats a 90-second wait every time.

This ties directly to the compute tier you pick. On a small container (say 0.25 CPU / 512 MB on a free tier), a concurrency limit of 1 and a short queue is realistic. If the model is heavier, memory-optimized and compute-optimized tiers exist up to 8 CPU / 16 GB — size the queue to the hardware, not to optimism.

Model loading: once, at startup

The classic Gradio performance bug is loading the model inside the prediction function. Every request pays the load cost. Load at module level instead, so it happens once when the process boots:

from transformers import pipeline

classifier = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")

def predict(text: str) -> dict:
    result = classifier(text)[0]
    return {result["label"]: result["score"]}

Where the weights live is a real decision. Baking them into the Docker image gives you fast, deterministic startups at the cost of image size. Downloading at boot (the Hugging Face default — set HF_HOME if you want to control the cache path) keeps images slim but makes every cold start pay the download. For anything beyond a small model, bake it in:

FROM python:3.12-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1 GRADIO_SERVER_NAME=0.0.0.0 GRADIO_SERVER_PORT=7860
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# bake weights into the image so cold starts don't download them
RUN python -c "from transformers import pipeline; pipeline('sentiment-analysis', model='distilbert-base-uncased-finetuned-sst-2-english')"
COPY . .
EXPOSE 7860
CMD ["python", "app.py"]

Pin gradio in requirements.txt. The project moves quickly and major versions have changed component APIs; gradio==4.* today is a deliberate choice, and upgrading should be too. (If you move to Gradio 5 and turn on its SSR mode, note the container also needs Node — the plain Python image above covers the standard mode.)

Persisting predictions to Postgres

Demos become products the moment you want to know what people typed in. Gradio's flagging can write to CSV, but a database is the version that survives redeploys. With a managed Postgres attached, log from the handler:

import os
import sqlalchemy as sa

url = os.environ["DATABASE_URL"]
if url.startswith("postgres://"):
    url = url.replace("postgres://", "postgresql://", 1)
engine = sa.create_engine(url, pool_size=3, pool_pre_ping=True)

def predict(text: str) -> dict:
    result = classifier(text)[0]
    with engine.begin() as conn:
        conn.execute(
            sa.text("INSERT INTO predictions (input, label, score) VALUES (:i, :l, :s)"),
            {"i": text, "l": result["label"], "s": float(result["score"])},
        )
    return {result["label"]: result["score"]}

Create the table once with a one-off CREATE TABLE before first deploy. A small pool is plenty here — the queue already limits concurrency, so three connections cover a concurrency limit of two with room for a dashboard query.

Deploying on PandaStack

  1. 1Provision the database (if you're logging predictions): a managed PostgreSQL from the dashboard.
  2. 2Connect the repo as a container app. PandaStack picks up the Dockerfile and builds it with rootless BuildKit in an ephemeral build pod. Watch the live build logs on the first deploy — the weight-baking step above is the slow layer, and it's cached on subsequent builds as long as requirements.txt doesn't change.
  3. 3Attach the database. DATABASE_URL lands in the container automatically; the snippet above reads it with no further configuration. Auth credentials for demo.launch(auth=...) go in as environment variables from the dashboard.
  4. 4Push to deploy. Every git push rebuilds and ships. Add a custom domain with automatic SSL when the demo outgrows its generated URL.

One free-tier behavior to plan around: idle apps scale to zero, and a Gradio cold start includes your model load. A distilbert-class model wakes up fast enough for a portfolio demo; a multi-gigabyte model on the free tier will make the first visitor wait. If the app needs to feel instant, run it on a paid tier where it stays warm on stable nodes — and where you're not on preemptible hardware that can recycle mid-inference.

The short version

Bind 0.0.0.0:7860, kill share=True, size the queue to your CPU, load the model once at startup, bake weights into the image, and log predictions to Postgres instead of a CSV that vanishes on redeploy. The [Gradio docs](https://www.gradio.app/guides) cover the component API well; the gap they leave is everything around the process, and that's the platform's job.

When you want the git-push-to-live-demo loop with the database already wired in, try it on [https://pandastack.io](https://pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also