用树莓派托管个人博客网站(一):搭建和部署Web应用

使用一块小巧的树莓派来托管个人博客网站是非常经济的选择,也是一个深入学习Web应用服务底层机制的项目。本文将介绍如何使用 Python 的 Flask 框架开发轻量级博客应用,并通过 gunicorn 作为 WSGI 服务器处理应用请求,再结合 nginx 工具作为反向代理和静态文件服务器,实现高性能、稳定的部署方案。


🎯 项目目标

本文的目标是搭建一个高度可定制的静态博客网站。由于本人习惯使用 Typora 写作,所以网站要支持 Markdown 文章管理。网站要使用标准 HTTP 端口(80),便于网络访问。另外,网站要保留未来升级为动态博客的可能性,Flask 框架可以满足这一要求。项目的整体架构如下:

text
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
用户浏览器
                     │
                     ▼
                http://树莓派IP (端口 80)
                     │
                     ▼
                   nginx ← 监听 80 端口(对外服务入口)
                     │
                     ├── 静态文件(/static/...)→ 直接返回文件(不经过 Python)
                     │
                     └── 动态请求(/.../post/...)→ 转发给 gunicorn (127.0.0.1:8000)
                                                     │
                                                     ▼
                                                 Flask 应用处理业务逻辑

🧩 技术栈组成

Web 框架:Flask

本项目关于 Web 应用的包管理采用 Conda ,网页应用的功能实现是基于 Flask 框架。Flask 是用 Python 写的一个基于 Jinja2 模板的轻量级 Web 应用框架,可以用来快速构建 Web 应用的核心功能。Jinja2 是一个为 Python 设计的、功能强大的模板引擎,常用于生成动态文本输出,如HTML、JSON等。Jinja2 模板中含有大量 Python 代码的语法元素,对熟悉 Python 的人来说很容易阅读。我们用 Jinja2 写好一个生成网页内容的模板,然后用 Flask 来调用。Flask 作为 Web 应用的核心,它负责:

  • 接收 HTTP 请求,如用户访问 /post/welcome
  • 根据 URL route调用对应的函数,如 def post(name):
  • 读取文件并处理数据,如读取包含文章内容的文件;
  • 渲染模板,比如把按照 Jinja2 语法写好的网站页面模板和文章内容合成最终 HTML;
  • 返回 HTTP 响应给浏览器。

WSGI:gunicorn

gunicorn 是一个 WSGI(Web Server Gateway Interface),是 Flask 应用和 Web 服务器之间的「桥梁」,高效处理并发请求。gunicorn 会监听本地端口(默认地址是 127.0.0.1:8000),接收来自 nginx 的请求。因为 Flask 本身不是为高并发设计的,而 gunicorn 可以启动多个工作进程(workers),可以同时处理多个用户请求。

Web 服务器:nginx

nginx 是面向公网的高性能反向代理服务器,处理所有外部流量。nginx 负责:

  • 监听标准 HTTP 端口(80)并接收来自互联网的所有请求;
  • 将动态请求(如 /post/xxx)转发给 gunicorn;
  • 直接提供静态文件(如 CSS、JS、图片),速度更快;
  • 提供 SSL/TLS(HTTPS)、缓存、压缩、负载均衡等高级功能;
  • 防止直接暴露 gunicorn 到公网,提升安全性。

Tip

之所以称之为「反向代理」,是因为人们习惯于将一个替客户端访问外部资源的程序(比如VPN)称之为代理,而 nginx 扮演的角色正好是反过来的,它是一个替外部用户访问服务器内部资源的程序,故而称之为反向代理。

进程管理:systemd

systemd 是 Linux 系统的标准服务管理器,通过 myblog.service 文件定义如何启动/停止你的应用,可以确保 gunicorn 服务和 nginx 服务在后台稳定运行,崩溃自动重启,开机自动启动。

