Back to Blog
Tutorial10 min read2026-08-13

Environment Variables Done Right — Prompting for Secrets with pandastack.json

Use pandastack.json to prompt for Flask environment variables at deploy time — no hard-coded secrets, no broken builds, and a clean onboarding path for template users.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Flask apps fail silently when environment variables are missing. os.getenv('DATABASE_URL') returns None, SQLAlchemy tries to connect to None, and the app crashes during the first database query instead of at startup. By the time you see the error in logs, you have already wasted a deploy, waited through the build, and now need to add the variable and redeploy.

The standard workaround is to document the required variables in the README and hope users read it. Most do not. They click deploy, watch it succeed, open the app, and see a 500 error. Then they search the logs, find the missing variable, add it in the dashboard, and redeploy. This wastes time and creates a bad first impression.

pandastack.json solves this by making the deploy screen prompt for variables before the build starts. The user fills in the values, clicks Deploy, and the app boots correctly on the first try.

Why Flask apps break when environment variables are missing

Flask does not enforce environment variables at import time. You can write:

DATABASE_URL = os.getenv('DATABASE_URL')
SECRET_KEY = os.getenv('SECRET_KEY')

and the app starts successfully even if both variables are undefined. The failure happens later, when your code tries to use DATABASE_URL to connect to the database or SECRET_KEY to sign a session. By that point, the deploy is marked as successful, the health check passed (because the root route does not hit the database), and the app is live but broken.

The only way to catch this early is to fail at startup if required variables are missing:

import os
import sys

required_vars = ['DATABASE_URL', 'SECRET_KEY']
missing = [var for var in required_vars if not os.getenv(var)]

if missing:
    print(f"Missing required environment variables: {', '.join(missing)}")
    sys.exit(1)

This crashes the app during startup, which fails the health check and prevents the deploy from completing. The error appears in the build logs, and you know immediately what is wrong.

But an even better solution is to prompt for the variables before the build runs, so they are never missing in the first place.

Use pandastack.json to prompt for variables at deploy time

Create pandastack.json in the repository root:

{
  "type": "container",
  "name": "flask-app",
  "language": "python",
  "startCommand": "gunicorn app:app --bind 0.0.0.0:8000",
  "env": [
    {
      "key": "DATABASE_URL",
      "description": "PostgreSQL connection string (e.g., postgres://user:pass@host:5432/db)"
    },
    {
      "key": "SECRET_KEY",
      "description": "Flask secret key for session signing (generate with os.urandom(24).hex())"
    },
    {
      "key": "FLASK_ENV",
      "description": "Flask environment (production or development)",
      "value": "production"
    }
  ]
}

The env array defines three variables. Each entry has a key (the variable name), a description (help text shown to the user), and an optional value (a default that can be overridden).

When a user deploys the app — whether via the dashboard, the deploy button, or the API — the deploy screen shows three input fields:

  1. 1DATABASE_URL (empty, required)
  2. 2SECRET_KEY (empty, required)
  3. 3FLASK_ENV (pre-filled with production, overridable)

The user fills in the database URL and secret key, leaves FLASK_ENV at the default, and clicks Deploy. The build runs with those variables injected into the container environment, and the app reads them via os.getenv() at startup.

No missing variables, no crashes, no wasted deploys.

Deploy via the API with environment variables in the request

If you are scripting a deploy from CI or a local script, you can pass environment variables directly in the API request instead of relying on interactive prompts:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "flask-app",
    "repositoryName": "yourname/flask-app",
    "branch": "main",
    "autoDeploy": true,
    "startCommand": "gunicorn app:app --bind 0.0.0.0:8000",
    "env": [
      { "name": "DATABASE_URL", "value": "postgres://..." },
      { "name": "SECRET_KEY", "value": "random-256-bit-hex" },
      { "name": "FLASK_ENV", "value": "production" }
    ]
  }'

The API creates the project and injects the variables. The build starts immediately, and the app boots with the correct configuration.

