全栈式部署练习项目:

(nginx,flask,mysql)

本项目记录了在 CentOS Stream 9 环境下,从零搭建一个完整任务管理系统(Todo List)的全过程。项目涵盖了数据库配置、后端接口开发、前端页面部署以及 Nginx 反向代理与 SELinux 安全加固。

Nginx web前端服务器的搭建

  • 环境&文件配置      

在开始前,要先部署好运行环境:                 

安装基础工具:Nginx 和 Node.js             

sudo yum install -y epel-release                 

sudo yum install -y nginx nodejs npm

创建一个放前端文件的文件夹(存放 .html, .css, .js)

mkdir -p /data/todo/frontend

(我们将项目放置在 /data 目录而非用户的 home 目录,这是为了方便多用户协作及权限统一管理)

后写入前端测速的文件:

vi /data/todo/frontend/index.html  

(配置文件会在上传的文件里)

给予文件夹执行权限:

chmod -R 755 /data/todo

-----------------------------------

4:读取---r

2:写入---w

1:执行---x

0:无权限  -

-----------------------------------

二.配置 Nginx 反向代理:

在 /etc/nginx/conf.d 下创建独立的配置文件:

vi /etc/nginx/conf.d/todo.conf

(配置文件会在上传的文件里)

加查语法和重启激活生效:

nginx -t  (如果末尾显示 test is successful,则配置正确 )

systemctl restart nginx

三.在防火墙和SElinux上放行:

1.防火墙上放行80(http)端口:

firewall-cmd --permanent --add-port=80/tcp  (永久开放 80 端口)

firewall-cmd --reload (重新加载防火墙使配置生效)

firewall-cmd --list-all (查看 80端口是否在列表中)

2.配置SELinux:

setenforce  0 (临时关闭)

setsebool -P httpd_can_network_connect 1 (运行进行网络连接)

chcon -Rt httpd_sys_content_t /data/todo(给SElinux打上正确的 Web 标签)

好了,现在可以去主机里去输入 http:// -前端web服务器IP-  来访问看看了

如果没有问题的是下面这个页面

可以f12打开网络这个模块看看

这里红色的报错是因为我们现在没有连接后端的aip

到这一步前端 Web 服务器(Nginx)配置已经基本完成了

好了接下来是在学习配置上遇到了一些问题和解决方法:

  1. 页面403(无法读取或发送这些文件):

首先直接去查看了/ var/log/nginx/ todo_error.log的报错日志文件:

2025/12/28 16:21:06 [error] 1550482#1550482: *1 open() "/data/todo/frontend/index.html" failed (13: Permission denied), client: 192.168.8.1, server: localhost, request: "GET / HTTP/1.1", host: "192.168.8.11"

核心报错翻译

open() "/data/todo/frontend/index.html" failed (13: Permission denied)

  • 动作:Nginx 想要打开(open)你的首页文件。
  • 路径:/data/todo/frontend/index.html。
  • 原因13: Permission denied(13号错误:权限不足)。

有三种原因:

  1. 文件本身的权限
  2. 路径上的文件夹权限
  3. SELinux 的标签不匹配

因为已经设置过路径文件夹和文件的权限了,所以这里直接锁定是问题 C

输入:setenforce 0  临时关闭后发现没有报错就确定是SELinux 的标签不匹配

chcon -Rt httpd_sys_content_t /data/todo

给目录打上正确的 Web 标签后解决

  1. 页面不更新:

使用ctrl+f5强制刷新无效后

删除原有的网页文件:

rm -f /data/todo/frontend/index.html

让后重新写入一个没有问题的文件(也许是代码格式有问题或者写错了?)

  1. 还有一些命令和代码的拼写错误导致各种的问题…..细心写入和仔细检查就能解决

flask后端服务器部署

  • 环境的部署:

1.因为部署的是flask,所以要准备python环境

sudo dnf update -y

sudo dnf install python3 python3-pip python3-devel -y

2.创建专门放后端的文件:在todo文件夹下创建一个专门放后端文件的backend

(后面因为SElinux的原因把整个todo文件夹移去了/var/www下)

3.Cd到backend目录下创建并激活虚拟环境:

python -m venv venv

source venv/bin/activate

( 激活后,你的命令行提示符前会多出一个 (venv) )

4.安装flask:

pip install flask gunicorn

  • 编写后端的python代码