如果将整个 Web 应用比作一家餐厅,Flask 就像「厨师」,我们用 Flask 框架编写的各种响应函数要分别负责理解顾客点单(URL),准备食材(读取文章内容),炒菜(渲染页面),然后上菜(返回网页)。gunicorn 就像厨房经理,管理多个厨师(worker 进程),协调他们同时处理多个订单。nginx 就像「餐厅前台接待员」,负责接待所有客人(用户请求),把点餐请求交给厨房经理(gunicorn),把饮料、小食(静态文件)直接从吧台递过去(更快)。systemd 就像餐厅的店长,确保厨房每天准时开门,厨师生病了马上找替补,设备坏了立刻维修。


🛠️准备项目环境

项目名称设置为 MyBlog,将 Flask 应用文件放置在这个文件夹里,并创建 posts,templates 和 static 三个文件夹,期中 posts 文件夹用于存放要发布到网页上的 markdown 文件,templates 文件夹用于存放 HTML 模板文件,static 文件夹用于存放静态文件。这是使用 Flask 框架搭建 Web 应用的标准步骤。

Bash
1
                2
                3
                4
# 创建项目目录
                # -p 参数表示 parents,意思是递归创建目录
                mkdir -p ~/MyBlog/posts ~/MyBlog/templates ~/MyBlog/static
                cd ~/MyBlog

在 MyBlog 目录下编写 Web 应用程序之前,需要先用 Conda 创建一个虚拟环境,并安装好需要用到的包。

Bash
1
                2
                3
                4
                5
                6
                7
                8
# -n 参数即 name,表示虚拟环境的名字
                conda create -n pythonBlog python=3.11
                
                # 激活虚拟环境
                conda activate pythonBlog
                
                # 安装项目需要用到的包
                conda install flask gunicorn

📝 编写 Flask 应用和HTML模板

编写 Flask 应用

首先要想好网页应用的基本功能,本项目先实现一个简单的个人博客网站的功能:打开网站主页后,用户可以看到博客网站的标题,下边是一个文章列表;用户点击列表中的任意一篇文章后,可以跳转至文章内容页面。基于此,我们可以先写好 app.py 的内容。

python
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                29
                30
                31
                32
                33
                34
                35
                36
                37
#app.py
                from flask import Flask, Response
                from pathlib import Path
                
                # Flask 是 flask 包里代表整个 Web 应用的一个类
                # 首先定义一个类代表整个 Web 应用,只需要传入一个参数作为这个类的名字,这里就采用本文件的名字
                app = Flask(__name__)
                
                # 如果用户点击网站主页,响应 index() 函数,功能为展示网站的主页
                @app.route('/')
                def index():
                    posts_dir = Path("./posts")
                    posts_dir.mkdir(exist_ok=True)
                    # 提取 "./posts" 路径下的所有文章的名字,得到一个文章列表
                    files = [f.stem for f in posts_dir.glob("*.html")]
                    # 网站的主页是由 Jinja2 模板构建的 index.html 文件,采用 render_template 函数插入文章列表
                    return render_template('index.html', files=files)
                
                # 如果用户点击文章列表中一篇名为 name 的文章,响应 show_post(name) 函数
                @app.route('/post/<name>')
                def show_post(name):
                    posts_dir = Path("./posts")
                    file_path = posts_dir / f"{name}.html"
                    
                    # 如果文章不存在,返回报错信息
                    if not file_path.exists():
                        return "文章不存在", 404
                
                    # 读取文章内容,读取的编码方式设置为 utf-8,读取的内容写入变量 content
                    with open(file_path, 'r', encoding='utf-8') as f:
                        content = f.read()
                
                    # 使用 Response 函数返回文章内容,参数 mimetype 是用来告诉浏览器返回文件的类型
                    return Response(content, mimetype='text/html')
                
                if __name__ == '__main__':
                    app.run(host='127.0.0.1', port=5000, debug=True)

