Flask #

Flask adalah microframework Python yang paling populer untuk membangun web application dan API. Dengan filosofi “minimal by design”, Flask tidak memaksakan struktur project, ORM, atau form handling. Kamu pilih sendiri: pakai SQLAlchemy atau raw query, pakai Jinja template atau React frontend, pakai Flask-Login atau JWT manual. Fleksibilitas ini yang menjadikan Flask cocok untuk API microservice, dashboard internal, atau aplikasi skala kecil-menengah.

Docker Compose melengkapi Flask dengan cara yang konsisten: aplikasi, database, dan cache berjalan sebagai container terpisah. Tidak perlu install Postgres atau Redis di host. Artikel ini membahas setup Docker Compose untuk local development Flask, dari Dockerfile untuk Python, hot reload, hingga integrasi SQLAlchemy, Celery, dan best practice.

Prasyarat #

Pastikan sudah terinstall:

  • Docker dan Docker Compose versi terbaru
  • Python 3.12+ (opsional, untuk development di host)
  • pip atau poetry

Struktur project Flask standar dengan 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

Application factory adalah pola idiom Flask yang memisahkan pembuatan instance aplikasi dari konfigurasi. Ini memudahkan testing (bikin app terpisah untuk setiap test) dan konfigurasi multi-environment.

Dockerfile untuk 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"]

Image production ramping dengan Gunicorn, non-root user.

Dockerfile untuk 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 memberi tahu Flask entry point. FLASK_DEBUG=1 mengaktifkan debug mode dan auto-reload.

.flaskenv dan Konfigurasi #

File .flaskenv (di-load otomatis oleh python-dotenv via 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:dev@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,
}

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 dengan 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}>"

Service Layer #

Pisahkan business logic dari route handler. Service layer adalah tempat validasi bisnis, transformasi data, dan orkestrasi repository.

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

API dengan Blueprint #

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 Check #

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 cek dependency — return 503 kalau database down. /readyz return 200 selalu (server hidup, siap menerima request).

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:dev@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:dev@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:

Service worker adalah Celery worker untuk background task. Diaktifkan dengan --profile with-worker.

Celery untuk Background Task #

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),  # jam 2 pagi
    },
    "send-daily-report": {
        "task": "app.tasks.send_daily_report",
        "schedule": crontab(minute=0),  # setiap jam
    },
}

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():
    # Hapus user yang tidak pernah verifikasi dalam 7 hari
    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)

Migration dengan Flask-Migrate #

# Init (sekali saja, sudah dilakukan di template project)
docker compose exec api flask db init

# Buat migration setelah ubah model
docker compose exec api flask db migrate -m "add user model"

# Apply migration
docker compose exec api flask db upgrade

# Rollback
docker compose exec api flask db downgrade

flask db adalah CLI dari flask-migrate, wrapper di atas Alembic. Ia mendeteksi perubahan model dan generate file migration.

Build dan Run #

# Build
docker compose build

# Jalankan semua service
docker compose up -d

# Jalankan dengan worker
docker compose --profile with-worker up -d

# Lihat log
docker compose logs -f api

# Shell Flask
docker compose exec api flask shell

# Lihat routes
docker compose exec api flask routes

# Stop
docker compose down

Akses:

  • 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 current user (pakai token)
curl http://localhost:5000/api/v1/users/me \
  -H "Authorization: Bearer <token>"

Testing dengan 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

Kapan Flask Tepat #

Pakai Flask jika:
  ✓ API microservice ringan
  ✓ Butuh fleksibilitas tinggi (pilih sendiri ORM, lib, dll)
  ✓ Aplikasi skala kecil-menengah
  ✓ Web app sederhana tanpa admin/ORM kompleks
  ✓ Tim suka Python idiomatic, simple

Hindari Flask jika:
  ✗ Butuh ORM dan admin out-of-the-box (pakai Django)
  ✗ Butuh type hints end-to-end (pakai FastAPI)
  ✗ Aplikasi tradisional server-rendered kompleks
  ✗ Real-time app (Flask-Flask-SocketIO terbatas)

