部署Django项目到Centos7上

创建于 2025-06-12 12:37 | 浏览次数 398


目录

title: 部署Django项目到Centos7上 date: 2025-05-23 15:06:00 categories: 运维 tags: - python - Django - Bootstrap5 - JQuery


环境

  • CentOS7.9;root用户
  • Python3.12
  • Django5
  • Sqlite3

源码安装 OpenSSL

问题引入

在执行编译的时候(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

为什么不能通过yum下载更高版本的openssl-devel

CentOS 7 最长支持期为 2024 年底,系统默认的软件包依赖 OpenSSL 1.0.2(如 SSHyum 自身等),所以官方不升级 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.solibcrypto.so),而不是静态库。Python 编译需要这个
zlib 启用 zlib 支持(压缩功能)。

让Python使用openssl-3.5.0

./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这个配置

安装SQLite3

问题引入

在执行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 停止维护

CentOS 7.9 已于 2024 年 6 月停止更新

  • CentOS 7 使用的是 非常老旧的包仓库
  • 官方 YUM 源中包含的 SQLite 版本大多为 3.7.x
  • 所以你通过 yum install sqlite 安装到的是旧版本,根本无法满足 Django 的要求
# 查了yum 中 sqlite 的信息
yum info sqlite

Name        : sqlite
Arch        : x86_64
Version     : 3.7.17

安装Sqlite3

# 安装依赖
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 

让 Python 使用新 SQLite(关键)

如果我们只是安装了最新的Sqlite,但是并没有让Pyhton去使用它,那么当我们使用python manage.py migrate的时候,还是会报如下错误:

django.db.utils.NotSupportedError: deterministic=True requires SQLite 3.8.3 or higher

具体如何关联,需要看Python的编译

卸载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

安装Python3

默认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(调试器)
  • binutilslibtoolautoconf

