# Flask Production Operations Guide (3.1 Series): The Overall Picture of Application Factory, Blueprints, Configuration, Context, and Production Deployment

> An overall guide to designing and operating Flask 3.1 series at production quality. We systematize — in real code faithful to the latest official documentation — the philosophy of the WSGI microframework, the create_app application factory, splitting with Blueprints, configuration management with from_prefixed_env and the instance folder, the current_app/g context, extensions' init_app, error handling and logging, SECRET_KEY and safe Cookies, production deployment with Gunicorn + ProxyFix, and testing with pytest.

- Published: 2026-06-26
- Author: 友田 陽大
- Tags: Python, Flask, WSGI, アーキテクチャ設計, 本番運用, バックエンド
- URL: https://tomodahinata.com/en/blog/flask-production-guide
- Category: Flask in production

## Key points

- Flask is a 'minimal core + extensions' WSGI microframework. It doesn't bundle ORM, forms, or validation; you select and load only what you need. The current stable version is the 3.1 series (minimum Python 3.9)
- The backbone of production design is the application factory (create_app). Discard the global app and assemble configuration, extensions, and Blueprints inside a function, solving testability and multiple environments at once
- Make configuration 12-factor with from_prefixed_env() and the instance folder, driving SECRET_KEY and the DB URL out of code. Don't drag the app reference around with current_app/g
- In production, don't use the development server — run with Gunicorn (-w CPU×2) + a reverse proxy + ProxyFix. Harden the security boundary with SECRET_KEY, HttpOnly/Secure/SameSite Cookies, CSRF (Flask-WTF), and Jinja's auto-escaping
- The specifics (large-scale structure, context, testing, deployment, security, observability, technology selection) go from this pillar to each spoke article. Explained grounded in operational knowledge of the Flask backend of a METI-Minister's-Award B2B SaaS

---

## **Introduction: Flask is not "small" but "the core only"**

Flask tends to be introduced as a "small framework," but that understanding is dangerous in production operations. Precisely, Flask is a framework with the design philosophy that it **provides only the core of a WSGI application (routing, request/response, templates, configuration, context) and bundles neither an ORM, nor forms, nor authentication.** When the official calls it "micro," it doesn't mean "poor in features" — it means **"you decide what to load."**

This philosophy is double-edged. Assemble a configuration minimally fitted to the requirements, and you get a backend thinner, faster, and more readable than FastAPI or Django. On the other hand, precisely because **there's only a core**, if you don't design the structure (application factory, Blueprints, configuration, context, deployment) yourself, a small app slides straight into "a legacy dominated by global variables and import order." Most Flask-project failures come not from Flask's lack of features but from **this lack of structural design.**

This article is the pillar (overall map) for assembling that structure at production quality, **faithful to the official documentation of the Flask 3.1 series (the current stable version).** The author **designed and implemented the backend of a B2B SaaS that won the METI Minister's Award in Python / Flask / SQLAlchemy / PostgreSQL, and operated it in production on API Gateway → ALB → ECS (Fargate).** What I show here is only the design that was necessary in that real combat.

> 💡 **The version covered in this article**: I assume the **Flask 3.1 series** (the latest stable version at the time of writing is **3.1.3**, released 2026-02). Flask 3.1 requires **Python 3.9 or higher** and has, as dependencies, **Werkzeug (the WSGI/HTTP layer), Jinja (templates), MarkupSafe, ItsDangerous (signing), Click (CLI), and Blinker (signals).** The code in this article is based on the official documentation's patterns. The specifics link from this pillar to individual spoke articles.

---

## **1. The overall picture: the "7 design targets" of a Flask app**

In a production Flask app, what actually requires a design decision is just the following 7. This pillar surveys these 7 and hands the deep dive of each to a dedicated article.