@app.route 就是装饰器(decorator),它可以把下面的函数「包装」起来,给下面的函数添加额外的功能。比如说在这里,@app.route('/')index() 添加了这样的功能:当用户访问网站主页时就要执行。同理,@app.route('/post/<name>')show_post(name) 添加了这样的功能:当用户访问 /post/<name> 这条 URL 时就要执行。URL(Uniform Resource Locator)即通用资源定位符,俗称网址或者网页链接,这个链接是在 HTML 文本中定义好的,与 Web 应用服务器的内部文件管理路径相互独立。

MIME(Multipurpose Internet Mail Extensions)最初用于电子邮件,后来被 Web 广泛采用,用来标识文件或数据的格式。在 HTTP 响应头中,服务器通过 Content-Type 字段发送 MIME 类型,浏览器据此决定将接收到的数据显示为网页,或者下载为文件,抑或者是渲染为图片、播放音频……

MIME 类型 含义
text/html HTML 文档 → 浏览器渲染成网页
application/json JSON 数据 → 通常用于 API
text/plain 纯文本 → 直接显示,不解析标签
image/png PNG 图片 → 显示图像
application/octet-stream 二进制流 → 通常触发下载

Tip

127.0.0.1 这个 IP 地址是 IPv4 的「本地环回地址」(loopback address),它的作用就是让一台电脑自己跟自己通信。任何发给 127.0.0.1 的数据都不会真的进入网络,而是直接在操作系统内部原路返回。因此它常被用来:

  • 测试本机网络协议是否正常,比如使用命令 ping 127.0.0.1
  • 开发阶段运行 Web 服务器、数据库等服务,只允许本机访问,避免暴露给外网。

Web 应用在编写阶段可以使用这个地址对应用功能进行本地调试。

编写 HTML 模板

编写主页基本功能

一个标准的 HTML 文件由以下几个部分构成:

html
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
<html lang="zh-CN">
                    <head>
                        <meta charset="UTF-8">
                        <style>
                        	/* 
                            这个区域设置文档各个模块的样式,包括
                            字体:font-family, font-size, font-weight
                            页边距:margin, margin-top, margin-bottom, margin-left, margin-right
                            设置内容盒子:box-sizing, box-radius
                            盒子内留白:padding, padding-top, padding-bottom, padding-left, padding-right
                            */
                        </style>
                        <body>
                            <!--> 放置文章内容的部分 </-->
                        </body>
                    </head>
                </html>

根据目标,可以写出一个简单的网站主页的设置,包括全局设置(字体和内容盒子)、全局限制最大宽度(内容容器)、头部区域设置(标题)、主体区域设置(列表样式)。

html
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                29
                30
                31
                32
                33
                34
                35
                36
                37
                38
                39
                40
                41
                42
                43
                44
                45
                46
                47
                48
                49
                50
                51
                52
                53
                54
                55
                56
                57
                58
                59
                60
                61
                62
                63
                64
                65
                66
                67
                68
                69
                70
                71
                72
                73
                74
                75
                76
                77
                78
                79
                80
                81
                82
                83
                84
                85
                86
                87
                88
                89
                90
                91
                92
                93
                94
                95
                96
                97