This is the right approach when deploying from GitHub Actions or another CI system where the secrets are stored in the CI environment and should not be typed into a web form.

Set health checks so missing variables fail the deploy

Flask apps often bind to 127.0.0.1:5000 by default, which makes them unreachable from outside the container. Kubernetes runs a health check by sending an HTTP request to the container's IP address on the port you specified. If the app is listening on localhost instead of all interfaces, the health check times out and the deploy fails.

The correct start command for production is:

gunicorn app:app --bind 0.0.0.0:8000

0.0.0.0 means "listen on all network interfaces," and 8000 is the port Kubernetes expects. If your Flask app crashes at startup because DATABASE_URL is missing, the health check fails and the deploy is marked as failed. The error appears in the logs, and the previous version of the app keeps running.

This prevents a broken build from replacing a working app, which is critical for zero-downtime deployments.

Use gunicorn for production instead of Flask's dev server

Flask's built-in server (flask run or app.run()) is single-threaded and not designed for production traffic. It handles one request at a time, which means a slow endpoint blocks every other request until it finishes.

Gunicorn is a production WSGI server that spawns multiple worker processes and handles concurrent requests. Install it:

pip install gunicorn

Add it to requirements.txt:

flask
gunicorn

Set the start command in pandastack.json:

{
  "startCommand": "gunicorn app:app --bind 0.0.0.0:8000 --workers 4"
}

The --workers 4 flag spawns four worker processes, which can handle four concurrent requests. Free-tier containers have 0.25 CPU and 512 MB RAM, so four workers is a reasonable default. Paid tiers with more CPU can run more workers.

Link a managed PostgreSQL database and get DATABASE_URL injected automatically

Instead of manually typing the database connection string into the deploy screen, provision a managed database and link it to your app. PandaStack injects DATABASE_URL as an environment variable, and your Flask app reads it from os.getenv('DATABASE_URL').

Create the database:

panda databases create --name flask-db --engine postgres --version 16

Link it to your project during creation:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "flask-app",
    "repositoryName": "yourname/flask-app",
    "branch": "main",
    "linkDatabase": "flask-db",
    "env": [
      { "name": "SECRET_KEY", "value": "random-256-bit-hex" }
    ]
  }'

The platform links the database, injects DATABASE_URL, and the app connects automatically. You no longer need to copy-paste connection strings or store them in environment variables manually.

Run database migrations before the app starts

Flask apps using SQLAlchemy or Alembic need to run migrations after the database is provisioned but before the app starts handling requests. The cleanest approach is to chain the migration command with the start command:

{
  "startCommand": "flask db upgrade && gunicorn app:app --bind 0.0.0.0:8000"
}

The && operator runs the migration first. If the migration fails, the start command never executes, and the deploy fails. This prevents a broken schema from taking down the app.

If you are using a custom migration script instead of Flask-Migrate, replace flask db upgrade with your script:

{
  "startCommand": "python migrate.py && gunicorn app:app --bind 0.0.0.0:8000"
}

Add the deploy button to your README for one-click deploys

If your Flask app is a starter template or a demo, add a deploy button to the README so users can launch their own instance with one click:

[![Deploy to PandaStack](https://dashboard.pandastack.io/deploy-button.svg)](https://dashboard.pandastack.io/deploy?repo=yourname/flask-app&type=container&lang=python)

When someone clicks the badge, they land on the deploy screen with the repository URL pre-filled. Because the repo has pandastack.json, the screen prompts for DATABASE_URL, SECRET_KEY, and FLASK_ENV. They fill in the values, click Deploy, and the app boots correctly.

This is how demo apps and tutorials should distribute themselves: the button encodes the configuration, and the pandastack.json file documents the required variables.

References

  • [Flask deployment documentation](https://flask.palletsprojects.com/en/latest/deploying/)
  • [Gunicorn configuration](https://docs.gunicorn.org/en/stable/settings.html)
  • [PandaStack environment variables guide](https://docs.pandastack.io/projects/env/)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also