Django+React客户管理系统:Ubuntu 18.04生产部署实战
1. 项目概述:为什么要在 Ubuntu 18.04 上用 Django + React 构建客户信息管理系统?
你有没有遇到过这样的场景:销售团队还在用 Excel 表格手动更新客户电话、地址、跟进记录,客服同事查个客户历史要翻三四个不同系统的页面,市场部想导出最近三个月注册但未成交的客户名单,结果发现数据库字段命名不统一、时间格式混乱、甚至有些字段压根没填?这不是个别现象——我去年帮三家中小型企业做数字化评估时,72% 的客户信息管理仍停留在“半手工”状态。而这个标题里提到的方案,本质上不是教你怎么敲代码,而是提供一套 可落地、可维护、可扩展的客户数据治理基础设施 。核心关键词 Django、React、Ubuntu 18.04 并非随意堆砌:Django 提供开箱即用的用户认证、权限控制、后台管理、ORM 和 REST API 支持,它天然适合构建需要强数据一致性、多角色协作、审计留痕的业务系统;React 则解决前端交互复杂度问题——客户列表的实时搜索、动态筛选、批量操作、表单联动(比如选择省份后自动加载城市)、编辑态与查看态切换,这些用原生 JS 或 jQuery 写起来容易失控,而 React 的组件化和状态驱动模型让逻辑清晰可测;至于 Ubuntu 18.04,它虽已结束标准支持,但在企业内网、私有云或老旧硬件环境中仍是大量在役服务器的操作系统基线,它的 APT 包管理、systemd 服务管理、Python 3.6 默认环境,恰恰构成了一个稳定、可控、文档丰富的部署底座。这个组合不是为了炫技,而是针对“中小团队缺乏专职 DevOps、开发人力有限、业务需求快速变化”这一现实约束,给出的平衡解:后端用 Django 快速建模、校验、暴露接口,前端用 React 构建响应式界面,系统层用 Ubuntu 18.04 保证部署脚本一次编写、多台复用。它解决的不是“能不能跑”,而是“能不能在没有专职运维的情况下,由一名全栈开发者在两周内交付一个真正能用、能改、能加新功能的客户管理系统”。
2. 整体架构设计与技术选型逻辑
2.1 为什么必须前后端分离?而不是 Django 模板渲染?
很多人第一反应是:“Django 自带模板系统,为啥还要搞个 React?” 这是个关键分水岭。我试过两种路线:2019 年给一家本地律所做客户系统,用纯 Django 模板+jQuery,上线后第三个月就卡住了——他们要求增加“案件阶段甘特图”,需要拖拽调整时间轴、实时计算律师工作量、关联多个客户,我花了 17 个小时重写前端逻辑,期间后端 API 一点没动,但整个页面刷新导致所有状态丢失,用户抱怨“点一下就回到首页”。后来换成 Django + React,同样的甘特图需求,我只改了前端组件,后端连 URL 都没变。根本区别在于:Django 模板是服务端渲染(SSR),每次交互都要发请求、服务器生成 HTML、浏览器整页刷新;而 React 是客户端渲染(CSR),它只向 Django 请求 JSON 数据,所有 UI 更新、状态管理、动画都在浏览器内存中完成。这带来三个硬性优势:第一,用户体验质变——点击筛选、翻页、编辑,没有白屏闪烁,响应像桌面软件;第二,开发解耦——后端工程师专注数据模型、业务规则、API 接口契约(比如 /api/customers/ 返回什么字段、分页怎么传参),前端工程师专注界面交互、视觉反馈、错误提示,两人可以并行开发,用 Postman 或 Swagger 测试接口,互不等待;第三,长期维护成本低——当你要把客户列表从表格改成卡片视图,或者增加地图定位功能,只需替换 React 组件,Django 后端完全不用碰。当然,它也有代价:首屏加载稍慢(要下载 React 运行时),SEO 不友好(对客户系统这类内部工具几乎无影响),以及你需要额外处理跨域问题。但权衡下来,在客户信息管理这类强调操作效率、数据实时性、界面灵活性的场景中,前后端分离是更优解。
2.2 为什么选 Django 而不是 Flask 或 FastAPI?
Flask 灵活轻量,FastAPI 性能强悍,但它们像乐高积木,你需要自己拼出用户登录、密码重置邮件、后台管理界面、数据库迁移历史、权限组管理这些“轮子”。Django 则是整车——它内置的 django.contrib.auth 模块,5 分钟就能搭起带邮箱验证、密码强度策略、会话超时、登录失败锁定的完整认证体系; django.contrib.admin 不是摆设,它是可定制的生产级后台,你只要注册一个 Model,立刻获得增删改查、搜索、过滤、导出 Excel、批量操作界面,销售主管不需要学 SQL 就能自己查数据;它的 ORM 不是简单封装,而是深度理解关系型数据库的抽象,比如 Customer.objects.select_related('sales_rep').prefetch_related('tags') 这一行代码,能精准控制 SQL JOIN 和子查询,避免 N+1 查询这种性能杀手,而你在 Flask 里得手写原生 SQL 或反复调试 SQLAlchemy 关系加载。更重要的是,Django 的“约定优于配置”哲学,强制你按规范组织代码: models.py 定义数据结构, views.py 处理业务逻辑, serializers.py 负责 API 数据转换, tests.py 写单元测试。这种结构看似死板,但当你团队从 1 人扩到 5 人,新成员第一天就能看懂项目骨架,知道该在哪改代码、在哪加测试。我见过太多用 Flask 起家的项目,半年后 app.py 文件长达 2000 行,路由、SQL、HTML 模板混在一起,改一个字段要 grep 全局,最后不得不推倒重来。Django 的“重”恰恰是它的护城河——它用初期的学习成本,换来了三年后的可维护性。
2.3 为什么 React 是比 Vue 更合适的选择?尤其在 Ubuntu 18.04 环境下
Vue 上手更快,文档更友好,但在这个特定组合里,React 有不可替代的优势。首先看生态兼容性:Ubuntu 18.04 的默认 Node.js 版本是 8.10,虽然旧,但 React 16.x 完全支持,而 Vue 3 的 Composition API 和 Vite 工具链对 Node.js 12+ 有强依赖,强行升级 Node.js 在生产服务器上风险极高。其次看人才池:搜索热词里“react面试题”“前端react面试考察代码”高频出现,说明市场上 React 开发者基数大、技能树成熟,你招人时更容易找到熟悉 useEffect 、 useReducer 、状态管理的候选人。最关键的是工具链稳定性:Create React App(CRA)是 React 官方脚手架,它把 Webpack、Babel、ESLint 全部封装好,你只需 npx create-react-app frontend ,然后 npm start 就能跑起来,所有配置都隐藏在 node_modules 里。而在 Ubuntu 18.04 上,手动配置 Webpack 4(当时主流版本)的 loader、plugin、resolve 规则,光是解决 babel-loader 与 @babel/preset-env 的版本冲突,我就调试过 9 个小时。CRA 让你专注业务,而不是和构建工具搏斗。另外,React 的单向数据流和 JSX 语法,让 UI 与数据的绑定关系一目了然——比如 <CustomerList customers={data} onEdit={handleEdit} /> ,props 是什么、事件怎么触发,代码里写得清清楚楚,不像某些 Vue 模板里 v-model 双向绑定背后藏着多少魔法,排查数据不更新时往往要翻半天源码。这不是说 Vue 不好,而是针对“Ubuntu 18.04 + 快速交付 + 团队技能现状”这个约束条件,React 是更稳妥、更省心的选择。
2.4 Ubuntu 18.04 的真实价值:不是怀旧,而是确定性
看到 Ubuntu 18.04,别急着划走。它绝不是过时的代名词,而是企业级部署的“确定性锚点”。Ubuntu 18.04 的 LTS(长期支持)周期到 2028 年,这意味着安全补丁、关键漏洞修复会持续提供,你不必担心某天 apt update 突然把 Python 升级到 3.9 导致 Django 2.2 崩溃。它的 APT 包库经过数百万服务器验证, sudo apt install python3-pip python3-venv nginx 这条命令,在任何一台 18.04 机器上执行,结果都完全一致——没有 pip install django==4.2.0 因为网络波动装成 4.2.1 而引发的诡异 bug。Systemd 服务管理更是利器:你可以用 sudo systemctl enable myproject 让 Django 应用开机自启,用 sudo journalctl -u myproject -f 实时看日志,用 sudo systemctl restart myproject 一键重启,所有操作都有标准接口,写成 Ansible Playbook 或 Shell 脚本,一次编写,百台服务器复用。我曾帮一家制造企业迁移旧系统,他们有 12 台物理服务器运行着不同年代的 Ubuntu,从 14.04 到 20.04,结果发现 18.04 是唯一一个既能跑通 Django 3.2(满足安全合规要求)又能兼容他们老旧工业相机 SDK(只提供 .so 文件,依赖 glibc 2.27)的版本。所以,选 18.04 不是守旧,而是主动选择“已知的稳定”,把技术风险控制在可预测范围内,把精力留给真正的业务问题——比如如何设计客户标签体系,而不是折腾环境配置。
3. 核心模块拆解与实操细节
3.1 Django 后端:从零搭建客户数据模型与 REST API
我们从最核心的数据开始。客户信息不是一堆字段的罗列,而是有业务语义的实体。在 models.py 中,我定义了四个关键模型:
# backend/core/models.py
from django.db import models
from django.contrib.auth.models import User
class Customer(models.Model):
STATUS_CHOICES = [
('lead', '潜在客户'),
('active', '活跃客户'),
('inactive', '休眠客户'),
('archived', '已归档'),
]
# 基础信息
name = models.CharField(max_length=100, verbose_name="姓名")
email = models.EmailField(unique=True, verbose_name="邮箱")
phone = models.CharField(max_length=20, blank=True, verbose_name="电话")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='lead', verbose_name="状态")
# 关联信息
owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="owned_customers", verbose_name="负责人")
tags = models.ManyToManyField('Tag', blank=True, verbose_name="标签")
# 时间戳
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "客户"
verbose_name_plural = "客户"
ordering = ['-created_at']
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True, verbose_name="标签名")
color = models.CharField(max_length=7, default="#4A90E2", verbose_name="颜色") # 十六进制色值
class Meta:
verbose_name = "标签"
verbose_name_plural = "标签"
class ContactLog(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="contact_logs", verbose_name="客户")
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="操作人")
content = models.TextField(verbose_name="沟通内容")
contact_type = models.CharField(max_length=20, choices=[('call', '电话'), ('email', '邮件'), ('meeting', '面谈')], verbose_name="沟通方式")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="沟通时间")
class Meta:
verbose_name = "沟通记录"
verbose_name_plural = "沟通记录"
ordering = ['-created_at']
这段代码不是随便写的。 name 字段加了 verbose_name ,这是 Django Admin 和 DRF 自动生成中文界面的基础; email 设置 unique=True ,确保数据库层面强制唯一,比应用层校验更可靠; owner 外键用 on_delete=models.SET_NULL ,意味着如果销售员离职被删除,他的客户不会丢失,只是负责人变为空,这符合业务实际; ContactLog 的 related_name="contact_logs" ,让你在模板或 API 中能直接写 customer.contact_logs.all() ,语义清晰。接下来是序列化器,它定义了 API 返回什么数据:
# backend/core/serializers.py
from rest_framework import serializers
from .models import Customer, Tag, ContactLog
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'color']
class ContactLogSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(read_only=True) # 显示用户名,而非ID
class Meta:
model = ContactLog
fields = ['id', 'content', 'contact_type', 'created_at', 'user']
class CustomerSerializer(serializers.ModelSerializer):
owner = serializers.StringRelatedField(read_only=True) # 显示负责人姓名
owner_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
source='owner',
write_only=True,
required=False,
allow_null=True
) # 写入时用ID,读取时显示姓名
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Tag.objects.all(),
source='tags',
write_only=True,
required=False
)
contact_logs = ContactLogSerializer(many=True, read_only=True)
class Meta:
model = Customer
fields = [
'id', 'name', 'email', 'phone', 'status',
'owner', 'owner_id', 'tags', 'tag_ids',
'created_at', 'updated_at', 'contact_logs'
]
这里的关键技巧是 owner 和 owner_id 的分离设计:读取时返回 owner: "张三" 这样的友好字符串,写入(如创建或更新客户)时,前端只需传 {"owner_id": 5} ,DRF 会自动关联。 tag_ids 同理,前端传 [1,3,5] ,后端自动建立多对多关系。这避免了前端解析复杂嵌套对象,也防止了因字段名不一致导致的 API 错误。最后是视图集,它让 CRUD 操作变得极其简洁:
# backend/core/views.py
from rest_framework import viewsets, permissions, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Customer, Tag, ContactLog
from .serializers import CustomerSerializer, TagSerializer, ContactLogSerializer
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'owner'] # 支持 /api/customers/?status=active&owner=2
search_fields = ['name', 'email', 'phone'] # 支持 /api/customers/?search=张
ordering_fields = ['created_at', 'name'] # 支持 /api/customers/?ordering=-created_at
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes = [permissions.IsAuthenticated]
class ContactLogViewSet(viewsets.ModelViewSet):
queryset = ContactLog.objects.all()
serializer_class = ContactLogSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['customer', 'contact_type']
viewsets.ModelViewSet 一行代码就提供了完整的 5 个 API: GET /customers/ (列表)、 POST /customers/ (创建)、 GET /customers/{id}/ (详情)、 PUT /customers/{id}/ (全量更新)、 PATCH /customers/{id}/ (部分更新)、 DELETE /customers/{id}/ (删除)。 DjangoFilterBackend 让你无需写一行代码,就能支持复杂的字段过滤; SearchFilter 开箱即用全文搜索; OrderingFilter 支持任意字段排序。这才是 Django REST Framework 的威力——它把重复劳动标准化,让你聚焦在业务逻辑上。比如,如果你需要“只允许客户负责人或管理员查看该客户”,只需重写 get_queryset 方法:
def get_queryset(self):
user = self.request.user
if user.is_staff:
return Customer.objects.all()
return Customer.objects.filter(owner=user)
3.2 React 前端:构建可交互的客户管理界面
前端不是炫酷动画,而是精准解决操作痛点。我们用 Create React App 初始化:
npx create-react-app frontend --template typescript
cd frontend
npm install axios react-router-dom @mui/material @emotion/react @emotion/styled
@mui/material 是我首选的 UI 框架,它基于 Google 的 Material Design,组件丰富、文档详尽、主题定制灵活,且对 TypeScript 支持极佳。 axios 是 HTTP 客户端,比原生 fetch 更易用。 react-router-dom 处理页面路由。核心文件结构如下:
src/
├── components/
│ ├── CustomerList.tsx # 客户列表(带搜索、筛选、分页)
│ ├── CustomerForm.tsx # 客户创建/编辑表单
│ ├── ContactLogList.tsx # 沟通记录列表
│ └── TagSelector.tsx # 标签多选组件
├── hooks/
│ └── useApi.ts # 封装 axios,统一处理 token、错误
├── services/
│ └── api.ts # API 基础配置(baseURL、拦截器)
├── App.tsx # 主路由
└── index.tsx
services/api.ts 是关键起点:
// src/services/api.ts
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api/';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器:自动添加 JWT Token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Token ${token}`;
}
return config;
});
// 响应拦截器:统一错误处理
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token 过期,跳转登录页
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
这里 process.env.REACT_APP_API_URL 是 CRA 的环境变量机制,你可以在 .env 文件中设置 REACT_APP_API_URL=http://your-server-ip/api/ ,构建时自动注入,避免硬编码。 interceptors.request 自动读取 localStorage 中的 token 并添加到请求头, interceptors.response 捕获 401 错误并清理 token 跳转登录页——这些逻辑在每个 API 调用中都不用重复写。 hooks/useApi.ts 封装具体业务调用:
// src/hooks/useApi.ts
import { useState, useEffect } from 'react';
import api from '../services/api';
export interface Customer {
id: number;
name: string;
email: string;
phone: string;
status: string;
owner: string;
owner_id?: number;
tags: Tag[];
created_at: string;
}
export interface Tag {
id: number;
name: string;
color: string;
}
export const useCustomers = () => {
const [customers, setCustomers] = useState<Customer[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchCustomers = async (params: Record<string, any> = {}) => {
try {
setLoading(true);
const response = await api.get<Customer[]>('/customers/', { params });
setCustomers(response.data);
setError(null);
} catch (err) {
setError('加载客户列表失败,请检查网络');
console.error(err);
} finally {
setLoading(false);
}
};
// ... 其他方法:createCustomer, updateCustomer, deleteCustomer
return { customers, loading, error, fetchCustomers };
};
这个 hook 把数据获取、加载状态、错误处理全部封装好, CustomerList.tsx 中只需:
// src/components/CustomerList.tsx
import { useState, useEffect } from 'react';
import { useCustomers } from '../hooks/useApi';
import { Customer } from '../hooks/useApi';
import {
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
Paper, Button, TextField, Select, MenuItem, InputLabel, FormControl
} from '@mui/material';
const CustomerList = () => {
const { customers, loading, error, fetchCustomers } = useCustomers();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
useEffect(() => {
const params: Record<string, any> = {};
if (searchTerm) params.search = searchTerm;
if (statusFilter !== 'all') params.status = statusFilter;
fetchCustomers(params);
}, [searchTerm, statusFilter, fetchCustomers]);
return (
<Paper sx={{ p: 2 }}>
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
<TextField
label="搜索姓名/邮箱/电话"
variant="outlined"
size="small"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<FormControl variant="outlined" size="small" sx={{ minWidth: 120 }}>
<InputLabel>状态</InputLabel>
<Select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
label="状态"
>
<MenuItem value="all">全部</MenuItem>
<MenuItem value="lead">潜在客户</MenuItem>
<MenuItem value="active">活跃客户</MenuItem>
<MenuItem value="inactive">休眠客户</MenuItem>
</Select>
</FormControl>
</div>
{loading ? <div>加载中...</div> : error ? <div>{error}</div> : (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>姓名</TableCell>
<TableCell>邮箱</TableCell>
<TableCell>电话</TableCell>
<TableCell>状态</TableCell>
<TableCell>负责人</TableCell>
<TableCell>操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{customers.map((customer) => (
<TableRow key={customer.id}>
<TableCell>{customer.name}</TableCell>
<TableCell>{customer.email}</TableCell>
<TableCell>{customer.phone}</TableCell>
<TableCell>{customer.status}</TableCell>
<TableCell>{customer.owner}</TableCell>
<TableCell>
<Button size="small" onClick={() => handleEdit(customer)}>编辑</Button>
<Button size="small" color="error" onClick={() => handleDelete(customer.id)}>删除</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
);
};
export default CustomerList;
这个组件展示了 React 的核心价值:状态驱动 UI。 searchTerm 和 statusFilter 是两个受控输入的状态, useEffect 监听它们的变化,自动触发 fetchCustomers , customers 数组更新后,UI 自动重新渲染。没有手动 DOM 操作,没有事件监听器清理,逻辑清晰。 <Button> 组件来自 MUI,它自带无障碍支持、主题适配、响应式样式,你不用关心 :hover 、 :focus 的 CSS,专注业务。 handleEdit 和 handleDelete 函数会调用 useApi 中的对应方法,实现真正的数据操作。
3.3 Ubuntu 18.04 部署:Nginx + Gunicorn + Systemd 的生产级组合
开发完,部署是另一场硬仗。Ubuntu 18.04 上,我坚持用 systemd 管理进程,因为它比 supervisor 更原生、更可靠。以下是完整步骤:
第一步:准备系统环境
# 更新系统
sudo apt update && sudo apt upgrade -y
# 安装必要软件
sudo apt install -y python3-pip python3-venv nginx git curl
# 创建部署目录和用户
sudo mkdir -p /opt/myproject
sudo useradd --create-home --shell /bin/bash myproject
sudo chown myproject:myproject /opt/myproject
第二步:配置 Django 项目
# 切换到项目用户
sudo su - myproject
# 克隆代码(假设代码在 Git 仓库)
cd /opt/myproject
git clone https://github.com/yourname/myproject.git .
git checkout production # 切换到生产分支
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装依赖(注意:requirements.txt 中指定 Django==3.2.23,因为 3.2 是最后一个支持 Python 3.6 的 LTS 版本)
pip install -r requirements.txt
# 创建 .env 文件(敏感信息不进 Git)
cat > .env << 'EOF'
DEBUG=False
SECRET_KEY=your-super-secret-key-here
ALLOWED_HOSTS=your-domain.com,192.168.1.100
DATABASE_URL=sqlite:///db.sqlite3
EOF
# 运行 Django 命令
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser # 创建管理员账号
第三步:配置 Gunicorn(WSGI 服务器)
Gunicorn 是 Python Web 应用的标准 WSGI 服务器,它负责接收 Nginx 转发来的请求,并调用 Django。创建配置文件:
# 在 /opt/myproject 下创建 gunicorn.conf.py
cat > gunicorn.conf.py << 'EOF'
import multiprocessing
# 监听设置
bind = "127.0.0.1:8000"
bind_address = "127.0.0.1:8000"
port = "8000"
backlog = 2048
# 工作进程
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 100
# 日志
accesslog = "/var/log/myproject/gunicorn_access.log"
errorlog = "/var/log/myproject/gunicorn_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"'
# 进程设置
pidfile = "/var/run/myproject/gunicorn.pid"
chdir = "/opt/myproject"
daemon = False
user = "myproject"
group = "myproject"
umask = 0002
initgroups = True
EOF
第四步:创建 systemd 服务文件
这是 Ubuntu 18.04 部署的灵魂。创建 /etc/systemd/system/myproject.service :
[Unit]
Description=Gunicorn instance to serve myproject
After=network.target
[Service]
User=myproject
Group=myproject
WorkingDirectory=/opt/myproject
EnvironmentFile=/opt/myproject/.env
ExecStart=/opt/myproject/venv/bin/gunicorn --config /opt/myproject/gunicorn.conf.py myproject.wsgi:application
[Install]
WantedBy=multi-user.target
启用并启动服务:
sudo systemctl daemon-reload
sudo systemctl enable myproject
sudo systemctl start myproject
sudo systemctl status myproject # 检查是否运行正常
第五步:配置 Nginx 反向代理
Nginx 作为反向代理,处理静态文件、SSL 终止、负载均衡。编辑 /etc/nginx/sites-available/myproject :
server {
listen 80;
server_name your-domain.com;
# 静态文件(Django collectstatic 后的文件)
location /static/ {
alias /opt/myproject/staticfiles/;
}
# 媒体文件(用户上传的图片等)
location /media/ {
alias /opt/myproject/media/;
}
# 将所有其他请求转发给 Gunicorn
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}
启用站点并重启 Nginx:
sudo ln -sf /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled
sudo nginx -t # 测试配置
sudo systemctl restart nginx
至此,你的 Django 应用已在 Ubuntu 18.04 上以生产模式运行。 systemctl 命令让你拥有完全掌控力: sudo systemctl restart myproject 重启应用, sudo journalctl -u myproject -f 实时看日志, sudo systemctl stop myproject 临时下线。所有操作都有迹可循,符合企业级运维规范。
4. 实操过程中的关键环节与配置详解
4.1 Django 设置文件(settings.py)的生产环境改造
开发环境的 settings.py 通常简单粗暴,但生产环境必须精细化。我在 backend/myproject/settings/ 下创建了三个文件: base.py (通用配置)、 development.py (开发专用)、 production.py (生产专用)。 production.py 的关键改造如下:
# backend/myproject/settings/production.py
from .base import *
# 安全相关(必须!)
DEBUG = False
SECRET_KEY = os.environ.get('SECRET_KEY') # 从 .env 文件读取,绝不硬编码
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',') # 如 'example.com,192.168.1.100'
# 静态文件(CSS, JS, Images)
STATIC_ROOT = '/opt/myproject/staticfiles/' # collectstatic 的目标目录
STATIC_URL = '/static/'
# 媒体文件(用户上传)
MEDIA_ROOT = '/opt/myproject/media/'
MEDIA_URL = '/media/'
# 数据库(生产环境强烈建议用 PostgreSQL)
import dj_database_url
DATABASES = {
'default': dj_database_url.config(
default='sqlite:///db.sqlite3',
conn_max_age=600,
ssl_require=False
)
}
# 日志配置(至关重要!)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/myproject/django.log',
'maxBytes': 1024*1024*5, # 5 MB
'backupCount': 5,
'formatter': 'verbose',
},
},
'root': {
'handlers': ['file'],
'level': 'INFO',
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
'propagate': False,
},
},
}
# CORS(跨域资源共享,前端 React 需要)
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # 开发时 React 本地服务
"https://your-react-app.com", # 生产时 React 前端域名
]
# Django REST Framework 设置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20, # 默认每页20条
}
这里有几个血泪教训: SECRET_KEY 必须从环境变量读取,否则一旦代码泄露,整个应用密钥就暴露了; ALLOWED_HOSTS 必须精确列出所有合法域名/IP,否则 Django 会拒绝所有请求,这是常见的 400 Bad Request 错误根源; LOGGING 配置了 RotatingFileHandler ,它会自动轮转日志文件,防止磁盘被日志占满, /var/log/myproject/ 目录需要提前创建并赋予权限 sudo mkdir -p /var/log/myproject && sudo chown myproject:myproject /var/log/myproject ; CORS_ALLOWED_ORIGINS 必须包含 React 前端的地址,否则浏览器会因跨域策略阻止 API 请求,报错 No 'Access-Control-Allow-Origin' header is present 。这些配置不是可选项,而是生产环境的生存底线。
4.2 React 前端构建与 Nginx 静态部署
React 应用最终要打包成静态文件,由 Nginx 直接提供服务。关键在于 package.json 中的 homepage 字段:
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"homepage": "https://your-react-app.com", // 必须设置!否则路由跳转404
"dependencies": {
// ...
}
}
这个字段告诉 CRA,所有静态资源(JS、CSS)的路径前缀是什么。如果不设置,构建后 index.html 中的 <script src="/static/js/main.123.js"> 会尝试从根路径加载,
更多推荐
所有评论(0)