Python 应用部署 #

Flask 应用部署 #

项目结构 #

text
myapp/
├── app/
│   ├── __init__.py
│   ├── routes.py
│   ├── models.py
│   └── config.py
├── migrations/
├── tests/
│   └── test_app.py
├── requirements.txt
├── Procfile
├── runtime.txt
├── .env.example
└── app.json

requirements.txt #

text
Flask==3.0.0
gunicorn==21.2.0
psycopg2-binary==2.9.9
redis==5.0.1
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.5
python-dotenv==1.0.0
flask-cors==4.0.0

runtime.txt #

text
python-3.12.0

应用代码 #

python
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import redis
import os

db = SQLAlchemy()
redis_client = None

def create_app():
    app = Flask(__name__)
    
    app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev')
    app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    
    db.init_app(app)
    CORS(app)
    
    global redis_client
    redis_url = os.environ.get('REDIS_URL')
    if redis_url:
        redis_client = redis.from_url(
            redis_url,
            ssl_cert_reqs=None
        )
    
    from app.routes import bp
    app.register_blueprint(bp)
    
    @app.route('/health')
    def health():
        return {'status': 'ok', 'timestamp': datetime.utcnow().isoformat()}
    
    return app
python
# app/routes.py
from flask import Blueprint, jsonify, request
from app import db
from app.models import User

bp = Blueprint('routes', __name__)

@bp.route('/')
def index():
    return jsonify({
        'message': 'Welcome to My App',
        'version': '1.0.0'
    })

@bp.route('/users', methods=['GET'])
def get_users():
    users = User.query.order_by(User.created_at.desc()).all()
    return jsonify([u.to_dict() for u in users])

@bp.route('/users/<int:id>', methods=['GET'])
def get_user(id):
    user = User.query.get_or_404(id)
    return jsonify(user.to_dict())

@bp.route('/users', methods=['POST'])
def create_user():
    data = request.get_json()
    
    if not data.get('email'):
        return jsonify({'error': 'Email is required'}), 400
    
    user = User(email=data['email'], name=data.get('name'))
    db.session.add(user)
    
    try:
        db.session.commit()
        return jsonify(user.to_dict()), 201
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': 'Email already exists'}), 409
python
# app/models.py
from datetime import datetime
from app 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)
    name = db.Column(db.String(255))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'email': self.email,
            'name': self.name,
            'created_at': self.created_at.isoformat()
        }

Procfile #

text
web: gunicorn app:create_app\(\)
release: flask db upgrade

app.json #

json
{
  "name": "myapp",
  "description": "Python Flask app on Heroku",
  "buildpacks": [
    {
      "name": "heroku/python"
    }
  ],
  "env": {
    "SECRET_KEY": {
      "generator": "secret"
    }
  },
  "addons": [
    {
      "plan": "heroku-postgresql:mini"
    },
    {
      "plan": "heroku-redis:mini"
    }
  ]
}

部署 Flask 应用 #

bash
# 创建应用
heroku create myapp

# 添加数据库
heroku addons:create heroku-postgresql:mini
heroku addons:create heroku-redis:mini

# 设置环境变量
heroku config:set SECRET_KEY=$(openssl rand -hex 32)
heroku config:set FLASK_APP=app

# 部署
git push heroku main

# 初始化数据库
heroku run flask db init
heroku run flask db migrate -m "Initial migration"
heroku run flask db upgrade

Django 应用部署 #

项目结构 #

text
myproject/
├── myproject/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── myapp/
│   ├── __init__.py
│   ├── models.py
│   ├── views.py
│   └── urls.py
├── manage.py
├── requirements.txt
├── Procfile
├── runtime.txt
└── static/

requirements.txt #

text
Django==5.0
gunicorn==21.2.0
psycopg2-binary==2.9.9
django-heroku==0.3.1
whitenoise==6.6.0
redis==5.0.1
django-redis==5.4.0

settings.py #

python
import os
import django_heroku

SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key')

DEBUG = os.environ.get('DEBUG', 'False') == 'True'

ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DATABASE_URL'),
    }
}

if os.environ.get('DATABASE_URL'):
    import dj_database_url
    DATABASES['default'] = dj_database_url.parse(os.environ.get('DATABASE_URL'))

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': os.environ.get('REDIS_URL', 'redis://localhost:6379/1'),
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

django_heroku.settings(locals())

Procfile #

text
web: gunicorn myproject.wsgi
release: python manage.py migrate

部署 Django 应用 #

bash
# 创建应用
heroku create myproject

# 添加数据库
heroku addons:create heroku-postgresql:mini
heroku addons:create heroku-redis:mini

# 设置环境变量
heroku config:set SECRET_KEY=$(openssl rand -hex 32)
heroku config:set DEBUG=False

# 部署
git push heroku main

# 创建超级用户
heroku run python manage.py createsuperuser

# 收集静态文件
heroku run python manage.py collectstatic --noinput

测试 #

Flask 测试 #

python
# tests/test_app.py
import pytest
from app import create_app, db

@pytest.fixture
def app():
    app = create_app()
    app.config['TESTING'] = True
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

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

def test_health(client):
    response = client.get('/health')
    assert response.status_code == 200
    assert response.json['status'] == 'ok'

def test_index(client):
    response = client.get('/')
    assert response.status_code == 200
    assert 'message' in response.json

def test_create_user(client):
    response = client.post('/users', json={
        'email': 'test@example.com',
        'name': 'Test User'
    })
    assert response.status_code == 201
    assert response.json['email'] == 'test@example.com'

Django 测试 #

python
# myapp/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import User

class UserModelTest(TestCase):
    def test_create_user(self):
        user = User.objects.create(
            email='test@example.com',
            name='Test User'
        )
        self.assertEqual(user.email, 'test@example.com')

class UserViewTest(TestCase):
    def test_user_list(self):
        response = self.client.get(reverse('user-list'))
        self.assertEqual(response.status_code, 200)

最佳实践 #

1. 环境变量管理 #

python
# app/config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY')
    DATABASE_URL = os.environ.get('DATABASE_URL')
    REDIS_URL = os.environ.get('REDIS_URL')
    
    @classmethod
    def validate(cls):
        required = ['SECRET_KEY', 'DATABASE_URL']
        missing = [k for k in required if not getattr(cls, k)]
        if missing:
            raise ValueError(f"Missing required config: {missing}")

2. 日志配置 #

python
import logging
import sys

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(levelname)s %(name)s %(message)s',
    handlers=[logging.StreamHandler(sys.stdout)]
)

logger = logging.getLogger(__name__)

3. 错误处理 #

python
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Not found'}), 404

@app.errorhandler(500)
def internal_error(error):
    return jsonify({'error': 'Internal server error'}), 500

下一步 #

Python 应用部署完成后,接下来学习 全栈应用部署 了解前后端分离部署!

最后更新:2026-03-28