1. 启动只是一个开始,后面这些事不做等于白跑
Django项目能在本地python manage.py runserver跑起来,充其量只能算“服务活着”,离“项目能正常交付”还差了一大截。我从第一次接触Django到现在,踩过最深的坑恰恰都集中在“启动之后”这个阶段:数据库表没迁移完整、静态文件404、跨域被前端同事追着问、日志什么都没有导致线上问题排查全靠猜。
这篇文章就是把我每次新建或接手Django后端后,会老老实实做一遍的准备工作整理出来。内容覆盖环境变量拆分、数据库初始化与脏数据清理、CORS与安全中间件配置、日志体系搭建、静态/媒体文件处理、常用管理命令封装,以及若干高频启动后问题的排查思路。不管你是刚用django-admin startproject创建项目的新手,还是准备把项目推送到服务器做联调的后端开发,这一套流程都能直接照着做。
我不会讲大而全的Django教程,只聚焦“启动之后那半小时到两小时内,到底该把哪些细节补齐”,这些恰恰是文档里最分散、但实战里最要命的部分。
2. 先把环境配置和依赖管理理顺
2.1 别把敏感配置写在settings.py里
很多新手项目跑起来之后,第一件事就是把数据库密码、SECRET_KEY、第三方API Key直接写在settings.py里,图省事。一旦项目上Git、多人协作或者部署到服务器,这些敏感信息就等于公开裸奔。我处理这个问题的标准做法是从一开始就引入环境变量。
具体来说,在项目根目录建一个.env文件,配合python-decouple或者django-environ读取。以django-environ为例:
python复制import environ
env = environ.Env()
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env.bool("DJANGO_DEBUG", default=False)
DATABASES = {
"default": env.db("DATABASE_URL", default="sqlite:///db.sqlite3"),
}
对应的.env文件长这样:
dotenv复制DJANGO_SECRET_KEY=your-secret-key-here
DJANGO_DEBUG=True
DATABASE_URL=postgres://dbuser:dbpass@127.0.0.1:5432/myproject
我建议再把.env加入.gitignore,同时提供一个.env.example提交到仓库,里面只放键名、不放真实值,方便团队其他成员拉代码后快速复制配置。
2.2 用pip-tools或poetry锁定依赖版本
Django项目启动后,pip freeze > requirements.txt能跑,但生成的内容非常粗糙:依赖没有分层、版本号虽然锁住了但传递依赖完全不可控。我通常把依赖拆成requirements/base.txt、dev.txt、prod.txt三层,或者直接改用poetry管理。
用pip-tools的话,维护一个requirements.in文件写明直接依赖:
text复制django>=4.2,<5.0
djangorestframework
django-cors-headers
psycopg2-binary
python-dotenv
然后执行pip-compile requirements.in -o requirements.txt,自动解析出完整的锁定版本列表,包含所有传递依赖。这样换机器部署时,pip install -r requirements.txt出来的环境几乎一模一样,而不是“我本地能跑,你那边报错”。
注意:
.venv虚拟环境目录一定不要提交到Git。如果项目已经犯过这个错,启动后记得先清理:git rm -r --cached .venv,再补上.gitignore规则。
2.3 起项目前先确认Python与Django版本兼容性
Django 4.2 LTS要求Python 3.8及以上,Django 5.0则要求Python 3.10以上。启动项目后第一件事,别急着写业务代码,先用下面的命令核对:
bash复制python --version
python -m django --version
这两行能省掉很多莫名其妙的报错。比如Python 3.6环境里强行装Django 4.2,pip会自动升级失败或者装上之后runserver直接抛语法错误,因为高版本Django用到了新语法特性。
3. 数据库准备:不只是跑个migrate那么简单
3.1 迁移前先检查数据库连接与权限
启动后跑python manage.py migrate,结果报connection refused或者permission denied,这种问题我见过太多次了。排查顺序一般是这样:
- 确认数据库服务已启动,PostgreSQL可以用
pg_isready -h 127.0.0.1 -p 5432检测。 - 确认数据库账号对目标库有建表权限:
GRANT ALL PRIVILEGES ON DATABASE myproject TO dbuser; - 确认DATABASE_URL里的host、port、用户名密码都没写错。
如果是MySQL,还要额外确认字符集设置,建议在创建库时就指定:
sql复制CREATE DATABASE myproject CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
否则后面会出现中文乱码或者emoji存储失败的问题。
3.2 迁移执行与反向检查
执行迁移的正确姿势不是只敲一次migrate就完事,尤其当项目由多个app组成、模型定义分散时,迁移文件之间有依赖关系。我一般按顺序做:
bash复制python manage.py makemigrations --check --dry-run
python manage.py migrate --plan
python manage.py migrate
makemigrations --check --dry-run用来检测是否有未生成的迁移,如果有输出,说明模型和迁移文件不同步,通常是有人改了模型但没生成迁移文件,这种状态最危险。migrate --plan会在真正执行前列出将要应用哪些迁移,我可以肉眼判断是否合理。执行完后,再用python manage.py showmigrations核对每个迁移有没有打上[X]标记。
3.3 初始化数据与超级用户创建
数据库空表对开发调试非常不友好。我常用的两种方式:
- 通过fixture加载基础数据:
python manage.py loaddata initial_data.json,适合字典表、权限配置这类固定数据。 - 通过
python manage.py shell批量创建,适合需要关联逻辑的初始化数据。
创建超级用户也别只用交互式createsuperuser,尤其在一个自动化脚本里,可以直接用环境变量方式非交互创建:
bash复制DJANGO_SUPERUSER_PASSWORD=admin123 python manage.py createsuperuser \
--username=admin \
--email=admin@example.com \
--noinput
3.4 排除数据库连接池相关隐患
Django默认每处理一个请求就新建一个数据库连接,请求结束就关闭。本地跑没问题,但并发上来后,数据库端会频繁出现“创建/销毁连接”的系统开销。我建议启动后立即给项目装上连接池。
最省事的方式是直接用django-db-connection-pool这个库,基于DBUtils实现。以PostgreSQL为例:
python复制DATABASES = {
"default": {
"ENGINE": "django_db_connection_pool.pooled_psycopg",
"NAME": "myproject",
"USER": "dbuser",
"PASSWORD": "dbpass",
"HOST": "127.0.0.1",
"PORT": "5432",
"CONN_MAX_AGE": 60,
"POOL_OPTIONS": {
"POOL_SIZE": 10,
"MAX_OVERFLOW": 10,
"RECYCLE": 3600,
},
}
}
CONN_MAX_AGE设为60秒,意味着同一进程内的请求会复用这个连接,而不是每次新建。POOL_SIZE根据后端并发量调整,一般10到20够用。要注意RECYCLE参数,MySQL默认8小时断开空闲连接,如果不定期回收,会出现“连接被服务器关闭”的报错。
4. 中间件、CORS与安全配置不能跳过
4.1 跨域问题为什么一定会在启动后爆发
前后端分离项目里,Django后端跑在127.0.0.1:8000,前端Vue或React跑在127.0.0.1:8080,浏览器会拦截跨域请求。启动后端后不配CORS,前端联调第一件事就是报CORS policy错误。
Django配套的django-cors-headers库是标准方案。安装后,在settings.py里这样配置:
bash复制pip install django-cors-headers
python复制INSTALLED_APPS = [
...
"corsheaders",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
...
]
CORS_ALLOWED_ORIGINS = [
"http://127.0.0.1:8080",
"http://localhost:8080",
]
CorsMiddleware要尽量放在中间件列表靠前位置,一般是放在CommonMiddleware之前。这个顺序很关键,因为中间件执行顺序是从上到下,CORS响应头需要在进入视图函数之前就准备好。
4.2 CORS配置里的三个细节
第一,生产环境的CORS_ALLOWED_ORIGINS不要用CORS_ALLOW_ALL_ORIGINS=True,除非你的接口是对外完全公开的API。用CORS_ALLOW_ALL_ORIGINS配合CORS_ALLOW_CREDENTIALS=True浏览器会直接拒绝,因为两者不能同时使用,这是个容易掉进去的坑。
第二,需要携带Cookie跨域时,前端axios要设置withCredentials: true,后端对应要加CORS_ALLOW_CREDENTIALS = True,而且CORS_ALLOWED_ORIGINS里不能出现*,必须写完整域名。
第三,非简单请求会触发预检OPTIONS请求,Django路由里不用专门处理,corsheaders中间件会直接拦截并返回适当的响应头。
4.3 安全中间件清单速查
Django自带的django.middleware.security.SecurityMiddleware提供了一些基础安全头,但启动后我还会检查这几项是否配置到位:
| 配置项 | 生产环境建议值 | 说明 |
|---|---|---|
SECURE_SSL_REDIRECT |
True |
HTTP请求重定向到HTTPS |
SECURE_HSTS_SECONDS |
31536000 |
强制浏览器使用HTTPS访问 |
SECURE_CONTENT_TYPE_NOSNIFF |
True |
防止MIME类型嗅探攻击 |
SECURE_BROWSER_XSS_FILTER |
True |
启用浏览器XSS过滤 |
CSRF_COOKIE_SECURE |
True |
CSRF Cookie仅通过HTTPS传输 |
SESSION_COOKIE_SECURE |
True |
Session Cookie仅通过HTTPS传输 |
X_FRAME_OPTIONS |
'DENY' |
禁止页面被iframe嵌套 |
这些开关在本地开发时可以全关,但服务器部署环境必须逐个核对。我见过太多项目在开发环境跑得好好的,一部署到生产环境就出现登录失效或者表单提交403,排查到最后发现是CSRF相关配置没跟上协议升级。
4.4 ALLOWED_HOSTS的配置陷阱
启动后遇到DisallowedHost报错,多半是ALLOWED_HOSTS为空或者没加当前域名。开发环境用ALLOWED_HOSTS = ["*"]图方便没问题,但生产环境一定要列出确切域名:
python复制ALLOWED_HOSTS = ["api.example.com", "www.example.com"]
如果用了DEBUG=True,Django会自动允许localhost和127.0.0.1。部署后必须把DEBUG关掉,不然ALLOWED_HOSTS为空时会直接拒绝所有请求,这是Django版本升级后的安全行为,很多人第一次部署时被这个搞懵。
5. 日志体系:现在搭好,出事时不慌
5.1 Django自带logging配置该怎么改
启动后最容易被忽略的准备工作就是日志。默认情况下Django只在控制台输出请求日志,文件里什么都没有。线上一旦出了问题,既没有请求记录也没有异常堆栈,排查完全靠猜。
我常用的一个基础日志配置长这样:
python复制LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
"simple": {
"format": "{levelname} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
"file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": os.path.join(BASE_DIR, "logs", "django.log"),
"when": "midnight",
"backupCount": 7,
"formatter": "verbose",
},
"error_file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": os.path.join(BASE_DIR, "logs", "error.log"),
"when": "midnight",
"backupCount": 14,
"formatter": "verbose",
"level": "ERROR",
},
},
"root": {
"handlers": ["console", "file"],
"level": "INFO",
},
"loggers": {
"django": {
"handlers": ["console", "file", "error_file"],
"level": "INFO",
"propagate": False,
},
"django.request": {
"handlers": ["error_file"],
"level": "ERROR",
"propagate": False,
},
"myproject": {
"handlers": ["console", "file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
},
}
TimedRotatingFileHandler按天切分日志文件,保留最近7天的普通日志和14天的错误日志,避免单个日志文件无限膨胀。业务日志统一用myproject这个logger,代码里直接logger = logging.getLogger("myproject")就能复用这套配置。
5.2 SQL日志与慢查询排查
启动后联调阶段,最痛苦的是接口返回慢但不知道卡在哪儿。这时可以临时打开数据库查询日志:
python复制LOGGING["loggers"]["django.db.backends"] = {
"handlers": ["console"],
"level": "DEBUG",
"propagate": False,
}
开启后控制台会打印每一条SQL语句及执行耗时。但我强烈建议只在调试时打开,生产环境开启会把SQL参数都打出来,存在敏感信息泄露风险,而且日志量爆炸。
定位慢查询更优雅的方式是接django-silk或者django-debug-toolbar。django-debug-toolbar在本地开发足够,django-silk则能在接口级别记录每一分钟耗时、SQL条数和具体语句,部署环境配合权限控制也能用。
5.3 自定义日志字段:加一个request_id
多用户并发环境下,光看日志很难把一次请求涉及的多条记录串起来。我一般会在中间件里生成一个request_id,塞进日志上下文:
python复制import uuid
import logging
class RequestIDMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.request_id = request_id
with logging_context(request_id=request_id):
response = self.get_response(request)
response["X-Request-ID"] = request_id
return response
配合python-json-logger把日志输出为JSON格式,排查问题时直接按request_id过滤,效率高很多。这一步属于锦上添花,不过一旦遇到“用户说请求失败了,但我找不到对应日志”的情况,你就会明白它有多值。
6. 静态文件与媒体文件,启动后就得想清楚
6.1 collectstatic不能等到部署才学
开发环境里,Django能自动伺服静态文件,但那只适用于DEBUG=True。一旦准备联调或部署,执行:
bash复制python manage.py collectstatic --noinput
这个命令会把所有app里的静态文件收集到STATIC_ROOT指定的目录。常见的坑有两个:一是STATIC_ROOT和STATICFILES_DIRS不能指向同一个目录,否则会报错;二是文件夹里旧文件不会被清除,--clear参数可以解决,但注意别在服务器上误删新上传的内容。
6.2 使用WhiteNoise伺服静态文件
生产环境不推荐用Nginx直接代理Django静态文件,因为Django的FileSystemFinder和AppDirectoriesFinder会扫描多个目录,Nginx配置起来啰嗦。更省心的方案是whitenoise,它能让Django自己高效伺服静态文件,完全不需要额外配置Web服务器静态目录。
bash复制pip install whitenoise
然后在MIDDLEWARE里、SecurityMiddleware之后加一行:
python复制"whitenoise.middleware.WhiteNoiseMiddleware",
再配合压缩:
python复制STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
这样静态文件会自动加内容哈希后缀,浏览器缓存失效问题也顺带解决了。
6.3 媒体文件目录与用户上传文件
使用ImageField或FileField时,MEDIA_ROOT和MEDIA_URL必须配置妥当。我见过有人把上传目录放在项目源码目录里,最后部署时和代码混在一起,迁移服务器时一个没注意就丢了所有用户图片。
我建议把媒体目录放到项目之外的独立位置,比如/var/www/myproject/media,或者直接用对象存储。如果暂时用本地存储,settings里这样配:
python复制MEDIA_URL = "/media/"
MEDIA_ROOT = "/var/www/myproject/media"
开发模式下需要伺服媒体文件,在urls.py里加:
python复制from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
这段代码只应在开发环境生效,生产环境要交给Nginx或对象存储来伺服媒体文件。
7. 管理命令封装,把重复工作变成一条命令
7.1 为什么需要自定义management command
启动后准备基础数据,如果每次都打开shell手动敲代码,既容易遗漏操作步骤,也没法保证团队其他人拿到的是同样的环境。自定义管理命令就是解决这个问题的:把初始化流程固化成代码,任何人执行同一条命令,得到相同结果。
我通常在项目里建一个core或者common的app,专门放这类命令:
bash复制python manage.py startapp core
目录结构:
text复制core/
management/
__init__.py
commands/
__init__.py
init_env.py
7.2 一个初始化命令的完整示例
下面是init_env.py的一个简化版本,功能是同步数据库、创建超级用户、加载基础字典数据:
python复制from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from django.core.management import call_command
class Command(BaseCommand):
help = "Initialize project environment after startup"
def add_arguments(self, parser):
parser.add_argument("--username", default="admin")
parser.add_argument("--email", default="admin@example.com")
parser.add_argument("--password", default="")
def handle(self, *args, **options):
self.stdout.write(self.style.NOTICE("Running migrations..."))
call_command("migrate")
username = options["username"]
email = options["email"]
password = options["password"]
if not User.objects.filter(username=username).exists():
if not password:
password = input("Superuser password: ")
User.objects.create_superuser(username, email, password)
self.stdout.write(self.style.SUCCESS(f"Superuser {username} created"))
else:
self.stdout.write(self.style.WARNING(f"Superuser {username} already exists"))
self.stdout.write(self.style.NOTICE("Loading fixture data..."))
call_command("loaddata", "initial_data.json")
self.stdout.write(self.style.SUCCESS("Environment initialization complete"))
以后启动项目只需一条命令:
bash复制python manage.py init_env --username=admin --password=xxx
团队成员不管谁拉下代码,执行这条命令,环境就绪状态完全一致。
7.3 定时任务也能塞进管理命令
Django项目跑起来后,往往需要定期执行清理过期Session、发送提醒邮件、扫描超时订单这类任务。用系统Cron直接跑Django脚本容易遇到环境变量和虚拟环境路径问题,更干净的方式是写一个管理命令,然后在服务器crontab里调用:
bash复制*/30 * * * * cd /path/to/project && /path/to/venv/bin/python manage.py clean_expired_sessions
只要管理命令写好,Cron怎么配都行。具体命令内部逻辑就是把模型操作封装好,配合BaseCommand的add_arguments和handle方法即可。
8. 启动后常见问题与排查实录
8.1 runserver可以启动但页面404/500
runserver能启动,访问首页却404,最常见原因是ROOT_URLCONF配置指向了不存在的urls模块,或者项目里根本没有配置根路由。排查步骤:
- 检查settings.py里的
ROOT_URLCONF值是否正确指向项目名.urls。 - 检查urls.py里有没有配置
path("", include(...))。 - 看控制台输出的请求日志,确认请求确实进了Django。
500错误则要先分清楚是模板错误还是视图异常。最快捷的方式是设置DEBUG=True看详细报错页,或者查看日志文件里有没有完整的异常堆栈。我习惯先看error.log,因为报错页在部分场景下会被中间件吞掉,但日志一定会记录。
8.2 静态文件样式全丢
runserver下面页面能打开,但CSS、JS全部404,十有八九是STATIC_URL配置错误,或者模板里用了{% load static %}但实际文件不在预期目录。检查顺序:
STATIC_URL是否以/结尾,正确写法是"/static/"- 各app下的
static目录是否和app同名嵌套,比如app/static/app/css/style.css - 模板里
{% static 'app/css/style.css' %}路径是否和实际物理路径一致
8.3 CSRF验证失败,POST请求全被拒
启动后写注册接口,前端POST请求报CSRF verification failed。开发调试阶段,我的处理方案是:
- 对不需要CSRF保护的API视图,加
@csrf_exempt装饰器。 - 如果是前后端分离且用Token认证,在Django REST Framework里配置
SessionAuthentication之外的TokenAuthentication,然后全局禁用Session的CSRF。 - 不能一股脑全跳过CSRF,Django表单类应用仍然需要它。
这里想提醒的是,很多人图省事直接注释掉CsrfViewMiddleware,这个操作有安全风险,不建议在生产环境这么做。
8.4 数据库连接被重置
项目在后端运行过程中,如果时常出现MySQL server has gone away或connection already closed,大概率是连接空闲超时被数据库服务端断开。解决办法:
- 设置
CONN_MAX_AGE时注意加CONN_HEALTH_CHECKS = True,这会让Django在向数据库请求前先检查连接存活状态。 django-db-connection-pool的RECYCLE参数设置成小于数据库wait_timeout的值,比如MySQL默认8小时,那RECYCLE设成3600秒比较保险。- 检查数据库服务端的
max_connections是否打满,用SHOW PROCESSLIST查看。
8.5 启动时提示端口被占用
Error: That port is already in use是开发期高频报错。我一般这样处理:
bash复制lsof -i :8000
kill -9 <PID>
如果多次出现端口占用,建议把默认端口改掉,比如python manage.py runserver 0.0.0.0:8080,这样还能方便局域网内用手机测试页面,不用额外配Nginx反向代理。
还有一个情况:不是端口被占用而是没有权限,python manage.py runserver 80会提示权限不足,需要sudo或以root身份运行。本地开发为了这个用sudo没必要,直接用8000端口就完事了。
8.6 管理命令不生效
代码文件放在management/commands/目录下,python manage.py help却看不到新命令,通常是两个问题:
management/commands/目录里缺少__init__.py文件,Django遍历不到。- 命令所在的app没有加入
INSTALLED_APPS。
这两点只要有一个遗漏,命令就会被静默忽略,没有任何报错提示。排查时先确认app在INSTALLED_APPS里,再看目录结构是否和Django官方文档约定的完全一致。
9. 联调前最后的检查清单
后端启动后的准备工作,说到底是为了让前后端联调更顺畅、让后续部署不返工。我每次在启动项目后,都会按下面这张清单逐项过一遍:
| 检查项 | 操作方式 | 通过标准 |
|---|---|---|
| 环境变量配置 | python manage.py check |
无敏感信息硬编码 |
| 数据库迁移 | python manage.py migrate --plan |
所有迁移已应用 |
| 跨域配置 | 前端跨端口访问接口 | 响应头含Access-Control-Allow-Origin |
| 安全中间件 | python manage.py check --deploy |
核心告警项已处理 |
| 日志落盘 | 发起一次错误请求 | error.log出现对应记录 |
| 静态文件 | python manage.py collectstatic --dry-run --noinput |
无文件收集报错 |
| 超级用户 | python manage.py createsuperuser |
能登录admin后台 |
| 管理命令 | 执行init_env |
可重复执行不报错 |
| 数据库连接池 | 并发压测接口 | 数据库连接数稳定不飙升 |
python manage.py check --deploy这条命令强烈建议执行一下,它会列出所有生产环境相关的安全建议,虽然有些条目是“建议级别”而不是强制错误,但花五分钟看完并逐条确认,能规避掉很大一部分上线后才会爆发的隐患。
我个人的经验是,把这张清单固化成一个Shell脚本或者Makefile,每次新起项目时直接跑一遍,省下的时间远超配置这些内容的时间。毕竟真正开发业务功能的时间很宝贵,不应该浪费在一个接一个的环境配置坑里。