| # | Design target | Central API / concept | Deep-dive article |
|---|---|---|---|
| 1 | App structure | `create_app` / `Blueprint` / `init_app` | [Large-scale structure guide](/blog/flask-application-factory-blueprints-large-app-structure-guide) |
| 2 | Configuration management | `config.from_*` / instance folder | This article §4 |
| 3 | Context | `current_app` / `g` / `request` / `session` | [Context thorough explanation](/blog/flask-application-request-context-g-current-app-guide) |
| 4 | Error handling & logs | `errorhandler` / `dictConfig` | [Error handling & observability guide](/blog/flask-error-handling-logging-observability-guide) |
| 5 | Security | `SECRET_KEY` / Cookie / CSRF / escaping | [Security implementation guide](/blog/flask-security-sessions-csrf-secure-cookies-guide) |
| 6 | Deployment | Gunicorn / `ProxyFix` / Docker | [Production deployment guide](/blog/flask-deployment-gunicorn-docker-production-wsgi-guide) |
| 7 | Testing | `test_client` / pytest fixtures | [Testing practical guide](/blog/flask-testing-pytest-test-client-fixtures-guide) |

And the upstream decision of whether to adopt Flask (comparison with FastAPI / Django) is summarized in the [technology-selection guide](/blog/flask-vs-fastapi-vs-django-comparison-guide).

---

## **2. Minimal Flask: first grasp that it's a "WSGI app"**

Before getting into the design talk, let me confirm what Flask is in 1 file. It's the minimal form of the official quickstart.

```python
# hello.py
from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
    return "Hello, World!"
```

Start it with the development server.

```bash
flask --app hello run --debug
```

What you should grasp here is 2 points.

1. **`app` is a WSGI application.** To borrow the official words, "Flask is a WSGI *application*, and a WSGI *server* runs it." That is, `flask run`'s development server and production's Gunicorn are **"servers"**, not Flask itself. This separation is the starting point of the deployment design described later.
2. **`--debug` is development-only.** The official clearly warns "**you must not deploy the development server to production. It's neither safe, nor stable, nor efficient.**" Production startup is the Gunicorn handled in §8.

> ⚠️ **Anti-pattern**: making `if __name__ == "__main__": app.run()` the entry point and starting that as-is in production. `app.run()` is the development server, and in production you have a WSGI server like Gunicorn load the `app` object. Always place `app.run()` in the `if __name__ == "__main__":` block and isolate it from the production path.

This minimal form is good for learning, but the point that **a global `app` is placed at the module top** becomes technical debt as-is in production. The next section resolves it.

---

## **3. The application factory: the backbone of production Flask**

### 3.1 Why discard the global `app`

Write `app = Flask(__name__)` at the module top, and 3 problems arise structurally.

- **You can't swap the configuration in tests**: `app` is fixed at import time, so there's no gap to switch to a test DB or test configuration.
- **You can't have multiple configurations**: you can't create apps with different configurations for production, staging, and test at the same time.
- **It induces circular imports**: views and models import `app`, and the `app` side imports them too, so the dependency circulates.

The solution is the **application factory.** **Assemble and return `app` inside a function.** This lets the caller control "when, with which configuration, loading which parts" the app is created. This is the canonical form of the official tutorial.

```python
# src/myapp/__init__.py
import os

from flask import Flask


def create_app(test_config: dict | None = None) -> Flask:
    # instance_relative_config=True で、設定をリポジトリ外の instance/ から読む
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        SECRET_KEY="dev",  # 本番では必ず上書き（§5・§7）
        DATABASE=os.path.join(app.instance_path, "myapp.sqlite"),
    )

    if test_config is None:
        # 本番/開発：instance/config.py があれば上書き（無ければ黙ってスキップ）
        app.config.from_pyfile("config.py", silent=True)
    else:
        # テスト：呼び出し側が渡した設定で上書き
        app.config.from_mapping(test_config)

    # instance フォルダは自動生成されない。明示的に作る
    os.makedirs(app.instance_path, exist_ok=True)

    @app.route("/health")
    def health():
        return {"status": "ok"}

    return app
```

For startup too, the `flask` CLI auto-detects the factory.

```bash
flask --app myapp run --debug
```