如果包下载的不完整,在执行编译的时候(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源码

下载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里面,找到:文件-->打开新文件数可以里面的文件进行上传和删除操作。但是有一个缺点,无法看到上传进度。

编译安装Python

附带openssl

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 启用额外优化(比如 PGOLTO),提高 Python 运行性能(编译时间更长)。
--with-ensurepip=install 编译后自动安装 pip
--with-openssl=/usr/local/openssl-3.5.0 指定使用你自己编译安装的 OpenSSL 路径,而不是系统默认的旧版本(重要)

--with-openssl 的作用 这个选项明确告诉 Python 的 configure 脚本 OpenSSL 的安装位置,它会:

  • 自动设置 OpenSSL 的头文件路径(相当于 -I/usr/local/openssl-3.5.0/include
  • 自动设置 OpenSSL 的库路径(相当于 -L/usr/local/openssl-3.5.0/lib

但是,OpenSSL 3.5.0 的库文件(libssl.so.3libcrypto.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/

附带openssl和Sqlite3

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

CPPFLAGS

作用:设置编译器寻找头文件(.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

LDFLAGS

作用:设置链接器寻找动态库(.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

同样,如果你没设置 -rpathLD_LIBRARY_PATH,你运行 Python 时会报错:

error while loading shared libraries: libssl.so: cannot open shared object file

LD_LIBRARY_PATH

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_PATH

这个变量是给工具 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 的参数,最终被传递给 gccld

--enable-optimizations

到底做了什么

它会在构建过程中启用 Python 性能优化编译流程,特别是使用 PGO(Profile-Guided Optimization)和 LTO(Link Time Optimization)。

PGO(Profile-Guided Optimization):启用这个选项后,构建流程会:

  • 编译一套临时的 Python 解释器;
  • 运行 python 的测试脚本收集运行时的性能数据;
  • 使用这些数据重新编译 Python,以生成更高性能的可执行文件。

LTO(Link Time Optimization):会在链接阶段做全程序优化,提升运行效率。

这个选项为什么会导致编译失败

原因一:PGO 会运行测试用例,依赖完整的系统环境和正确链接

当使用 --enable-optimizations 时,构建脚本会运行 ./python -m test 来收集性能数据。这个过程中会调用大量标准库和 C 扩展模块。如果你用的是自己安装的 SQLite(非系统默认路径 /usr),而没有正确设置 LD_LIBRARY_PATHrpath,那么:

  • Python 的临时解释器运行时找不到对应的 .so 文件;
  • 某些模块(如 _sqlite3)无法导入;
  • 结果就是测试阶段失败,从而导致整个编译流程中断。

原因二:LTO 对某些平台(如 CentOS 7)不友好

CentOS 7 使用较旧版本的 GCC(除非你升级),可能对 LTO 支持不完善,容易在链接阶段出错,尤其在涉及自定义 OpenSSL/SQLite 路径时。

LTO

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

PGO = Profile-Guided Optimization,又称为“性能分析引导优化”。它的核心思想是:让程序先运行一遍,从中“学习”热点函数、常用路径,然后再用这些信息重新编译,生成性能更优的程序。


PGO 的工作流程

  • 编译生成插桩版本(instrumented binary):这一步生成的 Python 是“带监控”的,每次运行时会记录函数调用次数、分支命中率等。
  • 运行程序生成性能数据:用这个插桩的 Python 去跑一套典型的用例。或者在你自己的项目上运行你典型的负载。这一步会生成一个 .gcda.profdata 数据文件。

  • 重新编译,使用收集的性能数据:这一步,编译器会根据你收集的使用信息,优化常走路径、常用函数、减少 cache miss 等等,生成真正优化后的可执行文件。

PGO与编译失败

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测试阶段需要运行时库路径

PGO的特殊执行环境

  • 会启动一个最小化的 Python 解释器 (_bootstrap_python)
  • 该解释器不继承你的终端环境变量
  • 却需要加载完整的标准库(包括依赖 SQLite3 的模块)

态库搜索机制:运行时如何找 .so 文件?

Linux 下,当一个程序运行时要加载动态库(比如 libsqlite3.so),它会按如下顺序查找:

  1. rpath(编译时设置的运行时路径,固定在 ELF 文件中)
  2. LD_LIBRARY_PATH(环境变量,运行时可变)
  3. /etc/ld.so.cache(系统缓存,来自 ldconfig
  4. 默认路径:/lib, /usr/lib, /lib64, /usr/lib64

反思

在编译Python的时候,我花了一个星期达到时间在这个问题上。但实际上,最后仍然是没有找到答案。

  • 我所有的路径都配置了。LD_LIBRARY_PATH配置了,但是编译还是失败
  • 但是,除去 --enable-optimizations 这个配置的时候,就可以配置成功,到底是为什么很难理解。
  • 也有可能我的是centOS7.9的版本,和PGOLTO 有些不匹配。
  • 总之,这次是真正感受到Docker的意义。

验证

# 安装后验证 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 ≡ make all:显式调用编译全部目标模块的过程。多数情况下就是构建主程序(这里是 Python 可执行文件)。

make -j$(nproc):多核并行编译,提高速度。

  • -j 指并行编译,$(nproc) 是获取当前 CPU 核心数的命令(比如 4 核返回 4)。
  • 相当于告诉 make:可以最多使用 N 个线程同时编译

  • 好处:编译速度显著加快。

假设你有 8 核 CPU:

命令 并发 效果
make 单线程 慢,顺序执行
make all 单线程 同上
make -j8make -j$(nproc) 8 线程 快,适合大项目如 Python

特性 make clean make distclean
主要目的 清理编译生成的中间文件和目标文件 彻底恢复源码目录到初始状态,包括配置生成的文件
删除内容 通常删除 .o 文件、静态库、动态库等 clean 的内容外,还删除配置文件(如 Makefile.in)、依赖文件、临时文件等
是否删除配置 不删除配置相关的文件 删除配置生成的文件(如 configure 生成的 Makefile
使用场景 重新编译前清理旧的编译结果 发布源码前或彻底重置项目目录时
依赖关系 通常作为基础清理目标 通常依赖 clean,并执行更彻底的清理
自动化工具关联 可能由编译器自动生成 常见于 Autotools(如 automake)项目

配置pip镜像源

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 ```

Docker的意义

问题类型 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

# 并安装 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 进入。

裸仓库 vs 普通仓库

特性 裸仓库(--bare 普通仓库(非裸)
工作区 ❌ 无(不能直接编辑文件) ✅ 有(可直接修改代码)
用途 作为远程中央仓库(仅接收推送) 本地开发使用
典型路径 /opt/repos/project.git/ ~/projects/my-project/
cd 进入 ✅ 是(因为是目录) ✅ 是

为什么用裸仓库?

  • 安全性:避免直接修改服务器代码(必须通过 git push 同步)。
  • 标准化:类似 GitHub/GitLab 的远程仓库设计。
  • 自动化:配合钩子(如 post-receive)实现自动部署。

设置 Git Hook 同步代码

在裸仓库里创建 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 是否匹配 BRANCHmain),只有匹配时才执行部署。

强制检出代码到目标目录:也就是把代码同步到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
  • Django 会自动去你设置的 STATICFILES_DIRS(比如 static/)中查找这类文件

collectstatic 是做什么用的?

  • 它会将所有静态文件(包含 app 中的 static/STATICFILES_DIRS 中的文件)复制STATIC_ROOT 中。
  • 在生产环境部署时,由 nginx 或其他 Web 服务器 读取这些文件并服务。

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 会:

  1. 匹配上你上面这条路由。
  2. "bootstrap5/bootstrap.min.css" 作为参数传给 serve 视图。
  3. settings.STATIC_ROOT + 'bootstrap5/bootstrap.min.css' 找到文件,返回它的内容。
  4. 即:/home/LinNote/staticfiles/bootstrap5/bootstrap.min.css

访问服务器

python manage.py runserver 0.0.0.0:80

  1. 指定了80端口:只能用http请求
  2. 配置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+Nginx部署

介绍

uWSGI 是什么?

uWSGI 是一种应用服务器,用于运行 Python Web 应用(比如 Django、Flask)。

它支持 WSGI 协议,能高效处理请求,比 Django 自带的 runserver 稳定、安全、性能更好。

Nginx 是什么?

Nginx 是一个高性能 Web 服务器和反向代理服务器。

它接收用户请求、处理 HTTPS、缓存静态资源,然后把请求转发给 uWSGI。

为什么要用它们?

组件 作用
Nginx 接收公网请求、处理 HTTPS、缓存静态资源、反向代理到 uWSGI
uWSGI 实际运行 Django 项目、处理动态内容请求(比如查询数据库)
Django 专注处理业务逻辑,不直接暴露给公网

uWSGI

下载uWSGI:

# 先在本地下载:
pip install uwsgi
# 导入到 requirements.txt
pip freeze > requirements.txt
# 把变化的文件通过git推送到远程
git push -u origin main  

# 远程:下载
pip install -r requirements.txt

uWSGI的配置:

[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.inienv = ... 才能确保环境变量在 uWSGI 进程中始终可用。

启动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

uwsgi --stop /tmp/uwsgi-linnote.pid

使用上面命令的前提是在 ini 文件中设置了 pidfile,例如:

pidfile = /tmp/uwsgi-linnote.pid

Nginx

安装Nginx

sudo yum update -y
sudo yum install -y nginx gcc python3-devel

保证Django有如下配置

# 生产环境
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'

配置Nginx

文件路径:/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 提供?

  • Django 在 DEBUG=False不会自动提供静态资源
  • 用 Nginx 直接提供静态文件效率远远高于 Django;
  • alias 指的是路径替换:/static/ → 实际的 /home/LinNote/staticfiles/

uwsgi_pass 的作用?

  • location / 是默认入口,除了静态和媒体文件,其他请求都通过 uWSGI 传给 Django;
  • 它通过 socket(如 127.0.0.1:8001)或 Unix 文件与 uWSGI 通信;
  • uWSGI 会根据 Django 的 URL 路由返回动态页面。

启动Nginx

检查Nginx配置

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

为什么有多个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,一起验证语法

重启 Nginx 服务

systemctl restart nginx
systemctl enable nginx

systemctl restart nginx

  • 重启 Nginx 服务:会先停止 Nginx,然后重新启动。
  • 适用于配置修改后,或者希望清除某些缓存状态时使用。
  • 一般用于正式应用配置后让其生效(例如启用 HTTPS、新站点等)。

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/访问即可

配置HTTPS

Let's Encrypt

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 证书?

  • 增强用户信任:浏览器地址栏会显示企业名称(EV 证书),适合银行、电商等对信任要求高的场景。
  • 合规要求:某些行业(如支付、医疗)强制要求 OV/EV 证书。

安装 Certbot 和 Nginx 插件

sudo yum install epel-release -y
sudo yum install certbot python2-certbot-nginx -y
  • EPEL(Extra Packages for Enterprise Linux)是 RHEL/CentOS 的官方扩展软件源,提供许多默认 yum 仓库中没有的额外软件包。

为什么需要它? certbot(Let's Encrypt 的官方客户端工具)和 python2-certbot-nginx(Certbot 的 Nginx 插件)不在 CentOS 默认仓库中,必须通过 EPEL 安装。

  • certbot:Let's Encrypt 官方推荐的 自动化 SSL 证书管理工具,用于:

  • 免费获取 SSL/TLS 证书(支持通配符证书)。

  • 自动续期证书(Let's Encrypt 证书有效期为 90 天)。
  • 自动配置 Web 服务器(如 Nginx、Apache)。

  • python2-certbot-nginx:Certbot 的 Nginx 插件,提供以下功能:

  • 自动识别 Nginx 配置中的 server_name(域名)。

  • 自动修改 Nginx 配置以启用 HTTPS。
  • 在证书续期时自动重新加载 Nginx。

无法下载: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

使用 Certbot 自动申请和配置 HTTPS

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 ```

  • 选择是否自动重定向 HTTP -> HTTPS(建议选 重定向

```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

echo 是一个非常基础且常用的命令行工具,它用于输出文本或变量内容到标准输出(通常是终端屏幕)。在 Shell 脚本中,echo 命令被广泛用来显示信息、调试脚本以及生成临时文件等。

exit

exit n:表示退出脚本,状态码为n

  • n=0: 表示正常
  • n!=0表示异常

||

||是一个逻辑“或”操作符,在 shell 中表示:

  • 如果前面的命令执行失败(返回非0状态码),则执行后面的命令。
  • 类似于编程语言中的 if not success then ...
cd "$PROJECT_DIR" || { echo "无法进入项目目录 $PROJECT_DIR"; exit 1; }

所有上面整个命令就是:

  • cd "$PROJECT_DIR":进入到$PROJECT_DIR里面
  • ||:如果失败
  • 在终端屏幕输出:无法进入项目目录 $PROJECT_DIR
  • 接着退出脚本,返回状态码为1

-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 文件,可以防止这类错误。

  • 确保 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)。
  • 使用指定的 shell 解释器运行脚本(bashsh)。

方式二:./

chmod +x restart.sh
./restart.sh
  • 给脚本添加可执行权限后直接运行./restart.sh

第一行的:\#!

举例说明

#!/bin/bash

这是一个 Shell 脚本的第一行,它被称为 shebanghashbang

这行代码告诉操作系统:“用 /bin/bash 这个解释器来运行这个脚本”

  • #! 是 magic number(魔数),标识这是一个 shebang。
  • /bin/bash 是 Bash shell 的路径,是大多数 Linux 系统上默认的 Shell 解释器。

这个不是强制必须有的,但推荐加上:

  • 明确指定解释器:如果你不加这一行,系统会使用当前 shell 来执行脚本(比如你用的是 bash、zsh、dash 等),可能会导致行为不一致。
  • 可执行权限下运行脚本时需要:当你给脚本加上了执行权限(chmod +x script.sh)并直接运行 ./script.sh 时,系统就是靠这一行知道要用哪个程序来执行你的脚本

其他常见 shebang 示例

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 来运行脚本,但它会:

  • 明确告诉使用者:这个脚本是为 Bash 写的
  • 如果你误用 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/bashsh restart.sh会失败?

  • 失败的具体原因可能不是因为source命令,而是脚本中其他对bash特性的使用,或者可能是路径问题。

  • sh restart.sh的执行方式下,虽然/bin/shbash,但它是作为sh调用的,因此会以POSIX兼容模式运行,这会禁用一些bash的扩展特性

使用git作自动部署

脚本

修改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)

创建superuser用户

(.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.