Flask Preview Environment in 5 Minutes — a Live URL for Every Branch
Your Flask app boots in under a second. Your test suite runs in 20. And then your product manager asks for a link to the branch you pushed this morning, and you're still editing a Docker Compose override so two PRs don't fight over port 5000.
Flask's simplicity is the whole point — a app.py and a requirements.txt and you're serving. But the preview workflow around it usually isn't. Most Flask teams still share one staging server that one person deploys to at a time, or hand-roll a CI script that provisions a VM, wires up DNS, and hopes the cleanup cron didn't silently fail three weeks ago.
PreviewDrop gives every Flask branch its own live URL — automatic deploys, HTTPS by default, database isolation, and zero CI configuration. Here's how to set it up in five minutes.
Why Flask previews are different from frontend previews
Vercel and Netlify built "push a branch, get a URL" for JavaScript frontends. That workflow assumes your app compiles to static files or serverless functions. Flask compiles to nothing — it's a long-lived Python process that listens on a port and needs a container, not a static host.
The tools that do handle container-based previews — Railway, Render, Northflank — charge per resource or per second. A Flask service with a Postgres dependency can cost $40–80/month in preview infrastructure alone for a team of three engineers. The meter runs whether anyone is reviewing the branch or not.
PreviewDrop takes a different route: if your app has a Dockerfile (or if PreviewDrop detects one from your requirements.txt), you get a live URL on every branch. Flat pricing, no per-resource metering, automatic teardown when the PR merges.
Step-by-step: your first Flask preview
Step 1 — Add a Dockerfile. If you already have one, skip to step 2. Here's a minimal multi-stage Dockerfile for a Flask app served with gunicorn:
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
# PreviewDrop injects PORT; gunicorn binds to it
CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT:-8000} app:app"]
Two things matter here, and each is a real gotcha the first time you skip it.
Bind to 0.0.0.0, not 127.0.0.1
Flask's dev server defaults to 127.0.0.1, which means the container won't accept traffic from outside itself. gunicorn's --bind 0.0.0.0 fixes that. If you run Flask's built-in server instead, pass --host=0.0.0.0:
flask run --host=0.0.0.0 --port=${PORT:-8000}
Read the injected PORT, don't hard-code it
PreviewDrop routes traffic to whatever port your app listens on via the PORT env var. Hard-coding 5000 still works if it matches, but reading PORT is the safe default — it's what keeps the preview routable the moment the container starts.
Step 2 — Connect your repo. Install the PreviewDrop GitHub App on your Flask repo. It takes about 60 seconds. The app receives push and PR webhooks automatically — no workflow files to write.
Step 3 — Set environment variables. In the PreviewDrop dashboard, add the variables your app needs:
PORT=8000
DATABASE_URL=postgres://user:pass@host:5432/preview_db
SECRET_KEY=your-preview-secret
FLASK_ENV=preview
Use a dedicated development database. Previews are ephemeral — point DATABASE_URL at a shared dev Postgres instance, or use a database-branching service for per-branch isolation.
Step 4 — Push a branch. That's it. PreviewDrop detects Flask from your requirements.txt, builds the image, starts the container, and posts a live HTTPS URL to your PR:
git checkout -b feature/new-endpoint
git push origin feature/new-endpoint
# URL posted to your PR automatically:
# https://pr-127.previewdrop.dev
What PreviewDrop detects in your Flask project
When PreviewDrop scans your repo, it looks for:
requirements.txtorpyproject.toml— confirms it's a Python project, reads dependenciesapp.py,wsgi.py, orwsgi.py:app— identifies the entry pointDockerfile— if present, uses it directly; if not, generates a build from the dependenciesmigrations/or Alembic config — detects schema migrations to run at build time
The build installs your dependencies, compiles nothing (Python is interpreted), and starts the container. If you use Flask-Migrate or Alembic, you can run migrations as part of the deploy so the preview database always matches the branch's schema.
Database strategy for Flask previews
Flask apps typically connect to Postgres via SQLAlchemy or the lighter psycopg2 directly, or use SQLite for smaller services. The database question is the same as any backend stack: share a dev database, or isolate per branch?
Option 1 — Shared dev database. Point all previews at one Postgres instance. Simplest setup, fine for read-heavy testing. The trade-off: two branches running conflicting migrations will step on each other.
Option 2 — Per-branch isolation. Use a database-branching service that creates lightweight Postgres clones in under a second. Set the branch name as an environment variable, and each preview gets its own isolated database. Here's how your Flask app reads it:
# config.py
import os
class Config:
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///app.db")
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret")
# PreviewDrop injects PREVIEW_URL at deploy time
PREVIEW_URL = os.getenv("PREVIEW_URL", "http://localhost:8000")
Option 3 — SQLite for previews. For simpler Flask apps, SQLite avoids external database dependencies entirely and works well for services that don't need Postgres-specific features.
Add a health check endpoint
Preview platforms need a health check to verify the container is ready. Flask makes this trivial:
# app.py
from flask import Flask, jsonify
from sqlalchemy import text
app = Flask(__name__)
@app.route("/health")
def health():
try:
from app import db
db.session.execute(text("SELECT 1"))
return jsonify(status="ok", database="connected"), 200
except Exception:
return jsonify(status="unhealthy"), 503
PreviewDrop polls /health after container start. When it returns 200, the preview is marked ready and the URL is posted to the PR.
Flask-specific gotchas
The dev server vs. a real WSGI server. Flask's built-in server is single-threaded and meant for development. For a preview that multiple reviewers will click at once, use gunicorn (as in the Dockerfile above) or waitress. It's the difference between "works when I test it" and "works when the whole team clicks it."
Secret keys. Flask's session cookies are signed with SECRET_KEY. Never reuse your production key in a preview. Generate a throwaway one for previews — the Flask CLI does it in one line:
python -c "import secrets; print(secrets.token_hex(32))"
CORS for a separate frontend. If a React or Vue frontend calls your Flask API, set CORS_ORIGINS to the preview URL so the browser allows the cross-origin request. Flask-CORS reads it from the environment:
from flask_cors import CORS
CORS(app, origins=[os.getenv("PREVIEW_URL", "http://localhost:3000")])
What it costs
PreviewDrop is flat workspace pricing, not per-seat. The Free plan gives you two live previews at a time, one preview per project, and up to three projects, with a four-hour preview lifetime. Starter is $19/mo for five concurrent previews, three previews per project, up to ten projects, and a 24-hour lifetime. Pro and Team extend the lifetime to three and seven days respectively, with more concurrent previews and larger container memory.
The point is that a Flask team can stop merging blind. Every pull request gets a real, running instance of the app — not a screenshot, not a staging server shared across the whole team.
Start your first Flask preview
PreviewDrop works with any Flask app — from a single app.py API to a multi-service monorepo with shared packages. The free tier gives you 2 concurrent previews with no credit card required. If you're new to the platform, the quickstart guide walks through connecting your first repository end to end.
Ready to give every branch a live URL?
Free tier — 2 concurrent previews, no credit card required.
Start free