> 💡 **Factory auto-detection**: `flask --app`, if the target module has a function named `create_app` or `make_app`, **automatically calls it as the factory.** When you want to pass arguments, you can write the parentheses' content as a Python literal, like `flask --app 'myapp:create_app("dev")' run`. In production's Gunicorn too, you can use the same format `gunicorn 'myapp:create_app()'` (§8).

### 3.2 Extensions are "unbound → bound with `init_app`"

Extensions like Flask-SQLAlchemy are also made consistent with the factory. **Generate the extension object at the module top without `app`, and call `init_app(app)` inside the factory to bind it.** This way, one extension object can be reused across multiple apps (production, test), and circular imports are avoided too.

```python
# src/myapp/extensions.py — どのアプリにも束縛されていない「裸」の拡張
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()
```

```python
# src/myapp/__init__.py（抜粋）
from .extensions import db, migrate


def create_app(test_config=None):
    app = Flask(__name__, instance_relative_config=True)
    # ...設定読み込み...

    db.init_app(app)              # ここで初めて app に束縛
    migrate.init_app(app, db)

    from .blueprints.api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix="/api")

    return app
```

The deep dive on this pattern, the package structure that avoids circular imports, and Blueprint nesting and CLI commands is split into the [large-scale structure guide](/blog/flask-application-factory-blueprints-large-app-structure-guide).

---

## **4. Configuration management: drive secrets out of code**

`app.config` is a dict subclass that "treats only uppercase keys as meaningful configuration." In production, **don't write secrets (`SECRET_KEY`, DB password, API keys) in code** — this is the core of 12-factor and the minimum line of security. Flask provides multiple loading entrances.

| Method | Use | Example |
|---|---|---|
| `from_mapping(**kw)` | In-code default values | `app.config.from_mapping(SECRET_KEY="dev")` |
| `from_object(obj)` | Per-environment Config class | `app.config.from_object("myapp.config.Production")` |
| `from_pyfile(path, silent=True)` | A Python config in the instance folder | `app.config.from_pyfile("config.py", silent=True)` |
| `from_prefixed_env()` | Environment variables (`FLASK_` prefix) | `app.config.from_prefixed_env()` |
| `from_file(path, load=...)` | JSON / TOML config | `app.config.from_file("config.toml", load=tomllib.load, text=False)` |

### 4.1 Recommended: `from_object` (defaults) → `from_prefixed_env` (environment override)

The basic form I use in production is the two-stage **"give safe default values with a Config class, override with production values via environment variables."** `from_prefixed_env()` is a method **added in Flask 3.0** that's optimal for the container/12-factor era, auto-loading environment variables starting with `FLASK_` and typing the values with `json.loads`.

```python
# src/myapp/config.py
class BaseConfig:
    JSON_SORT_KEYS = False
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"


class ProductionConfig(BaseConfig):
    SESSION_COOKIE_SECURE = True  # HTTPS 限定 Cookie（§7）


class TestConfig(BaseConfig):
    TESTING = True
```

```python
# create_app 内
app.config.from_object("myapp.config.ProductionConfig")
app.config.from_prefixed_env()  # FLASK_SECRET_KEY, FLASK_SQLALCHEMY_DATABASE_URI ...
```

```bash
# 本番の環境変数（コンテナの Secrets から注入）
export FLASK_SECRET_KEY='...'                 # python -c 'import secrets; print(secrets.token_hex())'
export FLASK_SQLALCHEMY_DATABASE_URI='postgresql+psycopg://...'
export FLASK_MAX_CONTENT_LENGTH=10485760       # json.loads されて int になる
```

> 💡 **The power of `from_prefixed_env`**: because the value is interpreted with `json.loads`, `FLASK_MAX_CONTENT_LENGTH=10485760` is loaded not as a string but as `int 10485760`. You can also assign to nested keys with `__` (double underscore). The typical worry of "environment variables are all strings and hard to handle" is resolved by the Flask side.

### 4.2 The instance folder: a configuration storage outside the repository

