创建于 2025-06-12 12:37 | 浏览次数 398
title: 部署Django项目到Centos7上 date: 2025-05-23 15:06:00 categories: 运维 tags: - python - Django - Bootstrap5 - JQuery
在执行编译的时候(make -j$(nproc)
)会发生下面的报错:
The necessary bits to build these optional modules were not found:
_hashlib _ssl _tkinter
To find the necessary bits, look in configure.ac and config.log.
Could not build the ssl module!
Python requires a OpenSSL 1.1.1 or newer
Checked 111 modules (31 built-in, 76 shared, 1 n/a on linux-x86_64, 0 disabled, 3 missing, 0 failed on import)
Python requires a OpenSSL 1.1.1 or newer
:这说明 Python 构建时找不到合适版本的 OpenSSL(>=1.1.1),尽管你已经装了 openssl-devel
。
查看yum
下载的 openssl-devel
版本。
# 查看版本信息
rpm -q openssl-devel
# 输出: openssl-devel-1.0.2k-26.el7_9.x86_64
# 查看更加详细的信息
rpm -qi openssl-devel
CentOS 7
最长支持期为 2024 年底,系统默认的软件包依赖 OpenSSL 1.0.2
(如 SSH
、yum
自身等),所以官方不升级 OpenSSL
,以避免破坏系统兼容性。
cd /usr/local/src
# 下载
wget https://www.openssl.org/source/openssl-3.5.0.tar.gz
# 解压
tar -xzf openssl-3.5.0.tar.gz
cd openssl-3.5.0
# 做配置
./config --prefix=/usr/local/openssl-3.5.0 --openssldir=/usr/local/openssl-3.5.0 shared zlib
# 编码
make -j$(nproc)
# 安装
make install
选项 | 说明 |
---|---|
./config ... |
配置 OpenSSL 编译选项 |
--prefix=/usr/local/openssl-3.5.0 |
安装路径。OpenSSL 会被安装到这个目录。 |
--openssldir=/usr/local/openssl-3.5.0 |
配置文件查找路径,如 openssl.cnf 。 |
shared |
编译生成共享库(libssl.so 、libcrypto.so ),而不是静态库。Python 编译需要这个。 |
zlib |
启用 zlib 支持(压缩功能)。 |
./configure --prefix=/usr/local/python3.12 \
--enable-optimizations \
--with-ensurepip=install \
--with-openssl=/usr/local/openssl-3.5.0
需要再Python编译的时候加入--with-openssl=/usr/local/openssl-3.5.0
这个配置
在执行python manage.py migrate
的时候,报如下错误:
django.db.utils.NotSupportedError: deterministic=True requires SQLite 3.8.3 or higher
这个错误说明你当前服务器上的 SQLite 版本太低,不支持 Django 某些新特性,比如:
models.Index(fields=["slug"], name="slug_idx", condition=..., deterministic=True)
查看当前服务器 SQLite 版本
sqlite3 --version
CentOS 7.9 已于 2024 年 6 月停止更新
yum install sqlite
安装到的是旧版本,根本无法满足 Django 的要求# 查了yum 中 sqlite 的信息
yum info sqlite
Name : sqlite
Arch : x86_64
Version : 3.7.17
# 安装依赖
sudo yum groupinstall -y "Development Tools"
sudo yum install -y readline-devel wget tar
# 下载最新版(以 3.45.1 为例)
cd /usr/local/src
sudo wget https://www.sqlite.org/2024/sqlite-autoconf-3450100.tar.gz
sudo tar xzf sqlite-autoconf-3450100.tar.gz
cd sqlite-autoconf-3450100
# 编译安装
sudo ./configure --prefix=/usr/local
sudo make -j$(nproc)
sudo make install
# 替换系统默认 SQLite(不覆盖,仅优先使用)
### 先确认新版本已装好
/usr/local/bin/sqlite3 --version
### 然后将它加入环境变量
echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
### 再次确认:
sqlite3 --version
如果我们只是安装了最新的Sqlite,但是并没有让Pyhton去使用它,那么当我们使用python manage.py migrate
的时候,还是会报如下错误:
django.db.utils.NotSupportedError: deterministic=True requires SQLite 3.8.3 or higher
具体如何关联,需要看Python的编译。
# 删除 `/usr/local/bin` 下相关的可执行文件
cd /usr/local/bin
rm -f python3 python3.12 python3-config python3.12-config
rm -f 2to3 2to3-3.12
rm -f idle3 idle3.12
rm -f pydoc3 pydoc3.12
rm -f pip3 pip3.12
# 删除 `/usr/local` 安装的包
rm -rf python3.12
# 删除标准库和头文件
rm -rf /usr/local/lib/python3.12
rm -rf /usr/local/lib/libpython3.12.a
rm -rf /usr/local/include/python3.12
# 删除软链接
rm -f /usr/bin/python3.12
# 删除虚拟环境: 如果曾设置了虚拟环境 `.venv`,建议一起删掉:
rm -rf /path/to/project/.venv
# 删除pyhton的包
rm -rf /home/Python-3.12.9
默认Centos7中只有Python2,没有Python3需要进行编译安装。
[root@14uuZ ~]# python2 --version
Python 2.7.5
bash
sudo yum -y groupinstall "Development Tools"
sudo yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel gcc-c++
yum -y groupinstall "Development Tools"
: 是在 一次性安装一整套编译和开发所需的工具链和依赖库,比如:
gcc
(C 语言编译器)make
(自动化构建工具)gdb
(调试器)binutils
、libtool
、autoconf
等如果包下载的不完整,在执行编译的时候(make -j$(nproc)
)会发生下面的报错:
Fatal Python error: init_import_site: Failed to import the site module
Fatal Python error: Python runtime state: init_import_siteinitialized:
Failed to import the site module
Python runtime state: initialized
Traceback (most recent call last):
Traceback (most recent call last):
File "/home/Python-3.12.9/Lib/site.py", line 73, in <module>
File "/home/Python-3.12.9/Lib/site.py", line 73, in <module>
import os
File "/home/Python-3.12.9/Lib/os.py", line 29, in <module>
import os
File "/home/Python-3.12.9/Lib/os.py", line 29, in <module>
from _collections_abc import _check_methods
from _collections_abc import _check_methods
SystemError: <built-in function compile> returned NULL without setting an exception
SystemError: <built-in function compile> returned NULL without setting an exception
make[1]: *** [Python/frozen_modules/abc.h] Error 1
make[1]: *** Waiting for unfinished jobs....
make[1]: *** [Python/frozen_modules/codecs.h] Error 1
make[1]: Leaving directory /home/Python-3.12.9'
make: *** [profile-opt] Error 2
下载Python源码:https://www.python.org/ftp/python/
wget https://www.python.org/ftp/python/3.12.9/Python-3.12.9.tgz
Wget 是一个免费的开源命令行工具,用于从网络上下载文件。它的名字来源于 "World Wide Web" 和 "get" 的组合。
因为服务器在国内,所以直接用这种方式下载会比较慢。因此,可以把文件下载到本地,再上传。
在阿里云的workbench
里面,找到:文件-->打开新文件数
可以里面的文件进行上传和删除操作。但是有一个缺点,无法看到上传进度。
cd /home/Python-3.12.9
# 确保干净
make distclean
./configure --prefix=/usr/local/python3.12 \
--enable-optimizations \
--with-ensurepip=install \
--with-openssl=/usr/local/openssl-3.5.0
make -j$(nproc)
make install
选项 | 说明 |
---|---|
./configure ... |
配置 Python 编译选项,指定 OpenSSL 路径等 |
--prefix=/usr/local/python3.12 |
安装路径,将最终的 Python 安装到该目录,不会覆盖系统默认 Python。 |
--enable-optimizations |
启用额外优化(比如 PGO 和 LTO ),提高 Python 运行性能(编译时间更长)。 |
--with-ensurepip=install |
编译后自动安装 pip 。 |
--with-openssl=/usr/local/openssl-3.5.0 |
指定使用你自己编译安装的 OpenSSL 路径,而不是系统默认的旧版本(重要)。 |
--with-openssl
的作用
这个选项明确告诉 Python 的 configure 脚本 OpenSSL 的安装位置,它会:
-I/usr/local/openssl-3.5.0/include
)-L/usr/local/openssl-3.5.0/lib
)但是,OpenSSL 3.5.0 的库文件(libssl.so.3
和 libcrypto.so.3
)安装在lib64
下面
ls /usr/local/openssl-3.5.0/lib64
cmake engines-3 libcrypto.a libcrypto.so libcrypto.so.3 libssl.a libssl.so libssl.so.3 ossl-modules pkgconfig
为此,我们可以创建符号链接
# 创建符号链接,让 lib 指向实际的 lib64
sudo ln -s /usr/local/openssl-3.5.0/lib64 /usr/local/openssl-3.5.0/lib
# 验证链接
ls -l /usr/local/openssl-3.5.0/
cd /home/Python-3.12.9
# 确保干净
make distclean
export CPPFLAGS="-I/usr/local/include -I/usr/local/openssl-3.5.0/include"
export LDFLAGS="-L/usr/local/lib -L/usr/local/openssl-3.5.0/lib64 -Wl,-rpath,/usr/local/lib:/usr/local/openssl-3.5.0/lib64"
export LD_LIBRARY_PATH=/usr/local/lib:/usr/local/openssl-3.5.0/lib64:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/local/openssl-3.5.0/lib64/pkgconfig
./configure --prefix=/usr/local/python3.12 \
--with-ensurepip=install \
--with-openssl=/usr/local/openssl-3.5.0
make -j$(nproc)
make install
作用:设置编译器寻找头文件(.h
文件)的路径。这是给 C/C++ 编译器(如 gcc) 设置的参数,告诉它到哪里去找头文件。
关键参数说明:
参数 | 意义 |
---|---|
-I/usr/local/include |
指定搜索标准第三方库头文件的路径,例如 SQLite 安装后的 sqlite3.h 可能就在这里 |
-I/usr/local/openssl-3.5.0/include |
指定 OpenSSL 的头文件路径,比如 openssl/ssl.h , openssl/err.h 等都在这个目录里 |
影响:当你在编译 Python 源码(用 C 写的)时,它会在源码里包含:
#include <openssl/ssl.h>
#include <sqlite3.h>
如果你不告诉编译器在哪里找这些文件,它就会默认只去系统路径如 /usr/include
下找;但你手动安装的 OpenSSL、SQLite 根本不在这些路径中,结果就找不到,导致编译报错:
fatal error: openssl/ssl.h: No such file or directory
作用:设置链接器寻找动态库(.so
或 .a
文件)的路径。这是给 链接器(ld 或 gcc 链接阶段) 设置的参数,告诉它到哪里去找你依赖的二进制库文件。
参数拆解:
参数 | 意义 |
---|---|
-L/usr/local/lib |
指定链接时优先在这个目录找库(比如 libsqlite3.so , libffi.so ) |
-L/usr/local/openssl-3.5.0/lib64 |
指定 OpenSSL 安装目录下的库文件(如 libssl.so , libcrypto.so )路径 |
-Wl,-rpath,... |
告诉链接器把这些路径写入最终可执行文件的“运行时查找路径”中,防止运行时报错找不到 libssl.so 等库 |
-Wl,xxx
的意思是:把参数xxx
传给 linker(ld)。-rpath
是一种让运行时自动在指定目录找库的机制。
举例:假设编译 Python 的 _ssl
模块时需要链接 libssl.so
:
gcc -o _ssl.so ... -lssl -lcrypto
如果你没设置 -L/usr/local/openssl-3.5.0/lib64
,那么链接器只能从默认系统路径 /usr/lib
、/lib64
中找,找不到就会报错:
ld: cannot find -lssl
同样,如果你没设置 -rpath
或 LD_LIBRARY_PATH
,你运行 Python 时会报错:
error while loading shared libraries: libssl.so: cannot open shared object file
LD_LIBRARY_PATH
是一个环境变量,用于设置动态链接器(dynamic linker)在程序运行时查找共享库 .so
文件的路径。在 Linux 中,一个程序运行时需要加载的共享库(如 libssl.so
)并不是嵌入在程序本体中的,而是程序运行时由动态链接器 ld.so
加载。
作用阶段:运行时。不是编译时,也不是链接时,而是程序真正执行时起作用。
举例说明:你自编译了 Python 并成功安装了它:
/usr/local/python3.12/bin/python3.12
它需要依赖你自编译的 OpenSSL 动态库 /usr/local/openssl-3.5.0/lib64/libssl.so.3
。
但系统默认只在这些目录找共享库:
/lib
/lib64
/usr/lib
/usr/lib64
而不会自动去 /usr/local/openssl-3.5.0/lib64
。
所以运行时你就会遇到这个错误:
error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory
这时你就需要告诉系统:“去我指定的目录找共享库”。这就是 LD_LIBRARY_PATH
:
export LD_LIBRARY_PATH=/usr/local/openssl-3.5.0/lib64:$LD_LIBRARY_PATH
你之后再运行 Python,它就能成功找到 libssl.so.3
并正常运行。
这个变量是给工具 pkg-config
用的。pkg-config
是一个辅助工具,能告诉你一个库:
-I
(头文件路径)-L
(库路径)-lssl -lcrypto
)用法如下:
pkg-config --cflags openssl
# 输出:-I/usr/local/openssl-3.5.0/include
pkg-config --libs openssl
# 输出:-L/usr/local/openssl-3.5.0/lib64 -lssl -lcrypto
但前提是你告诉它去哪里找 .pc
文件 —— 那些记录这些信息的元数据文件。这就是 PKG_CONFIG_PATH
的作用。
作用阶段:配置/编译前
有些软件(包括 Python 的 configure
脚本)会调用 pkg-config
来检测依赖。例如:
./configure --with-openssl=...
# 内部会用 pkg-config openssl 检查你装的 OpenSSL 是否合格
如果你不设置 PKG_CONFIG_PATH
,它找不到对应的 .pc
文件,就会误判 OpenSSL 没装,或者不支持 TLS1.3。
如果你从源码安装了 OpenSSL,它可能会生成:
/usr/local/openssl-3.5.0/lib64/pkgconfig/openssl.pc
所以你需要设置:
export PKG_CONFIG_PATH=/usr/local/openssl-3.5.0/lib64/pkgconfig
这让 pkg-config openssl
能正确找到并返回包含正确 -I
和 -L
的参数,最终被传递给 gcc
和 ld
。
--enable-optimizations
它会在构建过程中启用 Python 性能优化编译流程,特别是使用 PGO
(Profile-Guided Optimization)和 LTO
(Link Time Optimization)。
PGO(Profile-Guided Optimization):启用这个选项后,构建流程会:
python
的测试脚本收集运行时的性能数据;LTO(Link Time Optimization):会在链接阶段做全程序优化,提升运行效率。
原因一:PGO 会运行测试用例,依赖完整的系统环境和正确链接
当使用 --enable-optimizations
时,构建脚本会运行 ./python -m test
来收集性能数据。这个过程中会调用大量标准库和 C 扩展模块。如果你用的是自己安装的 SQLite(非系统默认路径 /usr
),而没有正确设置 LD_LIBRARY_PATH
或 rpath
,那么:
.so
文件;_sqlite3
)无法导入;原因二:LTO
对某些平台(如 CentOS 7)不友好
CentOS 7 使用较旧版本的 GCC(除非你升级),可能对 LTO 支持不完善,容易在链接阶段出错,尤其在涉及自定义 OpenSSL/SQLite 路径时。
LTO = Link Time Optimization(链接时优化)。它是指将跨源文件的优化工作从编译阶段延后到链接阶段进行,以便编译器可以“全局地”看到整个程序的结构,从而做更激进、更高级的优化。
阶段 | 普通编译 | 启用 LTO 的编译 |
---|---|---|
每个源文件编译为 .o |
编译器只优化当前 .c 文件 |
编译器生成特殊的 .o 文件,保留中间表示(IR) |
链接阶段 | 只做符号链接,不优化 | 编译器在链接阶段“重新编译”全部 .o ,进行全程序优化 |
优化范围 | 单个源文件 | 全程序、跨模块、跨函数优化 |
LTO 的缺点 / 潜在问题
编译时间增加(显著):链接时编译器要“重读”所有 .o
文件做一次 IR 分析,代价不小。
链接阶段容易失败:
如果 .o
文件来自不同版本的编译器,可能会不兼容;
如果某些第三方 .a
/ .so
库没有用 LTO 编译,可能出错;
某些旧平台(如 CentOS 7)对 LTO 支持不成熟,常见报错如下:
bash
lto1: internal compiler error: in lto_output_decl_index, at lto/lto.c:1234
collect2: error: ld returned 1 exit status
PGO = Profile-Guided Optimization,又称为“性能分析引导优化”。它的核心思想是:让程序先运行一遍,从中“学习”热点函数、常用路径,然后再用这些信息重新编译,生成性能更优的程序。
PGO 的工作流程
运行程序生成性能数据:用这个插桩的 Python 去跑一套典型的用例。或者在你自己的项目上运行你典型的负载。这一步会生成一个 .gcda
或 .profdata
数据文件。
重新编译,使用收集的性能数据:这一步,编译器会根据你收集的使用信息,优化常走路径、常用函数、减少 cache miss 等等,生成真正优化后的可执行文件。
graph LR
A[阶段1:编译带插桩的解释器] --> B[阶段2:运行性能测试]
B --> C[阶段3:优化重新编译]
失败发生在阶段2(运行测试)时:
# 错误信息表明
Fatal Python error: init_import_site: Failed to import the site module
File "/home/Python-3.12.9/Lib/site.py", line 73, in <module>
import os # 这里需要加载SQLite3!
为什么添加SQLite3路径后失败?
场景 | 动态库加载机制 | PGO阶段的影响 |
---|---|---|
系统默认SQLite | 位于标准路径 /usr/lib ,无需特殊配置 |
测试运行时自动找到库 |
自定义SQLite | 需要显式配置运行时路径 | PGO测试运行时找不到库 |
环境变量作用域:
CPPFLAGS
/LDFLAGS
只影响编译时PGO的特殊执行环境:
_bootstrap_python
)态库搜索机制:运行时如何找 .so
文件?
Linux 下,当一个程序运行时要加载动态库(比如 libsqlite3.so
),它会按如下顺序查找:
rpath
(编译时设置的运行时路径,固定在 ELF 文件中)LD_LIBRARY_PATH
(环境变量,运行时可变)/etc/ld.so.cache
(系统缓存,来自 ldconfig
)/lib
, /usr/lib
, /lib64
, /usr/lib64
在编译Python的时候,我花了一个星期达到时间在这个问题上。但实际上,最后仍然是没有找到答案。
LD_LIBRARY_PATH
配置了,但是编译还是失败--enable-optimizations
这个配置的时候,就可以配置成功,到底是为什么很难理解。centOS7.9
的版本,和PGO
与LTO
有些不匹配。# 安装后验证 Python 的 SSL 模块
/usr/local/python3.12/bin/python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
# 安装后验证 Python 的 SQLite3 模块
/usr/local/python3.12/bin/python3 -c "import sqlite3; print('SQLite3:', sqlite3.sqlite_version)"
ln -s /usr/local/python3.12/bin/python3 /usr/bin/python3.12
ls /usr/bin | grep python
python
python2
python2.7
python2.7-config
python2-config
python3
python3.12
python3.6
python3.6-config
python3.6m
python3.6m-config
python3.6m-x86_64-config
python3-config
python-config
python3.12 --version
Python 3.12.9
make ≡ make all
:显式调用编译全部目标模块的过程。多数情况下就是构建主程序(这里是 Python 可执行文件)。
make -j$(nproc)
:多核并行编译,提高速度。
-j
指并行编译,$(nproc)
是获取当前 CPU 核心数的命令(比如 4 核返回 4)。相当于告诉 make:可以最多使用 N 个线程同时编译。
好处:编译速度显著加快。
假设你有 8 核 CPU:
命令 | 并发 | 效果 |
---|---|---|
make |
单线程 | 慢,顺序执行 |
make all |
单线程 | 同上 |
make -j8 或 make -j$(nproc) |
8 线程 | 快,适合大项目如 Python |
特性 | make clean |
make distclean |
---|---|---|
主要目的 | 清理编译生成的中间文件和目标文件 | 彻底恢复源码目录到初始状态,包括配置生成的文件 |
删除内容 | 通常删除 .o 文件、静态库、动态库等 |
除 clean 的内容外,还删除配置文件(如 Makefile.in )、依赖文件、临时文件等 |
是否删除配置 | 不删除配置相关的文件 | 删除配置生成的文件(如 configure 生成的 Makefile ) |
使用场景 | 重新编译前清理旧的编译结果 | 发布源码前或彻底重置项目目录时 |
依赖关系 | 通常作为基础清理目标 | 通常依赖 clean ,并执行更彻底的清理 |
自动化工具关联 | 可能由编译器自动生成 | 常见于 Autotools(如 automake )项目 |
bash
# 清华大学源 镜像地址:
pip config set global.index-url https://pypi.mirrors.ustc.edu.cn/simple
常用镜像源地址:
```bash # 清华大学源 镜像地址: https://pypi.tuna.tsinghua.edu.cn/simple
# 阿里云源 镜像地址: https://mirrors.aliyun.com/pypi/simple
# 豆瓣源 镜像地址: https://pypi.douban.com/simple
# 中国科学技术大学源 镜像地址: https://pypi.mirrors.ustc.edu.cn/simple
# 腾讯云源 镜像地址: https://mirrors.cloud.tencent.com/pypi/simple ```
问题类型 | CentOS 7.9 宿主机的问题 | Docker 的解决方式 |
---|---|---|
系统老旧 | GCC、OpenSSL、SQLite 版本过低,需要手动编译 | Docker 镜像可以选用最新 Ubuntu/Debian/Fedora,系统包一键升级 |
编译复杂 | 配置 CPPFLAGS /LDFLAGS ,还要注意 PGO、LTO、运行时动态库路径 |
Dockerfile 里直接写死所有步骤,复现完全一致 |
试错成本高 | 编译失败要清理缓存、回溯日志、反复尝试 | 失败只要 docker build 重来,干净、快速 |
依赖冲突 | Python/SSL/SQLite 的路径互相干扰,系统包有冲突风险 | Docker 是隔离环境,不影响宿主系统 |
版本迁移困难 | CentOS 7 的 Python 3.12 + OpenSSL 3.5.0 组合很难搞 | Docker 中用任何版本自由组合,几行命令搞定 |
# 并安装 Git
sudo yum install git -y
# 验证安装
git --version
# 创建裸仓库
mkdir -p /home/repo/LinNote.git
cd /home/repo/LinNote.git
# 初始化裸仓库
git init --bare
# 创建用于存放代码的仓库
mkdir -p /home/LinNote
LinNote.git
是目录,不是文件当运行 git init --bare
时,Git 会创建一个没有工作区的仓库(即裸仓库),其结构如下:
/opt/repos/LinNote.git/
├── HEAD # 当前分支指针
├── branches/ # (旧版 Git 可能使用)
├── config # 仓库配置
├── description # 仓库描述
├── hooks/ # 钩子脚本(如 post-receive)
├── info/ # 排除规则等
├── objects/ # Git 对象数据库
└── refs/ # 分支和标签的引用
虽然名字以 .git
结尾,但它是一个目录,所以可以 cd
进入。
特性 | 裸仓库(--bare ) |
普通仓库(非裸) |
---|---|---|
工作区 | ❌ 无(不能直接编辑文件) | ✅ 有(可直接修改代码) |
用途 | 作为远程中央仓库(仅接收推送) | 本地开发使用 |
典型路径 | /opt/repos/project.git/ |
~/projects/my-project/ |
可 cd 进入 |
✅ 是(因为是目录) | ✅ 是 |
git push
同步)。post-receive
)实现自动部署。在裸仓库里创建 post-receive
钩子,使得每次 git push
后代码自动同步到网站目录。
cd /home/repo/LinNote.git/hooks
vim post-receive
写入以下内容:
#!/bin/bash
TARGET="/home/LinNote" # 项目部署的目标目录(网站运行目录)
GIT_DIR="/home/repo/LinNote.git" # Git 裸仓库的路径
BRANCH="main" # 只监听这个分支的推送
while read oldrev newrev ref
do
# 只有当推送的是指定分支时才执行
if [[ $ref = refs/heads/$BRANCH ]];
then
# 部署代码
echo "Ref $ref received. Deploying ${BRANCH} branch to production..."
# 强制检出代码到目标目录
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f $BRANCH
echo "Deployment completed!"
else
echo "Ref $ref received. Doing nothing: only the ${BRANCH} branch may be deployed."
fi
done
Git 在执行 post-receive
钩子时,会传入 3 个参数:
oldrev
:推送前的 commit ID。newrev
:推送后的 commit ID。ref
:推送的分支(如 refs/heads/main
)。脚本检查 ref
是否匹配 BRANCH
(main
),只有匹配时才执行部署。
强制检出代码到目标目录:也就是把代码同步到GIT_DIR
--work-tree=$TARGET
:指定代码检出到 TARGET
目录(/home/LinNote
)。--git-dir=$GIT_DIR
:指定 Git 仓库路径(/home/repo/LinNote.git
)。checkout -f
:强制覆盖目标目录的文件(避免冲突)。# 赋予权限
chmod +x /home/repo/LinNote.git/hooks/post-receive
# 查看权限是否存在:正常输出应包含 x(如 -rwxr-xr-x)
ls -l /home/repo/LinNote.git/hooks/post-receive
git remote add origin root@xxxx:/home/repo/LinNote.git
# 删除远程仓库:
git remote remove origin
git push -u origin main # 或 master
# 查看是否推送成功
ls -l /home/LinNote
# 查看LinNote文件夹的权限
ls -ld /home/LinNote
# 进入到裸仓库
cd /home/repo/LinNote.git
# 强制执行
echo "0000000000000000000000000000000000000000 $(git rev-parse HEAD) refs/heads/main" | ./hooks/post-receive
# 确认python的版本
python3.12 --version
# 创建名字为.venv的虚拟环境
python3.12 -m venv .venv
# 激活虚拟环境:成功激活之后会在前面出现 (.venv)
source .venv/bin/activate
(.venv) [root@14uuZ LinNote]#
# 关闭(退出)虚拟环境
deactivate
# 本地:导出依赖
pip freeze > requirements.txt
# 服务器:根据requirements.txt:下载相关的包:
pip install -r requirements.txt
# 因为是在虚拟环境中,所以 python 就直接等于 Python 3.12.9
(.venv) [root@4uuZ LinNote]# python --version
Python 3.12.9
# 将迁移脚本文件映射到数据库中:因为迁移脚本已经通过git同步到服务器代码中,所有不需要执行python manage.py makemigrations
(.venv) [root@4uuZ LinNote]# python manage.py migrate
# 把所有静态文件复制到 staticfiles/ 目录
python manage.py collectstatic
# 启动服务器:80端口
python manage.py runserver 0.0.0.0:80
# Django 的主urls.py需要如下配置
urlpatterns += [
re_path(r'^static/(?P<path>.*)$', serve, {'document_root': settings.STATIC_ROOT}),
]
确保你有以下设置:
# 生产环境
DEBUG = False
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
# 特别说明:开发模式从这里读取。也就是因STATICFILES_DIRS这个配置,我们才能在开发环境下读取static的静态文件
STATICFILES_DIRS = [BASE_DIR / "static"]
为什么在开发环境中不需要执行 collectstatic
模式 | 来源目录 | 是否需要 collectstatic |
是否自动提供静态资源 |
---|---|---|---|
开发模式 (DEBUG=True ) |
STATICFILES_DIRS 和 app 内的 static/ 文件夹 |
❌ 不需要 | ✅ Django 自带开发服务器自动处理 |
生产模式 (DEBUG=False ) |
只从 STATIC_ROOT 读取 |
✅ 需要 collectstatic |
❌ 需要 Web 服务器(如 nginx)提供服务 |
在开发模式下:
{% static 'bootstrap5/bootstrap.min.css' %}
加载的 URL 是 /static/bootstrap5/bootstrap.min.css
STATICFILES_DIRS
(比如 static/
)中查找这类文件collectstatic
是做什么用的?
static/
、STATICFILES_DIRS
中的文件)复制到 STATIC_ROOT
中。在 DEBUG=False
(生产模式)下,让 Django 能正确处理静态资源请求。
# 但要注意:这个方式 只是临时测试用的,Django 官方明确指出:
##### 不要在生产环境下使用 serve() 提供静态文件,它效率低、无缓存机制、安全性也差
# 在正式部署时,推荐使用 Nginx 或其他 Web 服务器 处理静态资源。
urlpatterns += [
re_path(r'^static/(?P<path>.*)$', serve, {'document_root': settings.STATIC_ROOT}),
]
部分 | 解释 |
---|---|
re_path(...) |
使用正则表达式添加一个 URL 路由 |
r'^static/(?P<path>.*)$' |
匹配所有以 /static/ 开头的 URL,例如 /static/css/style.css 、/static/js/app.js 等 |
(?P<path>.*) |
命名捕获组,表示 /static/ 之后的路径将被作为参数传入视图(比如 css/style.css ) |
serve |
Django 内置的视图函数,用于返回文件内容 |
{'document_root': settings.STATIC_ROOT} |
告诉 serve 函数从哪个目录中查找文件内容,settings.STATIC_ROOT 指向 collectstatic 收集后的文件目录 |
urlpatterns += [...] |
把这条路由添加到 Django 的路由系统中 |
假设你访问浏览器地址:
http://www.linnote.space/static/bootstrap5/bootstrap.min.css
Django 会:
"bootstrap5/bootstrap.min.css"
作为参数传给 serve
视图。settings.STATIC_ROOT + 'bootstrap5/bootstrap.min.css'
找到文件,返回它的内容。/home/LinNote/staticfiles/bootstrap5/bootstrap.min.css
python manage.py runserver 0.0.0.0:80
http
请求ALLOWED_HOSTS
ALLOWED_HOSTS = [
'linnote.space',
'www.linnote.space',
'your-server-ip-address',
'127.0.0.1',
'localhost'
]
当 DEBUG=False
时,Django 会检查请求中的 HTTP Host 头部是否在你允许的主机列表中,如果不在,就返回 400 Bad Request 错误。
如果用户访问:
http://www.linnote.space/
请求头中会包含:
Host: www.linnote.space
此时 Django 会检查这个域名是不是在你设置的
ALLOWED_HOSTS
里。
条目 | 允许的访问方式 |
---|---|
'linnote.space' |
用户访问 http://linnote.space 时 |
'www.linnote.space' |
用户访问 http://www.linnote.space/ 时 |
'your-server-ip-address' |
(需要你替换成实际 IP)直接通过公网 IP 访问时,例如 http://88.188.188.188 |
'127.0.0.1' |
本地回环地址,开发测试 |
'localhost' |
本地 DNS 名称,开发测试 |
uWSGI 是一种应用服务器,用于运行 Python Web 应用(比如 Django、Flask)。
它支持 WSGI 协议,能高效处理请求,比 Django 自带的 runserver
稳定、安全、性能更好。
Nginx 是一个高性能 Web 服务器和反向代理服务器。
它接收用户请求、处理 HTTPS、缓存静态资源,然后把请求转发给 uWSGI。
组件 | 作用 |
---|---|
Nginx | 接收公网请求、处理 HTTPS、缓存静态资源、反向代理到 uWSGI |
uWSGI | 实际运行 Django 项目、处理动态内容请求(比如查询数据库) |
Django | 专注处理业务逻辑,不直接暴露给公网 |
# 先在本地下载:
pip install uwsgi
# 导入到 requirements.txt
pip freeze > requirements.txt
# 把变化的文件通过git推送到远程
git push -u origin main
# 远程:下载
pip install -r requirements.txt
[uwsgi]
# 项目目录(即 manage.py 所在目录)
chdir = /home/LinNote
# 指定 Django 的 WSGI 模块(模块路径 + 应用对象)
# 相当于:from LinNote.wsgi import application
# application 是 Django 项目的 WSGI 接入点,uWSGI 通过它与 Django 框架交互。
# 注意:确保 LinNote.wsgi.py 文件存在,并且包含正确的 application 对象。
module = LinNote.wsgi:application
# Python 虚拟环境路径(指向你项目的 .venv 目录)
home = /home/LinNote/.venv
# 开启主进程管理多个 worker 子进程
# 主进程负责启动和管理 worker 子进程,监听信号、平滑重启、日志记录等。
master = true
# 启动 4 个 worker 进程(可根据 CPU 核心数调整)
# 通常设置为:CPU核心数 × 1~2。比如你服务器是 2 核,可以设置为 2~4。
processes = 4
# uWSGI 绑定一个 TCP socket,监听本地地址 127.0.0.1 上的 8001 端口。
# Nginx 与 uWSGI 通信的桥梁。
# 你在 Nginx 配置中使用 uwsgi_pass 127.0.0.1:8001; 来将请求反向代理给 uWSGI。
socket = 127.0.0.1:8001
# 当 uWSGI 退出时自动清理资源(如 Unix socket 文件)
vacuum = true
# 记录 uWSGI 主进程的 PID 文件,方便管理或关闭服务
# 有了这个配置,关闭uwsgi只需要运行:uwsgi --stop /tmp/uwsgi-linnote.pid
pidfile = /tmp/uwsgi-linnote.pid
# 将运行日志输出到指定日志文件
# 有了daemonize的配置,使uwsgi --ini uwsgi.ini变成后台启动
daemonize = /var/log/uwsgi-linnote.log
# 设置环境变量,这里用于让 settings/__init__.py 加载 prod.py
# uwsgi 使用 daemonize 或 systemd 启动后,不会继承当前 shell 的环境变量(比如 .bashrc 中设置的变量)。
env = DJANGO_ENV=prod
已经在
~/.bashrc
里面配置了export DJANGO_ENV=prod
,为什么还需要在uwsgi里面配置env = DJANGO_ENV=prod
?原因是:
uwsgi
使用daemonize
或 systemd 启动后,不会继承当前 shell 的环境变量(比如.bashrc
中设置的变量)。- 此时
DJANGO_ENV=prod
可能就不会被传入 Django,导致它默认加载dev.py
。只有显式写入
uwsgi.ini
的env = ...
才能确保环境变量在 uWSGI 进程中始终可用。
uwsgi --ini path/to/uwsgi.ini
:如果就在uwsgi.ini
的同级目录执行uwsgi
,那么就可以直接执行uwsgi --ini uwsgi.ini
(.venv) [root4uuZ LinNote]# ls
blog db.sqlite3 linauth LinNote manage.py requirements.txt static staticfiles templates uwsgi.ini
(.venv) [root4uuZ LinNote]# uwsgi --ini uwsgi.ini
[uWSGI] getting INI configuration from uwsgi.ini
[uWSGI] getting INI configuration from uwsgi.ini
:表示 uWSGI 成功读取并开始加载配置文件,但是这只是启动的第一步输出,完整判断是否启动成功,还需进一步验证。
查看是否有进程在运行:
bash (.venv) [root4uuZ LinNote]# ps aux | grep uwsgi root 13367 0.6 2.2 283404 40224 ? S 00:49 0:00 uwsgi --ini uwsgi.ini root 13376 0.0 1.8 283404 32844 ? S 00:49 0:00 uwsgi --ini uwsgi.ini root 13377 0.0 1.8 283404 32844 ? S 00:49 0:00 uwsgi --ini uwsgi.ini root 13378 0.0 1.8 283404 32844 ? S 00:49 0:00 uwsgi --ini uwsgi.ini root 13379 0.0 1.8 283404 32844 ? S 00:49 0:00 uwsgi --ini uwsgi.ini root 13405 0.0 0.0 112812 980 pts/0 S+ 00:50 0:00 grep --color=auto uwsgi
第一条是 uWSGI 的 master 主进程;其余是你配置的 4 个 worker 进程(对应
processes = 4
)。
bash root 13367 ... uwsgi --ini uwsgi.ini ← 主进程 root 13376 ... uwsgi --ini uwsgi.ini ← worker 1 root 13377 ... uwsgi --ini uwsgi.ini ← worker 2 root 13378 ... uwsgi --ini uwsgi.ini ← worker 3 root 13379 ... uwsgi --ini uwsgi.ini ← worker 4
因为主进程的 PID 最小(通常是启动最早的那一条)。
使用
pstree
查看进程树
bash (.venv) [root4uuZ LinNote]# pstree -p | grep uwsgi `-uwsgi(13367)-+-uwsgi(13376) |-uwsgi(13377) |-uwsgi(13378) `-uwsgi(13379)
默认情况下,uwsgi --ini uwsgi.ini
不是后台启动的,它会在前台运行。但是如果在 uwsgi.ini
中配置了这一行:
daemonize = /var/log/uwsgi-linnote.log
那 uWSGI 会自动以守护进程(后台)方式运行,并将日志写入这个文件中,而不是显示在当前终端窗口。
uwsgi --stop /tmp/uwsgi-linnote.pid
使用上面命令的前提是在 ini 文件中设置了 pidfile
,例如:
pidfile = /tmp/uwsgi-linnote.pid
sudo yum update -y
sudo yum install -y nginx gcc python3-devel
# 生产环境
DEBUG = False
ALLOWED_HOSTS = ['linnote.space', 'www.linnote.space', '127.0.0.1']
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'mediafiles'
# 开发环境
DEBUG = True
ALLOWED_HOSTS = ["*"]
STATIC_URL = '/static/'
# 开发环境用不到
# STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
# 开发环境直接存到media
MEDIA_ROOT = BASE_DIR / 'media'
文件路径:/etc/nginx/conf.d/linnote.conf
:
server {
listen 80;
# 监听 80 端口,即 HTTP 默认端口
server_name www.linnote.space linnote.space;
# 匹配的域名,用户访问这两个域名时由此 server 块处理
location /static/ {
alias /home/LinNote/staticfiles/;
# 当用户访问 /static/ 开头的 URL,例如 /static/css/style.css
# 实际会读取服务器上 /home/LinNote/staticfiles/css/style.css 的文件
# 注意:alias 末尾一定要加 /,表示整个 static 被替换
}
location /media/ {
alias /home/LinNote/mediafiles/;
# 与上面类似,用于用户上传的文件(图片、文档等)访问
# /media/uploads/avatar.jpg → /home/LinNote/mediafiles/uploads/avatar.jpg
}
location / {
include uwsgi_params;
# 加载标准的 uWSGI 参数,用于与 uWSGI 通信
uwsgi_pass 127.0.0.1:8001;
# 把除了 /static/ 和 /media/ 的请求都转发给本地的 uWSGI 服务(通过 socket 或端口通信)
# 这里 uWSGI 在 127.0.0.1:8001 上监听
}
}
为什么需要 /static/
和 /media/
用 Nginx 提供?
DEBUG=False
下 不会自动提供静态资源;alias
指的是路径替换:/static/
→ 实际的 /home/LinNote/staticfiles/
。uwsgi_pass
的作用?
location /
是默认入口,除了静态和媒体文件,其他请求都通过 uWSGI 传给 Django;127.0.0.1:8001
)或 Unix 文件与 uWSGI 通信;nginx -t
:测试 Nginx 配置语法是否正确(不重启服务)
nginx -t
nginx: [warn] conflicting server name "linnote.space" on 0.0.0.0:80, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
警告表示:你有多个 server
区块监听 0.0.0.0:80
(也就是 HTTP 默认端口),并且这些 server
块都配置了相同的 server_name linnote.space
。Nginx 不知道应该使用哪个 server
配置来响应 linnote.space
的请求,于是忽略了其中一个。
查看所有Nginx的配置:grep -r "server_name" /etc/nginx/
grep -r "server_name" /etc/nginx/
/etc/nginx/
/etc/nginx/conf.d/linnote.conf: server_name www.linnote.space linnote.space;
/etc/nginx/fastcgi_params:fastcgi_param SERVER_NAME $server_name;
/etc/nginx/fastcgi_params.default:fastcgi_param SERVER_NAME $server_name;
/etc/nginx/fastcgi.conf.default:fastcgi_param SERVER_NAME $server_name;
/etc/nginx/scgi_params:scgi_param SERVER_NAME $server_name;
/etc/nginx/uwsgi_params:uwsgi_param SERVER_NAME $server_name;
/etc/nginx/nginx.conf.default: server_name localhost;
/etc/nginx/nginx.conf.default: # server_name somename alias another.alias;
/etc/nginx/nginx.conf.default: # server_name localhost;
/etc/nginx/fastcgi.conf:fastcgi_param SERVER_NAME $server_name;
/etc/nginx/scgi_params.default:scgi_param SERVER_NAME $server_name;
/etc/nginx/nginx.conf: server_name linnote.space;
/etc/nginx/nginx.conf:# server_name _;
/etc/nginx/uwsgi_params.default:uwsgi_param SERVER_NAME $server_name;
发现在/etc/nginx/nginx.conf
下面配置了监控linnote.space
的配置。
打开这个文件,发现是之前hexo
的配置。
server {
listen 80;
listen [::]:80;
server_name linnote.space;
root /home/hexo;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
error_page 404 /404.html;
location = /404.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
将上面配置注释掉(使用#
注释),然后重新检查nginx -t
,重新加载Nginx配置(systemctl reload nginx
)
[root4uuZ ~]# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
[root4uuZ ~]# systemctl reload nginx
/etc/nginx/nginx.conf
是主配置文件。这是 Nginx 的入口配置文件,它控制整个 Nginx 的行为,包括是否加载 conf.d/
目录下的其他子配置文件。
在 nginx.conf
中,通常会有类似这一行:
include /etc/nginx/conf.d/*.conf;
这意味着:所有在 /etc/nginx/conf.d/
目录下以 .conf
结尾的文件都会被自动加载和生效。
当运行nginx -t
的时候,它其实会:
/etc/nginx/nginx.conf
开始解析include /etc/nginx/conf.d/*.conf;
/etc/nginx/conf.d/linnote.conf
,一起验证语法systemctl restart nginx
systemctl enable nginx
systemctl restart nginx
:
systemctl enable nginx
设置 Nginx 服务在开机时自动启动
创建一个系统级的 systemd
启动链接:
bash
Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.
直接使用http://www.linnote.space/
访问即可
Let's Encrypt 是一个免费的、自动化的、开放的 SSL/TLS 证书颁发机构(CA),由 非营利组织 Internet Security Research Group (ISRG) 运营。它的目标是让所有网站都能轻松启用 HTTPS 加密,从而提升互联网的安全性。
类型 | 验证方式 | 适用场景 | Let's Encrypt | 付费证书 |
---|---|---|---|---|
DV(Domain Validation) | 验证域名所有权 | 个人博客、小型网站 | ✅ 支持 | ✅ 支持 |
OV(Organization Validation) | 验证企业/组织真实性 | 企业官网、电商平台 | ❌ 不支持 | ✅ 支持 |
EV(Extended Validation) | 严格企业身份审核(浏览器显示公司名称) | 银行、金融、政府网站 | ❌ 不支持 | ✅ 支持 |
为什么有人需要 OV/EV 证书?
sudo yum install epel-release -y
sudo yum install certbot python2-certbot-nginx -y
yum
仓库中没有的额外软件包。为什么需要它?
certbot
(Let's Encrypt 的官方客户端工具)和python2-certbot-nginx
(Certbot 的 Nginx 插件)不在 CentOS 默认仓库中,必须通过 EPEL 安装。
certbot
:Let's Encrypt 官方推荐的 自动化 SSL 证书管理工具,用于:
免费获取 SSL/TLS 证书(支持通配符证书)。
自动配置 Web 服务器(如 Nginx、Apache)。
python2-certbot-nginx
:Certbot 的 Nginx 插件,提供以下功能:
自动识别 Nginx 配置中的 server_name
(域名)。
无法下载:yum install certbot python3-certbot-nginx -y
bash No package python3-certbot-nginx available.
在 CentOS 7 中,默认的 Python 版本是 2.7,因此当你运行
yum install python3-certbot-nginx
时,系统找不到这个包(因为 EPEL 仓库可能没有为 CentOS 7 提供基于 Python 3 的 Certbot Nginx 插件)。实际上,在 CentOS 7 上,Certbot 及其插件主要是针对 Python 2 的。因此,安装命令应该使用
python2-certbot-nginx
而不是python3-certbot-nginx
。
sudo certbot --nginx -d linnote.space -d www.linnote.space
然后按照提示操作:
bash
Enter email address (used for urgent renewal and security notices)
(Enter 'c' to cancel): 1909999999@qq.com
```bash
Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.5-February-24-2025.pdf. You must agree in order to register with the ACME server. Do you agree?
(Y)es/(N)o: Y ```
```bash
Would you be willing, once your first certificate is successfully issued, to share your email address with the Electronic Frontier Foundation, a founding partner of the Let's Encrypt project and the non-profit organization that develops Certbot? We'd like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom.
(Y)es/(N)o: Y ```
执行完成后,Nginx 配置会自动更新,生成 HTTPS 配置。
server {
server_name www.linnote.space linnote.space;
# 匹配的域名,用户访问这两个域名时由此 server 块处理
location /static/ {
alias /home/LinNote/staticfiles/;
# 当用户访问 /static/ 开头的 URL,例如 /static/css/style.css
# 实际会读取服务器上 /home/LinNote/staticfiles/css/style.css 的文件
# 注意:alias 末尾一定要加 /,表示整个 static 被替换
}
location /media/ {
alias /home/LinNote/mediafiles/;
# 与上面类似,用于用户上传的文件(图片、文档等)访问
# /media/uploads/avatar.jpg → /home/LinNote/mediafiles/uploads/avatar.jpg
}
location / {
include uwsgi_params;
# 加载标准的 uWSGI 参数,用于与 uWSGI 通信
uwsgi_pass 127.0.0.1:8001;
# 把除了 /static/ 和 /media/ 的请求都转发给本地的 uWSGI 服务(通过 socket 或端口通信)
# 这里 uWSGI 在 127.0.0.1:8001 上监听
}
# 监听 443 端口,启用 SSL/TLS 加密连接; Https
listen 443 ssl;
# SSL 证书路径(由 Certbot 自动管理)
ssl_certificate /etc/letsencrypt/live/linnote.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/linnote.space/privkey.pem;
# 包含 Certbot 提供的 SSL 优化配置(如加密套件、协议版本等)
include /etc/letsencrypt/options-ssl-nginx.conf;
# 使用 Diffie-Hellman 参数文件增强 SSL 安全性
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
# 如果请求的主机名是 www.linnote.space,则 301 重定向到 HTTPS
if ($host = www.linnote.space) {
return 301 https://$host$request_uri;
} # managed by Certbot
# 如果请求的主机名是 linnote.space,则 301 重定向到 HTTPS
if ($host = linnote.space) {
return 301 https://$host$request_uri;
} # managed by Certbot
# 监听 80 端口(HTTP)
listen 80;
# 定义服务器名称,匹配 www.linnote.space 和 linnote.space
server_name www.linnote.space linnote.space;
# 所有未匹配的请求返回 404 错误(防止无效域名访问)
return 404; # managed by Certbot
}
Let's Encrypt 证书有效期为 90 天,但可以自动续期。运行:
sudo certbot renew --dry-run
如果看到 Congratulations, all renewals succeeded
,说明自动续期配置无误。
会在/etc/letsencrypt/renewal/linnote.space.conf
里面生成一些配置,如下:
# 设置证书在到期前30天自动续期
# Certbot 默认会在证书到期前30天尝试自动续期。
# 如果未指定此选项,Certbot将采用默认值(即30天)。
# 因此,在这种情况下,即使不明确写出(注释掉)这个配置项,它的行为也不会改变。
# renew_before_expiry = 30 days
# Let's Encrypt客户端版本
version = 1.11.0
# 证书存档目录(包含历史版本的证书文件)
archive_dir = /etc/letsencrypt/archive/linnote.space
# 当前使用的证书文件路径
# 证书公钥
cert = /etc/letsencrypt/live/linnote.space/cert.pem
# 私钥
privkey = /etc/letsencrypt/live/linnote.space/privkey.pem
# 证书链
chain = /etc/letsencrypt/live/linnote.space/chain.pem
# 完整证书链(含证书+链)
fullchain = /etc/letsencrypt/live/linnote.space/fullchain.pem
# --- 以下是证书续期参数 ---
# Options used in the renewal process
[renewalparams]
# 使用 Nginx 作为认证方式(验证域名所有权)
authenticator = nginx
# 使用 Nginx 作为安装器(自动配置证书到 Nginx)
installer = nginx
# Let's Encrypt 账户 ID(唯一标识)
account = 87fd65c53a3e6132340f89137c84839e
# 是否允许手动记录公网 IP(默认 None 表示不记录)
manual_public_ip_logging_ok = None
# ACME 服务器地址(Let's Encrypt 生产环境 v2 API)
server = https://acme-v02.api.letsencrypt.org/directory
可以使用 SSL Labs 检查你的网站安全等级
#!/usr/bin/env bash
# 设置项目路径和虚拟环境路径
PROJECT_DIR="/home/LinNote"
VENV_ACTIVATE="$PROJECT_DIR/.venv/bin/activate"
UWSGI_INI="$PROJECT_DIR/uwsgi.ini"
PID_FILE="/tmp/uwsgi-linnote.pid"
# 切换到项目目录
cd "$PROJECT_DIR" || { echo "无法进入项目目录 $PROJECT_DIR"; exit 1; }
echo "[$(date)] 正在激活虚拟环境..."
if [ -f "$VENV_ACTIVATE" ]; then
echo "执行:source $VENV_ACTIVATE"
source "$VENV_ACTIVATE"
else
echo "虚拟环境未找到: $VENV_ACTIVATE"
exit 1
fi
echo "[$(date)] 正在停止 uWSGI..."
# 如果 PID 文件存在,则尝试停止 uWSGI
if [ -f "$PID_FILE" ]; then
echo "执行:uwsgi --stop $PID_FILE"
uwsgi --stop "$PID_FILE"
sleep 2 # 等待2秒确保进程关闭
else
echo "PID 文件不存在,可能没有运行中的 uWSGI 实例。"
fi
# 清理旧的 PID 文件
echo "[$(date)] 正在清理旧的 PID 文件..."
echo "执行:rm -f $PID_FILE"
rm -f "$PID_FILE"
# 启动 uWSGI
echo "[$(date)] 正在启动 uWSGI..."
echo "执行:uwsgi --ini $UWSGI_INI"
uwsgi --ini "$UWSGI_INI"
# 检查是否成功启动
# echo "$? - 检查 uWSGI 是否成功启动" # 我这里的本意是检查上一个命令的退出状态,
#但是加上之后 下面一行代码 $? 的取值最后取的是该echo的方法了
if [ $? -eq 0 ]; then
echo "[$(date)] Django 项目重启成功。"
else
echo "[$(date)] 启动失败,请检查配置文件或端口占用情况。"
exit 1
fi
echo
是一个非常基础且常用的命令行工具,它用于输出文本或变量内容到标准输出(通常是终端屏幕)。在 Shell 脚本中,echo 命令被广泛用来显示信息、调试脚本以及生成临时文件等。
exit n
:表示退出脚本,状态码为n
。
n=0
: 表示正常n!=0
表示异常||
||
是一个逻辑“或”操作符,在 shell 中表示:
if not success then ...
cd "$PROJECT_DIR" || { echo "无法进入项目目录 $PROJECT_DIR"; exit 1; }
所有上面整个命令就是:
cd "$PROJECT_DIR"
:进入到$PROJECT_DIR
里面||
:如果失败无法进入项目目录 $PROJECT_DIR
-f "$PID_FILE"
-f "$PID_FILE"
:这是 Bash Shell 脚本中的文件测试表达式(File Test Operator),用于判断某个文件是否存在并且是一个普通文件(而不是目录、设备文件等)。
-f
:这是一个条件测试操作符,用在 test
命令或者 [ ... ]
条件判断中,意思是:
“检查指定的路径是否是一个存在的普通文件”。
"$PID_FILE"
常见的文件测试操作符(File Test Operators)
[ -f /tmp/file.txt ] # 检查是否是一个存在的普通文件
[ -d /tmp/mydir ] # 检查是否是一个存在的目录
[ -e /tmp/something ] # 只要存在就为真,不关心类型
rm -f "$PID_FILE"
rm -f "$PID_FILE"
意思是强制删除 PID 文件(例如:/tmp/uwsgi-linnote.pid
)。虽然这不是 uWSGI 启动所必须的步骤,但在重启脚本中这么做是一个良好的实践,原因如下:
uWSGI 使用
.pid
文件来记录当前运行进程的 ID(PID)。如果上一次 uWSGI 是非正常关闭(比如断电、强制 kill、脚本未正确执行等),那么.pid
文件可能还存在。新启动时,uWSGI 可能会因为检测到旧的.pid
文件而报错:unable to remove pidfile
。所以在启动前手动清理旧的 PID 文件,可以防止这类错误。
每次启动 uWSGI 都会生成一个新的进程 ID。如果不清除旧的
.pid
文件,里面保存的是上一次的进程 ID,可能已经无效了。清理后重新生成的.pid
文件才能真实反映当前运行的进程 ID,方便后续管理或调试。
$? -eq 0
$?
: 在 Shell 中,这是一个特殊变量,它保存了上一个执行命令的退出状态码。
0
通常表示命令执行成功。1
, 2
, ...)则表示命令执行失败,并且不同的非零值可能代表不同类型的错误。-eq
: 这是一个比较操作符,在条件表达式中使用,表示“等于”(equal to)。此操作符用于整数比较。
$? -eq 0
的意思就是“检查上一条命令是否成功执行”。
uwsgi --ini "$UWSGI_INI"
# 执行 uwsgi --ini "$UWSGI_INI" 之后,$? 会存储该执行结果的状态码
# $? -eq 0 就是在检查 uwsgi --ini "$UWSGI_INI" 有没有执行成功
# 也就是:检查是否成功启动
if [ $? -eq 0 ]; then
echo "[$(date)] Django 项目重启成功。"
else
echo "[$(date)] 启动失败,请检查配置文件或端口占用情况。"
exit 1
fi
bash
或者sh
bash restart.sh
sh restart.sh
+x
)。bash
或 sh
)。./
chmod +x restart.sh
./restart.sh
./restart.sh
\#!
#!/bin/bash
这是一个 Shell 脚本的第一行,它被称为 shebang 或 hashbang。
这行代码告诉操作系统:“用 /bin/bash
这个解释器来运行这个脚本”。
#!
是 magic number(魔数),标识这是一个 shebang。/bin/bash
是 Bash shell 的路径,是大多数 Linux 系统上默认的 Shell 解释器。这个不是强制必须有的,但推荐加上:
chmod +x script.sh
)并直接运行 ./script.sh
时,系统就是靠这一行知道要用哪个程序来执行你的脚本。Shebang | 用途 |
---|---|
#!/bin/bash |
使用 Bash shell |
#!/bin/sh |
使用 POSIX shell(通常是 dash 或 bash 的软链接) |
#!/usr/bin/env bash |
使用 bash 解释器 |
#!/usr/bin/perl |
使用 Perl 解释器 |
#!/usr/bin/env bash 和
#!/bin/bash
有什么区别对比项 | #!/bin/bash |
#!/usr/bin/env bash |
---|---|---|
路径写法 | 使用绝对路径直接调用 bash | 使用 env 命令查找 PATH 中的 bash |
可移植性 | 可能不适用于所有系统(如 macOS) | 更加通用、适合跨平台 |
macOS 支持 | ❌ macOS 的 /bin/bash 是旧版本(通常为 3.2),且可能被移除 |
✅ 推荐方式,可以使用 Homebrew 安装的新版 bash |
沙箱/容器环境 | 如果 /bin/bash 不存在会失败 |
✅ 更灵活,只要 bash 在 PATH 中即可 |
安全性 / 控制性 | 更确定路径,避免环境变量干扰 | 受 PATH 影响,可能存在不可控风险 |
Linux 系统上:/bin
是 /usr/bin
的软链接(大多数现代 Linux 是这样)。所以使用/bin/bash
这个路径是可以找到bash
的。因此两种 shebang 都能正常工作
macOS 上: 默认 /bin/bash
是老版本 Bash(v3.2),甚至未来可能会被删除。如果你通过 Homebrew 安装了新版本 Bash(比如 /usr/local/bin/bash
),这个时候使用 #!/usr/bin/env bash
会使用 PATH 中最新的 Bash。但是如果使用 #!/bin/bash
则会使用旧版 Bash。
Docker 容器环境:可能没有 /bin/bash
(只有 /bin/sh
)但如果你安装了 Bash
并放在 /usr/bin
或其他 PATH 路径下使用 #!/usr/bin/env bash
就更有可能找到它。
加上\#!/usr/bin/env bash
并不会强制你不能用 sh script.sh
来运行脚本,但它会:
sh script.sh
,可能会导致行为异常(比如虚拟环境激活失败)比如,在我需要激活Python的虚拟环境的时候,我需要运行source /home/LinNote/.venv/bin/activate
。如果我使用sh restart.sh
去执行该脚本。那么可能执行失败,因为sh
不支持 source
命令激活虚拟环境(或者说即使执行了也不会保留激活状态)
[root@uuZ LinNote]# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Jun 28 2024 /bin/sh -> bash
有些时候,我们看似在执行sh
,但实际上执行的是bash
。在这样的环境中,/bin/sh
可能实际上是指向bash
的符号链接,而不是dash
或其他严格遵循POSIX的shell。因此,即使使用sh restart.sh
,实际上也是用bash
来执行脚本,所以支持source
命令。
为什么之前使用#!/bin/bash
时sh restart.sh
会失败?
失败的具体原因可能不是因为source
命令,而是脚本中其他对bash
特性的使用,或者可能是路径问题。
在sh restart.sh
的执行方式下,虽然/bin/sh
是bash
,但它是作为sh
调用的,因此会以POSIX兼容模式运行,这会禁用一些bash
的扩展特性。
修改git的/home/repo/LinNote.git/hooks/post-receive
钩子:
#!/usr/bin/env bash
TARGET="/home/LinNote" # 项目部署的目标目录(网站运行目录)
GIT_DIR="/home/repo/LinNote.git" # Git 裸仓库的路径
BRANCH="main" # 只监听这个分支的推送
RESTART_SCRIPT="/home/LinNote/restart.sh" # 重启脚本路径
while read oldrev newrev ref
do
# 只有当推送的是指定分支时才执行
if [[ $ref = refs/heads/$BRANCH ]];
then
# 部署代码
echo "Ref $ref received. Deploying ${BRANCH} branch to production..."
# 强制检出代码到目标目录
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f $BRANCH
echo "Deployment completed!"
# 执行重启脚本
if [ -f "$RESTART_SCRIPT" ]; then
echo "Executing restart script: $RESTART_SCRIPT"
# 执行重启脚本
bash "$RESTART_SCRIPT"
else
echo "Error: Restart script not found or not executable: $RESTART_SCRIPT" >&2
exit 1
fi
else
echo "Ref $ref received. Doing nothing: only the ${BRANCH} branch may be deployed."
fi
done
在Vim
中删除文件内容的几个命令:
操作 | 命令 |
---|---|
删除全文 | :%d |
删除某几行 | :10,20d (删除第 10 到 20 行) |
清空并退出 | :%d 然后 :wq |
不保存退出 | :%d 然后 :q! |
#本地推送代码:推送代码后会看到运行日志
git push -u origin main
root@'s password:
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 470 bytes | 470.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Ref refs/heads/main received. Deploying main branch to production...
remote: Already on 'main'
remote: Deployment completed!
remote: Executing restart script: /home/LinNote/restart.sh
remote: [Thu Jun 12 16:07:54 CST 2025] 正在激活虚拟环境...
remote: 执行:source /home/LinNote/.venv/bin/activate
remote: [Thu Jun 12 16:07:54 CST 2025] 正在停止 uWSGI...
remote: 执行:uwsgi --stop /tmp/uwsgi-linnote.pid
remote: [Thu Jun 12 16:07:56 CST 2025] 正在清理旧的 PID 文件...
remote: 执行:rm -f /tmp/uwsgi-linnote.pid
remote: [Thu Jun 12 16:07:56 CST 2025] 正在启动 uWSGI...
remote: 执行:uwsgi --ini /home/LinNote/uwsgi.ini
remote: [uWSGI] getting INI configuration from /home/LinNote/uwsgi.ini
remote: [Thu Jun 12 16:07:56 CST 2025] Django 项目重启成功。
To xxxx:/home/repo/LinNote.git
84408c6..0c88b78 main -> main
branch 'main' set up to track 'origin/main'.
# 在服务器端检验:
[root@ ~]# pstree -p | grep uwsgi
`-uwsgi(20959)-+-uwsgi(20969)
|-uwsgi(20970)
|-uwsgi(20971)
`-uwsgi(20972)
[root@ ~]# vim /home/repo/LinNote.git/hooks/post-receive
[root@ ~]# ls -l /home/repo/LinNote.git/hooks/post-receive
-rwxr-xr-x 1 root root 1127 Jun 12 16:05 /home/repo/LinNote.git/hooks/post-receive
[root@ ~]# pstree -p | grep uwsgi
`-uwsgi(21109)-+-uwsgi(21112)
|-uwsgi(21113)
|-uwsgi(21114)
`-uwsgi(21115)
(.venv) [root@ LinNote]# python manage.py createsuperuser
PROD environment detected, using production settings.
Username (leave blank to use 'root'): lin
Email address: 190@qq.com
Password:
Password (again):
Superuser created successfully.