Flask #

Flask is the most popular Python microframework for building web applications and APIs. With its “minimal by design” philosophy, Flask doesn’t impose a project structure, ORM, or form handling. You choose: SQLAlchemy or raw queries, Jinja templates or a React frontend, Flask-Login or manual JWT. This flexibility makes Flask a great fit for API microservices, internal dashboards, or small-to-mid-scale applications.

Docker Compose complements Flask consistently: the application, database, and cache run as separate containers. No need to install Postgres or Redis on the host. This article covers a Docker Compose setup for Flask local development, from Python Dockerfiles, hot reload, to SQLAlchemy, Celery, and best practices.

Prerequisites #

Make sure you have installed:

  • Docker and Docker Compose (latest versions)
  • Python 3.12+ (optional, for host-side development)
  • pip or poetry

A standard Flask project structure using the Application Factory pattern:

my-flask-app/
├── app/
│   ├── __init__.py        # Application factory
│   ├── config.py
│   ├── extensions.py      # SQLAlchemy, Migrate, etc
│   ├── models/
│   │   ├── __init__.py
│   │   └── user.py
│   ├── api/
│   │   ├── __init__.py
│   │   └── users.py       # Blueprint
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py
│   └── utils/
│       └── errors.py
├── migrations/             # Alembic / Flask-Migrate
├── tests/
├── wsgi.py                 # Entry point
├── requirements/
│   ├── base.txt
│   └── dev.txt
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .env
├── .flaskenv
└── .dockerignore

The application factory is the idiomatic Flask pattern that separates app instance creation from configuration. It makes testing easier (a separate app per test) and enables multi-environment configuration.

A Dockerfile for Production #

# syntax=docker/dockerfile:1.6
FROM python:3.12-slim AS builder

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements/base.txt requirements.txt
RUN pip install --upgrade pip \
    && pip install --prefix=/install --no-cache-dir -r requirements.txt

FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    curl \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /install /usr/local
COPY . .

RUN addgroup -S flask && adduser -S flask -G flask
USER flask

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "3", "--access-logfile", "-", "wsgi:app"]

A slim production image with Gunicorn and a non-root user.

A Dockerfile for Development #

# Dockerfile.dev
FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    FLASK_APP=wsgi.py \
    FLASK_DEBUG=1

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

COPY requirements/dev.txt requirements.txt
RUN pip install --upgrade pip \
    && pip install -r requirements.txt

EXPOSE 5000

CMD ["flask", "run", "--host=0.0.0.0", "--port=5000", "--debug"]

FLASK_APP=wsgi.py tells Flask the entry point. FLASK_DEBUG=1 enables debug mode and auto-reload.

.flaskenv and Configuration #

The .flaskenv file (auto-loaded by python-dotenv through the Flask CLI):

FLASK_APP=wsgi.py
FLASK_DEBUG=1
FLASK_ENV=development

config.py:

import os
from datetime import timedelta
from urllib.parse import urlparse


class BaseConfig:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-change-me")
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        "DATABASE_URL",
        "postgresql://flask:pass@db:5432/flaskapp",
    )
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ENGINE_OPTIONS = {
        "pool_pre_ping": True,
        "pool_recycle": 300,
    }

    REDIS_URL = os.environ.get("REDIS_URL", "redis://cache:6379/0")

    # Session & JWT
    SESSION_COOKIE_SECURE = False
    SESSION_COOKIE_HTTPONLY = True
    PERMANENT_SESSION_LIFETIME = timedelta(days=7)
    JWT_SECRET_KEY = os.environ.get("JWT_SECRET", "dev-jwt-secret")
    JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
    JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)

    # CORS
    CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "*")

    # Pagination
    ITEMS_PER_PAGE = 20

    # Logging
    LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")


class DevelopmentConfig(BaseConfig):
    DEBUG = True
    TEMPLATES_AUTO_RELOAD = True
    EXPLAIN_TEMPLATE_LOADING = False
    SQLALCHEMY_ECHO = False