<!DOCTYPE html>
                <html lang="zh-CN">
                <head>
                    <meta charset="UTF-8">
                    <!-- 设置视口,适配移动设备 -->
                    <meta name="viewport" content="width=device-width, initial-scale=1">
                    <style>
                        /* 全局重置样式:消除默认 margin/padding,统一盒模型,设置默认字体 */
                        * {
                            margin: 0;
                            padding: 0;
                            box-sizing: border-box;
                            /* Typora 风格的字体 */
                            font-family: "Helvetica Neue", Helvetica, Arial, "Segoe UI Emoji", "SF Pro", sans-serif;
                        }
                
                        /* 博客头部区域:标题 */
                        .header {
                            display: flex;
                            align-items: center;     /* 垂直居中 */
                            justify-content: center; /* 水平居中 */
                            margin-bottom: 24px;     /* 与下方内容的间距 */
                        }
                
                        /* 博客标题样式 */
                        .blog-title {
                            font-size: 2em;
                            font-weight: bold;
                            color: #2c3e50;          /* 深灰蓝色,提升可读性 */
                        }
                
                        /* 页面整体背景与文字颜色 */
                        body {
                            background-color: white;
                            color: #333;
                            line-height: 1.6;        /* 行高更舒适 */
                            padding: 20px;           /* 页面四周留白 */
                        }
                
                        /* 内容容器:限制最大宽度并居中 */
                        .container {
                            max-width: 1024px;
                            margin: 0 auto;
                        }
                
                        /* 文章列表链接样式 */
                        .post-list a {
                            display: block;                  /* 让 <a> 占满一行 */
                            padding: 12px 0;                 /* 上下内边距 */
                            color: #2c3e50;                  /* 默认文字颜色 */
                            text-decoration: none;           /* 去掉下划线 */
                            font-size: 1em;
                            font-weight: bold;
                            border-bottom: 1px solid #ddd;   /* 每篇文章底部加分割线(加深) */
                        }
                
                        /* 最后一篇文章不显示底部分割线 */
                        .post-list a:last-child {
                            border-bottom: none;
                        }
                
                        /* 无文章时的提示信息 */
                        .no-posts {
                            text-align: center;
                            color: #7f8c8d;   /* 灰色文字 */
                            font-size: 1em;
                            padding: 20px 0;
                        }
                    </style>
                </head>
                <body>
                    <!-- 主内容容器 -->
                    <div class="container">
                        <!-- 博客头部:标题 -->
                        <div class="header">
                            <!-- 使用 Flask 的 url_for 动态生成静态文件路径 -->
                            <h1 class="blog-title">Hoigin's Blog</h1>
                        </div>
                
                        <!-- 文章列表区域,使用 Jinja2 语法生成 -->
                        <div class="post-list">
                            <!-- 如果存在文章文件(files 变量由后端传入) -->
                            {% if files %}
                            {% for file in files %}
                            <!-- 每篇文章生成一个链接,点击跳转到 show_post 路由 -->
                            <a href="{{ url_for('show_post', name=file) }}">{{ file }}</a>
                            {% endfor %}
                            {% else %}
                            <!-- 若无文章,显示提示信息 -->
                            <p class="no-posts">
                                暂无文章。请在 <code>posts/</code> 目录下添加 <code>.html</code> 文件。
                            </p>
                            {% endif %}
                        </div>
                    </div>
                </body>
                </html>

Note

.post-list a { ... } 是一个复合选择器(descendant selector),意思是选中所有在 .post-list 容器内的 <a> 标签(也就是文章链接),用于统一设置这些文章链接的样式。

.post-list a:last-child { ... } 中单冒号 : 表示「伪类」,用来赋予某一类 <a> 标签一些特殊的用途。常用的伪类有:

类别 语法示例 典型用途
动态伪类 :hover 鼠标悬停效果
  :focus 表单元素获得焦点时
  :active 元素被点击瞬间
链接伪类 :link 未访问链接
  :visited 已访问链接
结构伪类 :first-child 第一个子元素
  :last-child 最后一个子元素
  :nth-child(n) 第 n 个子元素
状态伪类 :checked 单选/复选框被选中
  :disabled 表单元素被禁用
  :valid / :invalid 表单验证状态

美化主页展示外观

在以上模板的基础上,主页的内容还可以进一步丰富,比如:

  1. 添加一个圆角边框,在上述模板中插入:
html
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
<html>
                    <style>
                    	/* 卡片容器:模拟「纸张」效果,带阴影和圆角 */
                        .card {
                            background: white;
                            border: 1px solid #ccc;          /* 边框(稍深) */
                            border-radius: 12px;             /* 圆角 */
                            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); /* 阴影增强立体感 */
                            overflow: hidden;                /* 防止子元素溢出圆角 */
                            padding: 30px;                   /* 内边距 */
                        }
                    </style>
                    <body>
                        <div class="card">
                            ...
                        </div>
                    </body>
                </html>
  1. 在标题前面添加一个头像:
html
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
<html>
                    <style>
                    	/* 头像样式:圆形、固定尺寸、带边框 */
                        .avatar {
                            width: 60px;
                            height: 60px;
                            border-radius: 50%;      /* 圆形 */
                            object-fit: cover;       /* 图片裁剪填充 */
                            border: 1px solid #eee;  /* 浅灰色边框 */
                        }
                    </style>
                    <body>
                        <!-- 博客头部:包含头像和标题 -->
                        <div class="header">
                            <!-- 使用 Flask 的 url_for 动态生成静态文件路径 -->
                            <img src="{{ url_for('static', filename='avatar.jpg') }}" 
                                 alt="Avatar" class="avatar">
                            <span class="blog-title">Hoigin's Blog</span>
                        </div>
                    </body>
                </html>
  1. 在每一篇文章标题前面加上一个📄的图标,并且鼠标悬停文章标题时把文章链接渲染成红色:
html
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
<html>
                    <style>
                        /* 在每篇文章链接前添加图标(📄) */
                        .post-list a::before {
                            content: "📄 ";
                            margin-right: 8px;
                        }
                    	/* 鼠标悬停时变红色,增强交互反馈 */
                        .post-list a:hover {
                            color: #e74c3c;  /* 鲜艳的红色 */
                        }
                    </style>
                    <body>
                        
                    </body>
                </html>

Note

.post-list a::before 中双冒号 :: 表示「伪元素」,用来赋予 <a> 标签一些额外的元素或者样式。常见的伪元素有:

语法示例 典型用途
::before 在元素内容前插入装饰内容
::after 在元素内容后插入装饰内容
::first-letter 段落首字母样式
::first-line 段落首行样式
::selection 用户选中文本的背景
  1. 移动端窄屏幕的适配:
html
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
<html>
                    <style>
                    	/* 移动端适配:屏幕宽度 ≤ 600px 时 */
                        @media (max-width: 600px) {
                            .card {
                                padding: 18px;           /* 减小内边距 */
                                border-width: 1px;       /* 显式保留边框(防止某些浏览器忽略) */
                            }
                            h1 {
                                font-size: 1.6em;        /* 缩小标题字号 */
                            }
                        }
                    </style>
                </html>

Note

在 CSS 中,以 @ 开头的语句叫做 「at-rule」(中文常译为 「@规则」 或 「at 规则」),它是一种特殊的 CSS 语法指令,用于定义样式表的条件、行为或元信息。常见的@规则有:

@规则 作用
@media 媒体查询:根据设备特性(如屏幕宽度)应用不同样式
@import 导入其他 CSS 文件
@font-face 自定义字体
@keyframes 定义动画关键帧
@supports 根据浏览器是否支持某 CSS 特性来应用样式

🌐 部署Web服务

测试 Flask

首先在本地测试一下是否可以正常访问前面写好的 Web 应用。在树莓派终端启动 app.py 应用,如下所示:

bash
1
                2
                3
                4
                5
# 激活虚拟环境
                conda activate pythonBlog
                
                # 启动 app.py 应用
                python app.py

接着打开另一个终端,输入:

bash
1
                2
                3
# curl 命令用于发送网络请求,可以测试 API 接口或者调试 Web 服务
                # localhost 域名解析为 127.0.0.1,即回环地址
                curl http://localhost:5000

如果看到终端返回了网站主页的 HTML 文本,则说明 Web 服务能正常工作。接下来可以打开浏览器,输入 http://localhost:5000 ,这时应该能正常访问网页,可以点击页面链接再次测试一下网站是否正常工作。

配置 gunicorn 之前可以把 app.py 中的监听地址 127.0.0.1 改成 0.0.0.0 再用局域网内另外一台主机测试一下网络应用的连通性。0.0.0.0 这个 IP 地址在网络编程和服务器配置中一般被称为通配符地址(wildcard address)。它不是一个真正的 IP 地址,而是一个特殊的保留地址。当在服务器上启动一个 Web 服务并绑定到这个地址时,就是告诉主机在所有可用的 IPv4 网络接口(包括 127.0.0.1192.168.x.x、公网 IP 等)上监听这个端口。

