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 viaFLASK_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-extendeddengan access + refresh token.@jwt_required()decorator.- Celery untuk background task: dengan Redis broker, Flask app context di-inject via
ContextTask. Periodic task viabeat_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).