Flask sangat cocok untuk API microservice, dashboard internal, dan aplikasi skala kecil. Untuk aplikasi enterprise dengan admin, ORM, dan form handling kompleks, Django lebih tepat. Untuk API modern dengan type hints dan async, FastAPI lebih cocok.

Best Practice #

Application Factory #

Selalu pakai application factory pattern. Pisahkan pembuatan instance app dari konfigurasi. Memudahkan testing dan multiple config.

Pisahkan Config per Environment #

Gunakan class-based config: BaseConfig, DevelopmentConfig, ProductionConfig, TestingConfig. Pilih via FLASK_ENV environment variable.

Pakai SQLAlchemy 2.0 Style #

SQLAlchemy 2.0 memperkenalkan select() style query yang lebih Pythonic. Lebih type-safe dan lebih mudah di-debug.

Service Layer #

Pisahkan business logic dari route handler. Service layer tempat validasi dan orkestrasi. Handler cuma terima request, panggil service, return response.

Healthcheck dengan DB Ping #

/healthz harus cek dependency (database, cache). Return 503 kalau dependency down. /readyz cek app siap menerima request.

Pakai Flask-Migrate untuk Schema #

Jangan db.create_all() di production. Pakai Flask-Migrate (Alembic) untuk schema migration yang versioned.

Bind Mount Source Code di Dev #

Mount source code ke container. Flask debug mode akan auto-reload setiap save.

Pakai Gunicorn di Production #

Flask development server tidak untuk production. Pakai Gunicorn dengan workers yang sesuai (umumnya 2-4 × CPU cores).

Celery untuk Background Task #

Jangan proses email, report, atau task berat di request handler. Pakai Celery + Redis. Pattern ContextTask membuat task punya akses ke Flask app_context.

Troubleshooting #

Port 5000 Sudah Dipakai #

Di macOS Catalina+, port 5000 dipakai oleh AirPlay Receiver. Nonaktifkan di System Preferences atau ubah port Flask.

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

Import Error Setelah Tambah Library #

Rebuild image:

docker compose build api
docker compose up -d

Database Connection Refused #

Pakai depends_on: condition: service_healthy. Tambahkan retry logic atau entrypoint script.

Auto-Reload Tidak Jalan #

Pastikan FLASK_DEBUG=1 di set. Cek log untuk error Python — kadang ada error import yang阻止 reload.

Werkzeug vs Production Server #

Werkzeug (Flask dev server) hanya untuk development. Untuk production, selalu pakai Gunicorn atau uWSGI.

Ringkasan #

  • Flask ideal untuk API microservice dan aplikasi skala kecil-menengah yang butuh fleksibilitas.
  • Application factory adalah pola idiom Flask — pisahkan pembuatan instance app dari konfigurasi, memudahkan testing.
  • Pisahkan config per environment: BaseConfig, DevelopmentConfig, ProductionConfig, TestingConfig. Pilih via FLASK_ENV.
  • Dockerfile multi-stage untuk production: builder install dependency, runtime ramping dengan Gunicorn.
  • Dockerfile.dev untuk development: full Python + tools, auto-reload aktif.
  • Service layer memisahkan business logic dari route handler. Service tempat validasi, handler cuma orchestrasi.
  • SQLAlchemy 2.0 style query: select(), session.execute(). Lebih type-safe dan Pythonic.
  • Flask-Migrate untuk schema migration. Generate di dev, apply di prod, selalu commit.
  • Healthcheck cek dependency. Return 503 kalau database down.
  • JWT untuk authentication: flask-jwt-extended dengan access + refresh token. @jwt_required() decorator.
  • Celery untuk background task: dengan Redis broker, Flask app context di-inject via ContextTask. Periodic task via beat_schedule.
  • Bind mount source code di dev. Auto-reload aktif.
  • Pakai Gunicorn di production dengan workers yang sesuai. Flask dev server tidak untuk production.
  • Best practice: application factory, pisahkan config, service layer, healthcheck, migration, Gunicorn.
  • Alternatif: Django untuk full framework, FastAPI untuk API modern dengan type hints.
  • Gunakan Flask jika butuh fleksibilitas dan API microservice. Hindari jika butuh ORM/admin out-of-the-box (pakai Django).

← Sebelumnya: Django   Berikutnya: fasthttp →

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