配置 gunicorn 和 systemd

首先测试 gunicorn 是否能正常工作。以下命令可以启动 gunicorn 服务器程序,寻找并运行当前工作路径下的 Python Web 应用。

Bash
1
                2
                3
                4
# --workers 2 表示启动2个工作进程
                # --bind 127.0.0.1:8000 表示绑定到 IP 地址 127.0.0.1 的8000端口
                # app:app 中第一个 app 是 Python 文件名,第二个 app 是文件中 Flask 应用实例的名字
                gunicorn --workers 2 --bind 127.0.0.1:8000 app:app

与测试 Flask 应用的方法相同,此时打开另一个终端使用 curl 命令,或者使用本机浏览器,都可以测试 gunicorn 是否正常工作。

需要注意的是,这里 gunicorn 是通过将 app.py 导入来执行 app.py 中的函数的,也就是说 app.py__name__=='__main__' 这一判断条件不成立,所以 Flask 内置的 Web 请求监听函数 app.run(...) 是不会执行的,它只是在开发调试过程中用于测试而已,之后可以把这一段删掉。

接下来,将 gunicorn 托管的 Flask 应用加入系统服务,由 systemd 来管理。首先要找到 gunicorn 所在的安装路径:

Bash
1
                2
which gunicorn
                # 记下输出路径,如 /home/pi/miniforge3/envs/pythonBlog/bin/gunicorn

然后,创建 systemd 服务文件,命名为 myblog.service:

Bash
1
sudo nano /etc/systemd/system/myblog.service

内容如下(根据实际路径修改):

ini
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
[Unit]
                Description=MyBlog Flask Application
                After=network.target
                
                [Service]
                User=pi
                WorkingDirectory=/home/pi/MyBlog
                Environment="PATH=/home/pi/miniforge3/envs/pythonBlog/bin"
                ExecStart=/home/pi/miniforge3/envs/pythonBlog/bin/gunicorn --workers 2 --bind 127.0.0.1:8000 app:app
                Restart=always
                RestartSec=5
                
                [Install]
                WantedBy=multi-user.target

接着,启动这项服务:

Bash
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
# 重新加载 systemd 配置
                sudo systemctl daemon-reload
                
                # 启动服务
                sudo systemctl start myblog
                
                # 设置开机自启动
                sudo systemctl enable myblog
                
                # 检查服务状态
                sudo systemctl status myblog

配置 nginx

首先安装好 nginx 工具,安装好之后 nginx 会自动启动并监听80端口。

Bash
1
                2
# -y 表示 yes,即同意所有条款和要求,一键安装
                sudo apt install nginx -y

接下来在 /etc/nginx/ 路径下创建 nginx 配置文件。树莓派上运行的 Debian 系统约定:

  • /etc/nginx/sites-available/:存放所有可用的站点配置
  • /etc/nginx/sites-enabled/:存放已启用的站点配置(通过软链接激活)

「站点配置」(site configuration)指的是为一个独立 Web 应用所定义的一组规则,包括监听哪个端口(如 80443)、绑定哪个域名或 IP(server_name)、请求如何处理(代理到后端?返回静态文件?重定向?)、日志路径、SSL 证书、安全头等等。Debian 的这种设计便于快速启用/禁用站点,只需要在新建或者删除 /etc/nginx/sites-enabled/ 路径下的软链接即可。

所以,第一步先在 /etc/nginx/sites-available/路径下创建站点配置。

Bash
1
sudo vim /etc/nginx/sites-available/MyBlog

首先做一个简单的示例,配置内容如下:

nginx
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
server {
                    # 监听 80 端口(HTTP 默认端口)
                    listen 80;
                    # server_name 用于匹配域名,_ 是一个无效主机名,表示匹配任意域名或 IP 直接访问
                    server_name _;
                
                    # 所有以 / 开头的请求(即几乎所有动态页面),都转发给本地的 gunicorn 服务(运行在 127.0.0.1:8000)
                    location / {
                        proxy_pass http://127.0.0.1:8000;
                        # 传递原始请求信息给 Flask,否则 Flask 会以为所有请求都来自 127.0.0.1
                        # Host:用户访问的域名(如 192.168.1.100)
                        proxy_set_header Host $host;
                        # X-Real-IP:用户的真实 IP
                        proxy_set_header X-Real-IP $remote_addr;
                        # X-Forwarded-For:完整的 IP 链(防代理伪造)
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    }
                
                    # 当用户请求 /static/style.css,Nginx 直接从磁盘读取文件返回,不经过 gunicorn/Flask
                    location /static {
                        alias /home/pi/MyBlog/static;
                        # 设置 HTTP 缓存头,让浏览器 30 天内不再重复下载
                        expires 30d;
                    }
                }

接着,第二步去 /etc/nginx/sites-enabled/ 路径下激活软链接以启用站点配置。

Bash
1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
# 创建软链接,相当于激活这个配置
                # nginx 启动时会加载 sites-enabled/ 下的所有配置
                # ln 表示 link,即创建链接
                # -s 表示 soft,即指定是软连接
                sudo ln -s /etc/nginx/sites-available/MyBlog /etc/nginx/sites-enabled/
                
                # 删除默认站点(可选)
                # 如果不删,当自定义的配置有问题时,用户可能看到默认欢迎页
                sudo rm /etc/nginx/sites-enabled/default
                
                # 测试配置语法是否正确
                # 非常重要!避免因配置错误导致 nginx 崩溃
                # -t 表示 test,即测试
                sudo nginx -t
                
                # 重载 nginx
                # nginx 会平滑应用新配置,不中断现有连接
                sudo systemctl reload nginx

验证部署

检查 gunicorn 和 nginx 的服务状态是否正常。

Bash
1
                2
sudo systemctl status myblog    # 确保 gunicorn 在运行
                sudo systemctl status nginx     # 确保 nginx 在运行

使用 ss (socket statistics)工具检查 gunicorn 和 nginx 的端口占用情况是否正常。

Bash
1
                2
ss -tuln | grep :80   # 应该看到 nginx 监听 80 端口
                ss -tuln | grep :8000 # 应该看到 gunicorn 监听 8000 端口

直接访问网站 http://<树莓派IP>,检查 Web 应用是否能正常工作。

Note

ss(socket statistics)工具是现代 Linux 系统中查看网络连接、监听端口和套接字状态的高效命令,比传统的 netstat 更快、更强大。

参数 全称 / 含义 作用
-t TCP sockets 显示 TCP 协议 的连接和监听端口
-u UDP sockets 显示 UDP 协议 的连接和监听端口
-l Listening sockets 只显示 处于监听状态(LISTEN)的端口(即服务正在等待连接)
-n Numeric 不解析服务名和主机名,直接显示 IP 和端口号(例如显示 :80 而不是 :http

🔧 常用管理命令

重启服务:

Bash
1
                2
                3
                4
                5
# 重启 gunicorn 服务
                sudo systemctl restart myblog
                
                # 重启 nginx 服务
                sudo systemctl reload nginx

查看日志:

Bash
1
                2
                3
                4
                5
                6
                7
                8
# 查看 gunicorn 服务日志
                sudo journalctl -u myblog -f
                
                # 查看 nginx 访问日志
                sudo tail -f /var/log/nginx/access.log
                
                # 查看 nginx 错误日志
                sudo tail -f /var/log/nginx/error.log

添加新文章:将写好的文章导出成 HTML 格式,放入 /posts/ 目录即可。


🛡️ 安全建议

配置防火墙:

Bash
1
                2
                3
                4
                5
sudo apt install ufw -y
                sudo ufw allow ssh
                sudo ufw allow 80
                sudo ufw default deny incoming
                sudo ufw enable