Flask na produkcji — Gunicorn, Nginx i systemd na VPS
Opublikowano: 10 kwietnia 2026 · Kategoria: VPS
Flask to mikroframework — elegancki, minimalistyczny, bez narzucanej struktury. To jego siła i słabość. W produkcji sam Flask nie wystarcza: potrzebujesz Gunicorna jako serwera WSGI, systemd do zarządzania procesem i Nginx jako reverse proxy z obsługą plików statycznych. Ten artykuł pokazuje kompletną konfigurację od application factory pattern, przez Gunicorn workers, aż po Flask-Migrate i konfigurację logowania.
Application Factory Pattern — architektura Flask
Application factory (wzorzec fabryki) to standard w Flask — zamiast tworzyć instancję
aplikacji globalnie, pakujesz ją w funkcję create_app(). Umożliwia to różne
konfiguracje dla dev/test/prod i unikasz circular imports.
# Struktura projektu
# myapp/
# ├── app/
# │ ├── __init__.py # create_app()
# │ ├── config.py # konfiguracja per env
# │ ├── models.py # SQLAlchemy modele
# │ ├── extensions.py # db, migrate, login_manager
# │ └── blueprints/
# │ ├── auth.py # Blueprint dla auth
# │ └── api.py # Blueprint dla API
# ├── migrations/ # Flask-Migrate
# ├── wsgi.py # entry point dla Gunicorn
# ├── .env # zmienne lokalne (nie commituj)
# └── requirements.txt
# app/extensions.py — singleton extensions
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
# app/__init__.py — application factory
from flask import Flask
from .extensions import db, migrate, login_manager
from .config import config_by_name
def create_app(config_name: str = 'production') -> Flask:
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(config_by_name[config_name])
# Inicjalizacja extensions
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
# Rejestracja blueprintow
from .blueprints.auth import auth_bp
from .blueprints.api import api_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(api_bp, url_prefix='/api/v1')
return app
# app/config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'change-this-in-prod'
SQLALCHEMY_TRACK_MODIFICATIONS = False
JSON_SORT_KEYS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///dev.db'
class ProductionConfig(Config):
DEBUG = False
TESTING = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 10,
'max_overflow': 20,
'pool_pre_ping': True,
}
config_by_name = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': ProductionConfig,
}
# wsgi.py — entry point dla Gunicorn
from app import create_app
app = create_app('production')
if __name__ == '__main__':
app.run() Gunicorn — konfiguracja workers
# Instalacja Gunicorn i gevent w venv pip install gunicorn gevent # gunicorn.conf.py bind = "unix:/run/flask/flask.sock" workers = 4 # (2 * CPU) + 1 worker_class = "sync" # "sync" | "gevent" | "eventlet" | "gthread" # gevent: --workers 2 --worker-class gevent --worker-connections 1000 # gthread: --workers 4 --worker-class gthread --threads 4 timeout = 30 keepalive = 5 max_requests = 1000 # restart workera po N requestach max_requests_jitter = 100 accesslog = "/var/log/flask/access.log" errorlog = "/var/log/flask/error.log" loglevel = "info" access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' # preload_app przspiesza start, ale uniemozliwia hot-reload konfiguracji preload_app = True proc_name = "flask-app" # Test lokalny gunicorn wsgi:app -c gunicorn.conf.py
Systemd service i Nginx
# /etc/systemd/system/flask-app.service
[Unit]
Description=Flask Gunicorn Application
After=network.target postgresql.service
[Service]
User=flask
Group=flask
WorkingDirectory=/srv/myapp
RuntimeDirectory=flask
EnvironmentFile=/srv/myapp/.env
ExecStart=/srv/myapp/venv/bin/gunicorn wsgi:app -c gunicorn.conf.py
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
# /etc/nginx/sites-available/flask-app
upstream flask_backend {
server unix:/run/flask/flask.sock fail_timeout=0;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Pliki statyczne Flask (katalog static/)
location /static/ {
alias /srv/myapp/app/static/;
expires 30d;
add_header Cache-Control "public";
}
location / {
proxy_pass http://flask_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
}
}
# Wlaczenie
sudo systemctl daemon-reload && sudo systemctl enable --now flask-app
sudo nginx -t && sudo systemctl reload nginx Flask-Migrate — migracje bazy danych
# Flask-Migrate jest wrapperem nad Alembic
pip install flask-migrate
# Inicjalizacja (tylko raz)
flask --app wsgi:app db init
# Generuje katalog migrations/
# Tworzenie migracji po zmianie modelu
flask --app wsgi:app db migrate -m "add user table"
# Sprawdz wygenerowany plik migrations/versions/*.py
# Aplikowanie migracji
flask --app wsgi:app db upgrade
# Wycofanie ostatniej migracji
flask --app wsgi:app db downgrade
# Historia migracji
flask --app wsgi:app db history --verbose
# app/models.py — przykladowe modele
from datetime import datetime, timezone
from .extensions import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
hashed_password = db.Column(db.String(255), nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(
db.DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False
)
def __repr__(self):
return f'<User {self.email}>' Konfiguracja logowania produkcyjnego
// app/__init__.py — logging setup (dodaj do create_app)
import logging
from logging.handlers import RotatingFileHandler
def configure_logging(app: Flask) -> None:
if not app.debug:
handler = RotatingFileHandler(
'/var/log/flask/app.log',
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5,
)
handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s %(name)s [%(filename)s:%(lineno)d] %(message)s'
))
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
# Logowanie w aplikacji
from flask import current_app
@api_bp.errorhandler(Exception)
def handle_error(error):
current_app.logger.error(f"Unhandled error: {error}", exc_info=True)
return {"error": "Internal server error"}, 500 Flask vs FastAPI — porównanie
| Cecha | Flask | FastAPI |
|---|---|---|
| Typ serwera | WSGI (sync) | ASGI (async) |
| Wydajność | Dobra (z gevent) | Bardzo dobra (natywny async) |
| Walidacja danych | WTForms / Marshmallow | Pydantic (wbudowany) |
| Dokumentacja API | Flasgger (ręczna) | Swagger auto-gen |
| Ekosystem rozszerzeń | Bogaty (Flask-Login, Admin, CORS) | Rosnący |
| Krzywa nauki | Niska (popularne tutoriale) | Niska–Średnia |
| Async support | Ograniczony | Natywny (cały framework) |