class TestingConfig(BaseConfig):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
    WTF_CSRF_ENABLED = False


class ProductionConfig(BaseConfig):
    DEBUG = False
    SESSION_COOKIE_SECURE = True
    SQLALCHEMY_ECHO = False


config_by_name = {
    "development": DevelopmentConfig,
    "testing": TestingConfig,
    "production": ProductionConfig,
    "default": DevelopmentConfig,
}

The Application Factory #

app/__init__.py:

import os
from flask import Flask
from .config import config_by_name
from .extensions import db, migrate, jwt, cors, ma


def create_app(config_name=None):
    if config_name is None:
        config_name = os.environ.get("FLASK_ENV", "development")
    app = Flask(__name__)
    app.config.from_object(config_by_name[config_name])

    # Init extensions
    db.init_app(app)
    migrate.init_app(app, db)
    jwt.init_app(app)
    cors.init_app(app, resources={r"/api/*": {"origins": app.config["CORS_ORIGINS"]}})
    ma.init_app(app)

    # Register blueprints
    from .api.users import users_bp
    from .api.auth import auth_bp
    from .api.health import health_bp

    app.register_blueprint(health_bp, url_prefix="/")
    app.register_blueprint(auth_bp, url_prefix="/api/v1/auth")
    app.register_blueprint(users_bp, url_prefix="/api/v1/users")

    # Error handlers
    from .utils.errors import register_error_handlers
    register_error_handlers(app)

    return app

app/extensions.py:

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_jwt_extended import JWTManager
from flask_cors import CORS
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
migrate = Migrate()
jwt = JWTManager()
cors = CORS()
ma = Marshmallow()

wsgi.py:

from app import create_app

app = create_app()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

Models with SQLAlchemy #

app/models/user.py:

import uuid
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from ..extensions import db