在backend目录下创建一个app.py并写入文件:

vi app.py

接着就可以写入代码了:

------------------------------------------------------------------------------

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/api/tasks', methods=['GET'])

def hello():

        return jsonify({

                    "status": "success",

                            "message": "这是来自后端的响应!",

                                    "data": [1, 2, 3, 4, 5]

                                        })

        if __name__ == '__main__':

                app.run(host='0.0.0.0', port=3000)

~                                                          

------------------------------------------------------------------------------

注意那俩处标记的地方:

/api/tasks要和前端的连接部分对应

端口也要和前后端都一至

三.为了能保证进程不会中断的正常和保证服务的自启动

1.我们要配一下systemd:

sudo vi /etc/systemd/system/flask_app.service

------------------------------------------------------------------------------

[Unit]

Description=Flask Backend

Service After=network.target

[Service]

User=qqq

Group=qqq

WorkingDirectory=/var/www/todo/backend

# 路径一定要写对,这是刚才新建的 venv 路径 ExecStart=/var/www/todo/backend/venv/bin/gunicorn -w 3 -b 127.0.0.1:3000 app:app

Restart=always

[Install]

WantedBy=multi-user.target

------------------------------------------------------------------------------

注意标记的地方

WorkingDirectory:后端的文件夹         3000:的前后端一至的端口

2.经典重启三连:

刷新配置:sudo systemctl daemon-reload

启动服务:sudo systemctl restart flask_app

查看状态:sudo systemctl status flask_app

(或许会报错,不过没关系,如果文件夹和文件权限问题的话大概率就是SE的问题,

可以先 sudo setenforce 0 看看,如果可以就是SE的问题,接下来就会去解决)

  • 配置SElinux:
  1. 先把测试时关了的重新打开:

sudo setenforce 1

2. 解决 Nginx 的“断连”问题:

sudo setsebool -P httpd_can_network_connect 1

3.永久修复目录标签:

确保selinux管理工具没有问题:

sudo dnf install policycoreutils-python-utils -y

4. 告诉系统:/var/www/todo 及其子目录都是 Web 服务的内容

sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/todo(/.*)?"

5. 允许虚拟环境 bin 目录下的东西可以被执行:

sudo semanage fcontext -a -t bin_t "/var/www/todo/backend/venv/bin(/.*)?"

6.应用这些刚刚配置的规则:

sudo restorecon -Rv /var/www/todo

接下来就是重启验证一下了:

sudo systemctl restart flask_app

sudo systemctl status flask_app

接下来就是去web看看结果了:

这样就是代表着已经成功接入后端了

接下来是我再学习配置时遇到的问题和解决方法

1. 进程管理问题:status=203/EXEC 报错

  • 现象:手动运行 python app.py 成功,但执行 systemctl start flask_app 报错 203/EXEC。
  • 原因
    1. 路径错误:Systemd 找不到 gunicorn 的绝对路径。
    2. 权限不足:虽然文件属于你,但 Systemd 切换到 qqq 用户后,由于父目录没有 +x 权限,导致其无法“穿透”进入目录执行程序。
  • 解决方法
    • 使用 which gunicorn 确定绝对路径并写入 .service 文件。
    • 对父级目录执行 chmod +x,确保用户可以访问路径下的文件

2. 文件所有权(Ownership)混乱

  • 现象:使用 sudo 操作后,原本属于 qqq 的文件变成了 root 所有,导致 qqq 运行的后端服务无法读取配置文件。
  • 解决方法:使用 sudo chown -R qqq:qqq /var/www/todo 重新声明“主权”。

3. 目录架构问题:虚拟环境(venv)的“水土不服”

  • 现象:将项目从 /data 搬到 /var/www 后,后端彻底无法启动。
  • 原因虚拟环境的路径依赖。venv 内部的脚本硬编码了创建时的绝对路径(/data/...),移动目录后,脚本找不到原来的 Python 解释器。
  • 解决方法:彻底删除旧的 venv 文件夹,在新的 /var/www/todo 目录下执行 python3 -m venv venv 重新创建并重装依赖。

4. Nginx 配置逻辑冲突

  • 现象 1 (502):服务显示绿色的 running,但网页报错 502。
    • 原因端口不匹配。后端配置了监听 5000 端口,但 Nginx 的 proxy_pass 写的是 3000。
    • 解决:统两端的端口号(全部改回 3000)。
  • 现象 2 (404):后端接口通了,但前端网页找不到了。
    • 原因:项目搬家后,Nginx 配置文件里的 root 指令还指向旧的路径。
    • 解决:修改 Nginx .conf 文件中的 root 路径并重启 Nginx:

打开 Nginx 配置:

sudo vi /etc/nginx/conf.d/todo.conf

找到 root 这一行,把路径改成新的路径:

root /var/www/todo/frontend

然后重启一下nginx:

sudo systemctl restart nginx

5. SELinux 安全策略拦截(最大挑战)

  • 现象:在 setenforce 0(宽容模式)下一切正常,一旦 setenforce 1 就报错 502 或服务无法启动。
  • 细分问题与对策
    • 拦截 Nginx 联网:SELinux 默认不准 Nginx 连接后端端口。
      • 解决:执行 sudo setsebool -P httpd_can_network_connect 1。
    • 拦截 Systemd 执行非标目录文件:SELinux 不信任 /data 下的二进制文件。
      • 解决:将项目搬迁到标准 Web 目录 /var/www,并使用 restorecon 刷新文件安全上下文(标签)。
    • 二进制文件标签不对:Gunicorn 被视为可疑程序。
      • 解决:使用 chcon -t bin_t 或 semanage 永久标记其为合法的可执行程序。

mysql数据库服务器部署

  • 首先要为了防止冲突,先清理一下环境

sudo dnf remove mariadb mariadb-server -y

sudo dnf remove mysql mysql-server mysql-common mysql-libs -y

把系统默认的 MySQL 模块关了

sudo dnf module disable mysql -y

二.安装mysql数据库

导入mysql官方社区源

sudo dnf install https://dev.mysql.com/get/mysql80-community-release-el9-1.noarch.rpm -y

安装mysql服务器

sudo dnf install mysql-community-server -y

启动服务&设置自启

sudo systemctl start mysqld

sudo systemctl enable mysqld

三.配置sql密码:

寻找临时密码:

sudo grep 'temporary password' /var/log/mysqld.log

一般会有这样的信息

2025-12-18T09:00:00.123456Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: ;k2>f8#pX&z1

接下了我们拿这个临时密码进去修改密码

ALTER USER 'root'@'localhost' IDENTIFIED BY ' …… ' ;

四.创建数据库和用户

1. 创建业务数据库

CREATE DATABASE todo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

2.创建 Python 专用的用户(名字叫 qqq,密码设为 qqq)

——因为我把密码策略改了,所以这里密码可以设置的非常简单

CREATE USER 'qqq'@'127.0.0.1' IDENTIFIED BY 'qqq';

3. 给这个用户授权

GRANT ALL PRIVILEGES ON todo.* TO 'qqq'@'127.0.0.1';

4.接下来刷新权限就好了

Flush privileges ;

五.在数据库里插入点数据:

1. 切换到我们需要操作的数据库

USE todo;

2. 创建一张叫 tasks 的表

CREATE TABLE tasks (

id INT AUTO_INCREMENT PRIMARY KEY,

content VARCHAR(255) NOT NULL,

 status TINYINT(1) DEFAULT 0

 );

-- id: 自动编号,content: 任务内容,status: 完成状态(0是未完成)

3. 往表里塞两条测试数据(上货)

INSERT INTO tasks (content) VALUES ('这是数据库里的第一条数据');

INSERT INTO tasks (content) VALUES ('这是数据库里的第二条数据');

4.验证一下有没有写进去:

SELECT * FROM tasks;

五.接下来编辑python文件

先进入虚拟环境

cd /var/www/todo/backend && source venv/bin/activate

安装一下依赖文件

pip install pymysql cryptography

接下来就是把之前后端的文件改一下

vi app.py

from flask import Flask, jsonify

import pymysql

app = Flask(__name__)

# 1. 配置数据库连接信息

db_config = {

    'host': '127.0.0.1',

    'user': 'qqq',

    'password': 'qqq',  # 刚才 SQL 语句里设的密码

    'database': 'todo',

    'charset': 'utf8mb4',

    'cursorclass': pymysql.cursors.DictCursor

}

@app.route('/api/tasks')

def get_tasks():

    try:

        # 2. 建立连接

        conn = pymysql.connect(**db_config)

       

        with conn.cursor() as cursor:

            # 3. 执行 SQL 查询

            cursor.execute("SELECT * FROM tasks")

            # 4. 获取所有结果

            result = cursor.fetchall()

       

        # 5. 关闭连接

        conn.close()

       

        # 6. 返回 JSON 数据

        return jsonify({

            "status": "success",

            "data": result

        })

       

    except Exception as e:

        # 如果连不上,把错误打印出来方便调试

        print(f"数据库连接失败: {e}")

        return jsonify({"status": "error", "message": str(e)}), 500

if __name__ == '__main__':

    app.run()

改成这个注意标记的那些地方

接下来就是SE的授权

sudo setsebool -P httpd_can_network_connect_db 1

然后重启一下后端

sudo systemctl restart flask_app

就可以去浏览器看看了

接下来就是我在学习时遇到的问题和解决方案

1.Mysql密码不在日志里面,登陆不了

先把mysql服务停止了

sudo systemctl stop mysqld

然后去开启‘免密模式’

修改配置文件,告诉 MySQL 启动时不要查密码

sudo vi /etc/my.cnf

[mysqld] 这一行的下面写入

skip-grant-tables

进去重置密码

Mysql -u root  (不需要密码了)

进来先刷新然后更改密码:

FLUSH PRIVILEGES;

ALTER USER 'root'@'localhost' IDENTIFIED BY ' …… ' ;

好了后还要把免密登陆关闭

Sudo vi/etc/my.cnf

把刚刚写入的那行 skip-grant-tables #注释掉或者直接删除这行

然后重启服务就好了

sudo systemctl restart mysqld

2.在配置后端文件时因为缩进问题导致flask启动失败和各种报错

解决方法:

vi自动缩进修复功能

  快速输入 gg(跳转到文件开头)。

  按下 等于号 键和 大写 G:=G

在粘贴之前,先开启 Paste 模式:

: set paste

这是前端优化后代码

<!DOCTYPE html>

<html lang="zh-CN">

<head>

    <meta charset="UTF-8">

    <title>我的全栈运维练习</title>

    <style>

        body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }

        h1 { color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }

        .status-box { background: #e8f5e9; padding: 15px; border-radius: 5px; color: #2e7d32; margin-bottom: 20px;}

        table { width: 100%; border-collapse: collapse; margin-top: 20px; }

        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }

        th { background-color: #f2f2f2; }

        .tag { padding: 4px 8px; border-radius: 4px; font-size: 0.85em; }

        .done { background: #d1fae5; color: #065f46; }

        .todo { background: #fee2e2; color: #991b1b; }

    </style>

</head>

<body>

    <h1>📋 任务管理系统</h1>

   

    <div id="status" class="status-box">正在连接后端 API...</div>

    <table>

        <thead>

            <tr>

                <th>ID</th>

                <th>任务内容</th>

                <th>状态</th>

            </tr>

        </thead>

        <tbody id="task-list">

            <!-- 这里的 JavaScript 会自动填入数据 -->

        </tbody>

    </table>

    <script>

        // 1. 定义 API 地址

        const apiUrl = '/api/tasks';

        // 2. 获取数据

        fetch(apiUrl)

            .then(response => {

                if (!response.ok) {

                    throw new Error(`HTTP 错误! 状态码: ${response.status}`);

                }

                return response.json();

            })

            .then(res => {

                const statusBox = document.getElementById('status');

                const list = document.getElementById('task-list');

                // 显示连接成功信息

                statusBox.textContent = `✅ 后端连接成功!数据来源:${res.source || 'MySQL 数据库'}`;

               

                // 3. 渲染列表

                list.innerHTML = ''; // 清空旧数据

                res.data.forEach(task => {

                    const row = document.createElement('tr');

                    const statusHtml = task.status

                        ? '<span class="tag done">已完成</span>'

                        : '<span class="tag todo">待办</span>';

                   

                    row.innerHTML = `

                        <td>${task.id}</td>

                        <td>${task.content}</td>

                        <td>${statusHtml}</td>

                    `;

                    list.appendChild(row);

                });

            })

            .catch(error => {

                const statusBox = document.getElementById('status');

                statusBox.textContent = `❌ 连接失败: ${error.message}`;

                statusBox.style.backgroundColor = '#fee2e2';

                statusBox.style.color = '#991b1b';

            });

    </script>

</body>

</html>

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