Specify `Flask(__name__, instance_relative_config=True)`, and `app.instance_path` (by default `<project>/instance/`) becomes "**a storage for configuration, SQLite, and uploads not committed to the repository.**" `from_pyfile("config.py")` is resolved relative to this `instance/`. Be sure to put `instance/` in `.gitignore`.

> ⚠️ **Don't write `DEBUG` in a config file.** The official cautions "enabling `DEBUG` in code or configuration may not work as expected." Enable debug **only during development**, with the `FLASK_DEBUG=1` environment variable or `flask run --debug`. `DEBUG=True` in production is a serious vulnerability that exposes the interactive debugger.

### 4.3 The "production-effective" settings added in Flask 3.1

In the Flask 3.1 series, settings directly tied to production hardening have been added. The details are handled in each spoke, but let me list them as the pillar.

| Setting key | Default | Added | Effect |
|---|---|---|---|
| `SECRET_KEY_FALLBACKS` | `None` | 3.1 | Key rotation (continue signature verification with the old key) |
| `SESSION_COOKIE_PARTITIONED` | `False` | 3.1 | CHIPS (partitioned Cookie). Enabling it also forces `SECURE` |
| `MAX_FORM_MEMORY_SIZE` | `500_000` | 3.1 | The size cap for non-file form values (DoS mitigation) |
| `MAX_FORM_PARTS` | `1_000` | 3.1 | The cap on the number of form parts (DoS mitigation) |
| `TRUSTED_HOSTS` | `None` | 3.1 | Verify the Host header at routing time (Host-header attack countermeasure) |

---

## **5. Context: "don't drag it around" with `current_app` and `g`**

Create the app with a factory, and a situation arises where "you can't import `app` from anywhere" (because it doesn't exist at import time). Flask solves this with **context locals.**

- **`current_app`**: a proxy to the app currently being processed. Reaches the configuration and extensions without importing `app`.
- **`g`**: a namespace that lives only during the current context (≒ 1 request). Used for caching a DB connection, etc.
- **`request` / `session`**: the current request/session.

The typical pattern the official shows is "during a request, reuse the same DB connection, and close it at the end."

```python
from flask import g, current_app


def get_db():
    if "db" not in g:                       # リクエスト内で初回だけ接続
        g.db = connect_to_database(current_app.config["DATABASE"])
    return g.db


@app.teardown_appcontext
def teardown_db(exception):                  # コンテキスト終了時に必ず閉じる
    db = g.pop("db", None)
    if db is not None:
        db.close()
```

> 💡 **`g` is not a "global variable."** The official states clearly — "the 'g' in `g` is for global, but it means global *within the context.* The data is lost when the context ends, and **it can't be used for storage across requests.** For storage between requests, use `session` or the DB." Furthermore, as an important fact, Flask's context locals are implemented with **Python's `contextvars` and Werkzeug's `LocalProxy`**, not mere thread locals. This is the reason they function correctly even in `async` views too.

The mechanism of pushing/popping the context, `_get_current_object()`, the difference between `teardown_appcontext` and `teardown_request`, `copy_current_request_context` that carries the context out into a background task like an invoice, and the correct handling of `RuntimeError: Working outside of application context` — these are dug into in the [context thorough explanation](/blog/flask-application-request-context-g-current-app-guide).

---

## **6. Error handling and logging: don't "stay silent" in production**

### 6.1 Error handlers: an API design that returns JSON, not HTML

In a REST API, the standard play is to return exceptions as **structured JSON**, not an HTML error page. Flask can register a handler per exception class/status code with `@app.errorhandler`.

```python
from flask import jsonify
from werkzeug.exceptions import HTTPException


@app.errorhandler(HTTPException)
def handle_http_exception(e: HTTPException):
    """すべての HTTP エラーを JSON で返す。"""
    return jsonify(code=e.code, name=e.name, description=e.description), e.code or 500


@app.errorhandler(404)
def not_found(e):
    return jsonify(error="resource not found"), 404
```

From a view, raise an error with `abort(404, description="...")`. The handler resolution order (code → class hierarchy → the most specific), the priority of Blueprint-side handlers and the exception "a Blueprint can't catch 404," and the handling of `InternalServerError.original_exception` are split into the [error handling & observability guide](/blog/flask-error-handling-logging-observability-guide).

### 6.2 Logging: `dictConfig` "before app creation"

Flask's logging is the standard `logging` itself. The official's most important caution is **"configure logging before app creation"** — because touching `app.logger` before configuration attaches a default handler. The canonical form is `logging.config.dictConfig`.

```python
from logging.config import dictConfig

dictConfig({
    "version": 1,
    "formatters": {"default": {
        "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
    }},
    "handlers": {"wsgi": {
        "class": "logging.StreamHandler",
        "stream": "ext://flask.logging.wsgi_errors_stream",
        "formatter": "default",
    }},
    "root": {"level": "INFO", "handlers": ["wsgi"]},
})

app = Flask(__name__)  # ← dictConfig の後で生成する
```

In production, extend this to **JSON structured logs + request ID** (inject `request.url` etc. with a `RequestFormatter`). The design of observability (correlation IDs, Sentry, health checks) also goes to the [error handling & observability guide](/blog/flask-error-handling-logging-observability-guide).

---

## **7. Security: harden the boundary with "configuration"**

Flask's "micro" is, in security too, "smart defaults + your choices." Let me list the boundaries to harden at minimum in production (the deep dive is the [security implementation guide](/blog/flask-security-sessions-csrf-secure-cookies-guide)).

- **Always set `SECRET_KEY`.** Flask's `session` is **a client-side signed Cookie** (signed with ItsDangerous). Tampering can be detected, but if `SECRET_KEY` leaks or is weak, the signature can be forged. Generate with `python -c 'import secrets; print(secrets.token_hex())'`.
- **Harden the Cookie attributes.** `SESSION_COOKIE_HTTPONLY` (default `True`), `SESSION_COOKIE_SECURE` (default `False` → **`True` in production**), `SESSION_COOKIE_SAMESITE` (`"Lax"` recommended).
- **CSRF is not bundled.** The official states "a form-validation framework doesn't exist in Flask." Introduce CSRF protection with **Flask-WTF's `CSRFProtect`.**
- **XSS: Jinja's auto-escaping is the default.** `.html` / `.htm` / `.xml` / `.xhtml` templates are auto-escaped. Import `escape` / `Markup` from **`markupsafe`** (not from `flask`).
- **Security headers aren't attached by default.** Attach HSTS, CSP, `X-Content-Type-Options: nosniff`, and `X-Frame-Options` yourself, or leave it to **Flask-Talisman.**

```python
# 本番の Cookie 既定（ProductionConfig 等で）
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
)
```

---

## **8. Deployment: discard the development server, run with Gunicorn**

In production, don't use `flask run` (the development server). Since Flask is a WSGI *app*, a **WSGI *server*** loads and runs the `app`. The standard on Linux is **Gunicorn.**

```bash
# app オブジェクトを直接指す場合
gunicorn -w 4 'myapp:app'

# アプリケーションファクトリの場合（§3）
gunicorn -w 4 'myapp:create_app()'
```

- **`-w` (number of workers)**: the official's starting point is **`CPU × 2`.** The default is 1 worker, which is insufficient for production.
- **Behind a reverse proxy, `ProxyFix`.** To trust `X-Forwarded-*` behind nginx / ALB, interpose Werkzeug's `ProxyFix`. **If you don't correctly set the number of trusted proxy hops, a client could send a fake `X-Forwarded-For`**, so be careful here.

```python
from werkzeug.middleware.proxy_fix import ProxyFix

app.wsgi_app = ProxyFix(
    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
```

How to choose the worker type (synchronous vs `gevent`), Docker multi-stage / non-root / health checks, graceful shutdown, and the handling of `TRUSTED_HOSTS`/`SERVER_NAME` are consolidated in the [production deployment guide](/blog/flask-deployment-gunicorn-docker-production-wsgi-guide).

> 💡 **The misconception of `async def` views**: Flask supports `async def` views from 2.0 (`pip install flask[async]`), but the official clearly cautions — **"each request still occupies 1 worker. Making a view async doesn't *change the number of requests handled simultaneously*."** Where async takes effect is IO concurrency like "calling multiple external APIs in parallel within 1 view," not a throughput improvement. If full-fledged async is a requirement, consider ASGI-premised Quart — that's the official's position.

---

## **9. Testing: fix the contract with `test_client` and pytest fixtures**

Flask is a highly testable framework. With `app.test_client()` you can round-trip requests without standing up an actual server, and with a factory configuration you can **create a test-only-configured app every time.**

```python
# tests/conftest.py
import pytest
from myapp import create_app


@pytest.fixture()
def app():
    app = create_app({"TESTING": True})
    yield app


@pytest.fixture()
def client(app):
    return app.test_client()


@pytest.fixture()
def runner(app):
    return app.test_cli_runner()
```

```python
# tests/test_health.py
def test_health(client):
    res = client.get("/health")
    assert res.status_code == 200
    assert res.json == {"status": "ok"}
```

The meaning of `TESTING=True`, session manipulation with `session_transaction()`, post-request verification with `with client:`, `follow_redirects`, and testing CLI commands (`test_cli_runner`) are dug into in the [testing practical guide](/blog/flask-testing-pytest-test-client-fixtures-guide).

---

## **10. Recommended package structure: connecting everything**

Let me bundle the design so far into one directory structure. This is the "form" of production Flask.

```text
myapp/
├── pyproject.toml
├── src/
│   └── myapp/
│       ├── __init__.py        # create_app (the application factory) §3
│       ├── config.py          # per-environment Config classes §4
│       ├── extensions.py      # "bare" extensions like db = SQLAlchemy() §3.2
│       ├── logging.py         # dictConfig §6.2
│       ├── errors.py          # common error handlers §6.1
│       ├── models/            # SQLAlchemy models
│       └── blueprints/
│           ├── auth/          # auth Blueprint
│           └── api/           # API Blueprint
├── instance/                  # .gitignore (secrets, SQLite) §4.2
│   └── config.py
└── tests/
    ├── conftest.py            # app / client / runner fixtures §9
    └── test_*.py
```

> 💡 **The "extract on the third" principle**: there's no need to finely split `blueprints/` from the start. Start with one file, and split into Blueprints after features increase to 2 or 3 — that's YAGNI. However, put in just the separation of `create_app` and `extensions.py` from the start. This is a separation to satisfy not "future extension" but "the current requirement of testability."

---

## **Summary: Flask is a framework that takes responsibility for "design freedom"**

Flask's essence is **"provide only the core, and you decide the structure."** That's exactly why production quality is decided by the following discipline.

1. With the **application factory** (`create_app`), discard the global `app` and gain testability and multiple environments at once.
2. Make configuration 12-factor with **`from_prefixed_env` + the instance folder**, driving secrets out of code.
3. Don't drag the app reference around with **`current_app` / `g`**, and bind resources to the context's lifespan.
4. **Errors in JSON, logs with `dictConfig`**, so it doesn't stay silent in production.
5. Harden the security boundary with configuration via **`SECRET_KEY`, safe Cookies, CSRF, and auto-escaping.**
6. Discard the development server and run with **Gunicorn + ProxyFix.**
7. Fix the boundary's contract in tests with **`test_client` + pytest fixtures.**

These 7 are universal design targets that, in changed forms, become necessary in FastAPI and Django too. Flask lets you write them **the thinnest and most explicitly** — that's Flask's strength when designed appropriately. For the deep dives of the specifics, proceed to each spoke article linked from this pillar. The judgment of whether to adopt Flask itself is best started from the [Flask vs FastAPI vs Django technology-selection guide](/blog/flask-vs-fastapi-vs-django-comparison-guide).