class User(db.Model):
    __tablename__ = "users"

    id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    full_name = db.Column(db.String(255), nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    is_active = db.Column(db.Boolean, default=True, nullable=False)
    is_verified = db.Column(db.Boolean, default=False, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    def to_dict(self):
        return {
            "id": self.id,
            "email": self.email,
            "username": self.username,
            "full_name": self.full_name,
            "is_active": self.is_active,
            "is_verified": self.is_verified,
            "created_at": self.created_at.isoformat(),
        }

    def __repr__(self):
        return f"<User {self.email}>"

The Service Layer #

Separate business logic from route handlers. The service layer is where business validation, data transformation, and repository orchestration live.

app/services/user_service.py:

import re
from typing import Optional, List
from ..extensions import db
from ..models.user import User


class UserServiceError(Exception):
    pass


class UserService:
    @staticmethod
    def _validate_email(email: str) -> str:
        email = email.strip().lower()
        if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", email):
            raise UserServiceError("invalid email format")
        return email

    @staticmethod
    def _validate_password(password: str) -> str:
        if len(password) < 8:
            raise UserServiceError("password must be at least 8 characters")
        if len(password) > 72:
            raise UserServiceError("password too long")
        return password

    def register(self, email: str, username: str, full_name: str, password: str) -> User:
        email = self._validate_email(email)
        password = self._validate_password(password)

        if User.query.filter_by(email=email).first():
            raise UserServiceError("email already registered")
        if User.query.filter_by(username=username).first():
            raise UserServiceError("username already taken")

        user = User(email=email, username=username, full_name=full_name)
        user.set_password(password)
        db.session.add(user)
        db.session.commit()
        return user

    def authenticate(self, email: str, password: str) -> Optional[User]:
        user = User.query.filter_by(email=email.strip().lower()).first()
        if not user or not user.check_password(password):
            return None
        return user

    def get(self, user_id: str) -> Optional[User]:
        return User.query.get(user_id)

    def list(self, page: int = 1, per_page: int = 20) -> dict:
        pagination = User.query.order_by(User.created_at.desc()).paginate(
            page=page, per_page=per_page, error_out=False
        )
        return {
            "data": [u.to_dict() for u in pagination.items],
            "page": pagination.page,
            "per_page": pagination.per_page,
            "total": pagination.total,
            "pages": pagination.pages,
        }

    def update(self, user_id: str, **kwargs) -> Optional[User]:
        user = User.query.get(user_id)
        if not user:
            return None
        for key, value in kwargs.items():
            if key in ("email", "full_name", "is_active") and hasattr(user, key):
                setattr(user, key, value)
        db.session.commit()
        return user

    def delete(self, user_id: str) -> bool:
        user = User.query.get(user_id)
        if not user:
            return False
        db.session.delete(user)
        db.session.commit()
        return True

APIs with Blueprints #

app/api/users.py:

from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from ..services.user_service import UserService, UserServiceError

users_bp = Blueprint("users", __name__)
user_service = UserService()


@users_bp.route("", methods=["POST"])
@jwt_required()
def create_user():
    data = request.get_json() or {}
    try:
        user = user_service.register(
            email=data.get("email", ""),
            username=data.get("username", ""),
            full_name=data.get("full_name", ""),
            password=data.get("password", ""),
        )
        return jsonify(user.to_dict()), 201
    except UserServiceError as e:
        return jsonify({"error": str(e)}), 400


@users_bp.route("", methods=["GET"])
@jwt_required()
def list_users():
    page = request.args.get("page", 1, type=int)
    per_page = request.args.get("per_page", 20, type=int)
    return jsonify(user_service.list(page=page, per_page=per_page)), 200


@users_bp.route("/<string:user_id>", methods=["GET"])
@jwt_required()
def get_user(user_id):
    user = user_service.get(user_id)
    if not user:
        return jsonify({"error": "user not found"}), 404
    return jsonify(user.to_dict()), 200


@users_bp.route("/<string:user_id>", methods=["PUT"])
@jwt_required()
def update_user(user_id):
    data = request.get_json() or {}
    user = user_service.update(user_id, **data)
    if not user:
        return jsonify({"error": "user not found"}), 404
    return jsonify(user.to_dict()), 200


@users_bp.route("/<string:user_id>", methods=["DELETE"])
@jwt_required()
def delete_user(user_id):
    if not user_service.delete(user_id):
        return jsonify({"error": "user not found"}), 404
    return "", 204


@users_bp.route("/me", methods=["GET"])
@jwt_required()
def me():
    user_id = get_jwt_identity()
    user = user_service.get(user_id)
    if not user:
        return jsonify({"error": "user not found"}), 404
    return jsonify(user.to_dict()), 200

app/api/auth.py:

from flask import Blueprint, request, jsonify
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
from ..services.user_service import UserService, UserServiceError

auth_bp = Blueprint("auth", __name__)
user_service = UserService()


@auth_bp.route("/register", methods=["POST"])
def register():
    data = request.get_json() or {}
    try:
        user = user_service.register(
            email=data.get("email", ""),
            username=data.get("username", ""),
            full_name=data.get("full_name", ""),
            password=data.get("password", ""),
        )
        return jsonify(user.to_dict()), 201
    except UserServiceError as e:
        return jsonify({"error": str(e)}), 400


@auth_bp.route("/login", methods=["POST"])
def login():
    data = request.get_json() or {}
    user = user_service.authenticate(
        email=data.get("email", ""),
        password=data.get("password", ""),
    )
    if not user:
        return jsonify({"error": "invalid credentials"}), 401

    access_token = create_access_token(identity=user.id)
    refresh_token = create_refresh_token(identity=user.id)
    return jsonify({
        "access_token": access_token,
        "refresh_token": refresh_token,
        "user": user.to_dict(),
    }), 200


@auth_bp.route("/refresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh():
    user_id = get_jwt_identity()
    access_token = create_access_token(identity=user_id)
    return jsonify({"access_token": access_token}), 200

Error Handlers #

app/utils/errors.py:

from flask import jsonify
from werkzeug.exceptions import HTTPException
from sqlalchemy.exc import IntegrityError


class APIError(Exception):
    def __init__(self, message, status_code=400, payload=None):
        super().__init__()
        self.message = message
        self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv["error"] = self.message
        rv["status"] = self.status_code
        return rv


def register_error_handlers(app):
    @app.errorhandler(APIError)
    def handle_api_error(err):
        return jsonify(err.to_dict()), err.status_code

    @app.errorhandler(HTTPException)
    def handle_http(err):
        return jsonify({"error": err.description, "status": err.code}), err.code

    @app.errorhandler(IntegrityError)
    def handle_integrity(err):
        from ..extensions import db
        db.session.rollback()
        return jsonify({"error": "database integrity error"}), 409

    @app.errorhandler(404)
    def handle_404(err):
        return jsonify({"error": "not found"}), 404

    @app.errorhandler(500)
    def handle_500(err):
        return jsonify({"error": "internal server error"}), 500

Health Checks #

app/api/health.py:

from flask import Blueprint, jsonify
from sqlalchemy import text
from ..extensions import db, jwt

health_bp = Blueprint("health", __name__)


@health_bp.route("/healthz", methods=["GET"])
def healthz():
    try:
        db.session.execute(text("SELECT 1"))
        db_ok = True
    except Exception:
        db_ok = False
    return jsonify({
        "status": "ok" if db_ok else "degraded",
        "database": "up" if db_ok else "down",
    }), 200 if db_ok else 503


@health_bp.route("/readyz", methods=["GET"])
def readyz():
    return jsonify({"status": "ready"}), 200

/healthz checks dependencies — returns 503 when the database is down. /readyz always returns 200 (server alive, ready to accept requests).

docker-compose.yml #

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: flask-app:dev
    container_name: flask-api
    command: flask run --host=0.0.0.0 --port=5000 --debug
    volumes:
      - ./:/app
    ports:
      - "5000:5000"
    environment:
      - FLASK_APP=wsgi.py
      - FLASK_ENV=development
      - FLASK_DEBUG=1
      - DATABASE_URL=postgresql://flask:pass@db:5432/flaskapp
      - REDIS_URL=redis://cache:6379/0
      - SECRET_KEY=local-dev-secret-change-me
      - JWT_SECRET=local-dev-jwt-secret
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  worker:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: celery -A app.celery worker --loglevel=info
    volumes:
      - ./:/app
    environment:
      - DATABASE_URL=postgresql://flask:pass@db:5432/flaskapp
      - REDIS_URL=redis://cache:6379/0
    depends_on:
      - api
      - cache
    profiles: ["with-worker"]

  db:
    image: postgres:16-alpine
    container_name: flask-db
    environment:
      - POSTGRES_USER=flask
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=flaskapp
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U flask -d flaskapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: flask-cache
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    volumes:
      - cache-data:/data
    ports:
      - "6379:6379"

volumes:
  db-data:
  cache-data:

The worker service is a Celery worker for background tasks. Enabled with --profile with-worker.

Celery for Background Tasks #

app/celery.py:

from celery import Celery
from celery.schedules import crontab

celery_app = Celery(
    "flask-app",
    broker="redis://cache:6379/1",
    backend="redis://cache:6379/2",
    include=["app.tasks"],
)


class ContextTask(celery_app.Task):
    abstract = True

    def __call__(self, *args, **kwargs):
        from app import create_app
        with create_app().app_context():
            return self.run(*args, **kwargs)


celery_app.Task = ContextTask

# Schedule
celery_app.conf.beat_schedule = {
    "cleanup-expired-tokens": {
        "task": "app.tasks.cleanup_expired_tokens",
        "schedule": crontab(minute=0, hour=2),  # 2 AM
    },
    "send-daily-report": {
        "task": "app.tasks.send_daily_report",
        "schedule": crontab(minute=0),  # every hour
    },
}

app/tasks.py:

import logging
from datetime import datetime, timedelta
from .celery import celery_app
from .extensions import db
from .models.user import User

log = logging.getLogger(__name__)


@celery_app.task(name="app.tasks.cleanup_expired_tokens")
def cleanup_expired_tokens():
    # Delete users who never verified within 7 days
    cutoff = datetime.utcnow() - timedelta(days=7)
    deleted = User.query.filter(
        User.is_verified == False,
        User.created_at < cutoff,
    ).delete()
    db.session.commit()
    log.info(f"cleaned up {deleted} unverified users")
    return {"deleted": deleted}


@celery_app.task(name="app.tasks.send_daily_report", bind=True, max_retries=3)
def send_daily_report(self):
    try:
        total_users = User.query.count()
        new_today = User.query.filter(
            User.created_at >= datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
        ).count()
        log.info(f"daily report: {total_users} total, {new_today} new today")
        return {"total": total_users, "new_today": new_today}
    except Exception as e:
        log.exception("daily report failed")
        raise self.retry(exc=e, countdown=60)

Migrations with Flask-Migrate #

# Init (once; already done in the project template)
docker compose exec api flask db init

# Create a migration after changing models
docker compose exec api flask db migrate -m "add user model"

# Apply the migration
docker compose exec api flask db upgrade

# Rollback
docker compose exec api flask db downgrade

flask db is the CLI from flask-migrate, a wrapper over Alembic. It detects model changes and generates migration files.

Build and Run #

# Build
docker compose build

# Run all services
docker compose up -d

# Run with the worker
docker compose --profile with-worker up -d

# View logs
docker compose logs -f api

# Flask shell
docker compose exec api flask shell

# View routes
docker compose exec api flask routes

# Stop
docker compose down

Access:

  • API: http://localhost:5000
  • Health: http://localhost:5000/healthz
  • PostgreSQL: localhost:5432
  • Redis: localhost:6379

Test:

# Register
curl -X POST http://localhost:5000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","username":"dedi","full_name":"Dedi","password":"password123"}'

# Login
curl -X POST http://localhost:5000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"password123"}'

# Get the current user (using a token)
curl http://localhost:5000/api/v1/users/me \
  -H "Authorization: Bearer your-token-here"

Testing with Pytest #

tests/conftest.py:

import pytest
from app import create_app
from app.extensions import db as _db


@pytest.fixture(scope="session")
def app():
    app = create_app("testing")
    with app.app_context():
        _db.create_all()
        yield app
        _db.drop_all()


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


@pytest.fixture
def db(app):
    yield _db
    _db.session.rollback()

tests/test_auth.py:

def test_register(client):
    response = client.post("/api/v1/auth/register", json={
        "email": "[email protected]",
        "username": "testuser",
        "full_name": "Test User",
        "password": "password123",
    })
    assert response.status_code == 201
    data = response.get_json()
    assert data["email"] == "[email protected]"
    assert "password" not in data


def test_login(client):
    client.post("/api/v1/auth/register", json={
        "email": "[email protected]",
        "username": "testuser",
        "full_name": "Test",
        "password": "password123",
    })
    response = client.post("/api/v1/auth/login", json={
        "email": "[email protected]",
        "password": "password123",
    })
    assert response.status_code == 200
    assert "access_token" in response.get_json()


def test_login_invalid(client):
    response = client.post("/api/v1/auth/login", json={
        "email": "[email protected]",
        "password": "wrong",
    })
    assert response.status_code == 401
docker compose exec api pytest

When Flask Fits #

Use Flask if:
  ✓ Lightweight API microservices
  ✓ You need high flexibility (choose your own ORM, libraries, etc.)
  ✓ Small-to-mid-scale applications
  ✓ Simple web apps without complex ORM/admin needs
  ✓ The team likes simple, idiomatic Python

Avoid Flask if:
  ✗ You need an out-of-the-box ORM and admin (use Django)
  ✗ You need end-to-end type hints (use FastAPI)
  ✗ Complex traditional server-rendered apps
  ✗ Real-time apps (Flask-SocketIO is limited)

Flask fits API microservices, internal dashboards, and small-scale apps very well. For enterprise apps with admin, ORM, and complex form handling, Django is more appropriate. For modern type-hinted async APIs, FastAPI fits better.

Best Practices #

Application Factory #

Always use the application factory pattern. Separate app instance creation from configuration. Makes testing and multiple configs easy.

Split Config per Environment #

Use class-based config: BaseConfig, DevelopmentConfig, ProductionConfig, TestingConfig. Choose via the FLASK_ENV environment variable.

Use SQLAlchemy 2.0 Style #

SQLAlchemy 2.0 introduced the select() query style, which is more Pythonic, more type-safe, and easier to debug.

Service Layer #

Separate business logic from route handlers. The service layer handles validation and orchestration. Handlers only receive requests, call services, and return responses.

Healthchecks with DB Pings #

/healthz must check dependencies (database, cache). Return 503 when a dependency is down. /readyz checks whether the app is ready for requests.

Use Flask-Migrate for Schemas #

Don’t db.create_all() in production. Use Flask-Migrate (Alembic) for versioned schema migrations.

Bind-Mount Source Code in Dev #

Mount source code into the container. Flask debug mode auto-reloads on every save.

Use Gunicorn in Production #

The Flask development server is not for production. Use Gunicorn with appropriate workers (usually 2-4 × CPU cores).

Celery for Background Tasks #

Don’t process emails, reports, or heavy tasks in request handlers. Use Celery + Redis. The ContextTask pattern gives tasks access to the Flask app_context.

Troubleshooting #

Port 5000 Already in Use #

On macOS Catalina+, port 5000 is used by the AirPlay Receiver. Disable it in System Preferences or change the Flask port.

lsof -i :5000
# or disable the AirPlay Receiver
sudo killall AirPlayUIAgent

Import Errors After Adding Libraries #

Rebuild the image:

docker compose build api
docker compose up -d

Database Connection Refused #

Use depends_on: condition: service_healthy. Add retry logic or an entrypoint script.

Auto-Reload Not Working #

Make sure FLASK_DEBUG=1 is set. Check the logs for Python errors — sometimes an import error prevents the reload.

Werkzeug vs Production Servers #

Werkzeug (the Flask dev server) is only for development. For production, always use Gunicorn or uWSGI.

Summary #

  • Flask is ideal for API microservices and small-to-mid-scale apps needing flexibility.
  • The application factory is the idiomatic Flask pattern — separate app instance creation from configuration, making testing easier.
  • Split config per environment: BaseConfig, DevelopmentConfig, ProductionConfig, TestingConfig. Choose via FLASK_ENV.
  • Multi-stage Dockerfiles for production: a builder installs dependencies, a slim runtime with Gunicorn.
  • Dockerfile.dev for development: full Python + tools, auto-reload enabled.
  • The service layer separates business logic from route handlers. Services validate, handlers orchestrate.
  • SQLAlchemy 2.0 style queries: select(), session.execute(). More type-safe and Pythonic.
  • Flask-Migrate for schema migrations. Generate in dev, apply in prod, always commit.
  • Healthchecks check dependencies. Return 503 when the database is down.
  • JWT for authentication: flask-jwt-extended with access + refresh tokens. The @jwt_required() decorator.
  • Celery for background tasks: with a Redis broker, the Flask app context is injected via ContextTask. Periodic tasks via beat_schedule.
  • Bind-mount source code in dev. Auto-reload enabled.
  • Use Gunicorn in production with appropriate workers. The Flask dev server is not for production.
  • Best practices: application factory, split configs, service layer, healthchecks, migrations, Gunicorn.
  • Alternatives: Django for a full framework, FastAPI for modern type-hinted APIs.
  • Use Flask if you need flexibility and API microservices. Avoid it if you need an out-of-the-box ORM/admin (use Django).

← Previous: Django   Next: fasthttp →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact