Bootstrap

OpenSUSE+nginx+gunicorn+django+redis+postgresql网站部署

之前曾经在Ubuntu下安装过类似的一个网站,不过Ubuntu自带的软件包不太符合我的需求,而且实际用起来我感觉Ubuntu速度有点慢,所以后来想还是切换到openSUSE

这是之前在Ubuntu下折腾的记录

Ubuntu+nginx+gunicorn+django+redis+postgresql网站部署_silent_missile的博客-CSDN博客_gunicorn redis

在OpenSUSE服务器上部署一个自己开发的网站,网站比较复杂,用到了很多组件,这些组件如标题所示,需要一步步进行部署。这里把我摸索的步骤记录下来,以供未来参考

首先是数据库服务,因为数据库和其他的软件没有依赖关系。

我的站点因功能原因,写和读的量差不多,而且访问量也不大,所以选择了postgresql

首先是安装,openSUSE自带了postgresql的安装包,直接zypper安装就可以。

openSUSE安装的postgresql默认是没有密码的,但是没有密码却无法登录,所以直接用数据库管理员权限强行登陆后再修改设定密码。

sudo systemctl start postgresql

启动数据库服务后,会在
/var/lib/pgsql
目录下生成data目录,那里面就是postgresql的数据存放目录。

然后

su
su postgres
psql

通过su命令切换为postgresql的拥有者postgres用户,这个用户是一个uid小于1000的用户,默认的登录界面是看不到的,实际上也是无法登录的,只能用来启动后台服务,这样可以保证安全。不过可以通过su命令切换为这个用户,然后进行一些只有这个用户才能做的工作,比如在无密码的情况下登录postgresql服务器

进入sql界面以后,可以设置数据库管理员密码

\password postgres

按照提示设定好postgresql数据库服务的管理员密码,这个密码不是系统中的postgres用户的登陆密码,实际上postgres这个用户默认是不允许登陆的,所以只能通过转换为root,然后再切换为postgres的方法来使用该用户。然后

\q

退出sql命令,此时bash界面里的当前用户还是postgres用户,需要退出,可以直接用exit命令或者关闭当前会话

编辑/var/lib/pgsql/data/postgresql.conf
找到第63行、64行
listen_addresses = '*'
port = 5432
如果特别在意安全,也可以把监听地址设置为localhost

再编辑/var/lib/pgsql/data/pg_hba.conf
#TYPE   Database    User    Address     Method
local   all             all                                     scram-sha-256
host    all             all             127.0.0.1/32            scram-sha-256
host    all             all             ::1/128                 scram-sha-256
把method都设定为scram-sha-256。要注意,如果是早期的发行版,自带的postgresql版本低于13.x的话,是不支持scram-sha-256加密的,要设置为md5;只有14以上的版本才能顺利支持scram-sha-256

这两个文件的权限归属为postgres用户,所以用一般登陆用户编辑不了,但是如果使用root编辑,kde的kate编辑器会把权限归属变成root,这也不行。所以要么继续用刚刚切换的postgres用户,在命令行里用postgres用户完成编辑,要么root编辑后修改文件归属。如果权限错误,那么服务将无法启动。

然后

sudo systemctl restart postgresql

重新启动数据库服务器,前面的设置生效。然后以普通用户运行

psql -U postgres -W

按照提示输入刚刚设定的密码,以数据库管理员身份登录数据库服务器。然后创建普通用户和对应的数据库,这里为了凸显通用性,用户名和数据库名是不同的,不过实践中,用户名和数据库名一般是相同的。

postgres=# create user username with password '****';
CREATE ROLE
postgres=# create database dbtest owner username; -- 创建数据库指定所属者
CREATE DATABASE
postgres=# grant all on database dbtest to username; -- 将dbtest所有权限赋值给username
GRANT
postgres=#

需要注意:

1. 要以英文分号结尾

2.密码需要引号包裹

然后\q退出,再以刚刚创建的用户登录上来

psql -U username

看看用户是否已经生效。

另外Django是通过psycopg来访问postgresql的。而psycopg在psycopg2时代是通过

pip install psycopg2

来安装的。

到了现在已经是psycopg3了,可以用

pip install psycopg[binary,pool]

安装人家已经编译好的二进制文件。

也可以通过

pip install psycopg[C]

从源代码编译生成,我一般采用这种。但是这就要求,系统内有相关的头文件,也就是必须要把dev包装上。

valkey缓存服务器

流行的缓存服务器从memcached进化到redis,后来redis改了许可协议,于是有人fork了valkey,兼容redis

和数据库一样valkey缓存服务器也不依赖其他的服务。openSUSE也有rpm的包,直接zypper安装即可

默认情况下,valkey服务是开机启动的,而且没有密码,所以如果没有特别的安全性要求,可以直接用,不过既然我把所有的服务都放到一台服务器上,用户既然能够访问网关,自然也就能够访问valkey服务。所以肯定不能这样暴露,密码一定要设置。

首先进入valkey配置文件存放的目录,openSUSE默认该目录不允许一般用户进入,所以要切换成root

su
cd /etc/valkey

然后把默认的例子配置文件复制一份作为自己要用的

cp default.conf.example default.conf 

root复制的文件默认用户和分组都是root,这里对文件权限的要求是分组必须是valkey,所以要调整一下文件所有人

chgrp valkey default.conf

然后打开这个文件进行编辑

找到requirepass的那一行,默认是注释掉的,把注释取消掉,然后修改为

requirepass redispassword

另外阿里云的主机在申请时可以设定是否ipv6,如果仅仅是ipv4,没有v6,那么需要在配置文件中找到bind行修改

# bind 127.0.0.1 ::1
bind 127.0.0.1

要注意和前面postgresql的配置文件一样,如果不用root是无法编辑的,但是如果用了root,就一定要注意保存之后的用户所有人变化,这个配置文件要求拥有人是root,拥有组是valkey。如果保存后文件所有权变了,记得用chmod和chgrp调整一下。

chown root:valkey /etc/valkey/default.conf

然后重启valkey服务器

启动时要用

systemctl start valkey@default

@后面是配置文件的文件名(不包含扩展名conf)

另外可能会涉及到另一个文件
/var/log/valkey/default.log
如果出现了访问权限问题,可以先删除,然后再重启服务即可

如果是从redis升级过来的,那么可能在/etc/valkey/default.conf中这一行还是

/var/log/redis/default.log

这样就会出现访问权限问题,只要到/etc/valkey/default.conf中把这一行改成

/var/log/valkey/default.log

就行了

如果还有问题,那就需要把bind这一行注释掉,启动valkey服务之后可以再改回来127.0.0.1,然后再重启valkey

与之对应的,django网站的代码也需要作出修改

传统上,一般使用django-redis插件来实现,不过这个插件已经很久不更新了。到了Django4,已经不需要任何插件就可以直接访问redis服务,而valkey兼容redis服务,所以不用更改,直接用就行。实际上Django4是通过redis包来访问的,所以

pip install redis

把valkey的密码加入settings.py
 

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": ["redis://:你的密码@服务器地址:6379/0"]
    }
}

“你的密码”自然就是刚刚设置的valkey密码,“服务器地址”这个选项,则是因为valkey可以通过网络远程访问,如果所有的服务都设置在一台服务器上,那么直接写localhost即可

创建虚拟环境

python可以创建虚拟环境,不同的虚拟环境可以搭配不同的包组合,用以满足不同的使用需求。网络上有太多如何创建的文章,这里简单提一下我自己的配置。

我在创建虚拟环境时尝试使用系统自带的python包--system-site-packages,但是发现了问题,也就是说在这个虚拟环境运行时,python程序会访问系统的/usr/lib等目录查找包,但是不会真的把这个包复制到虚拟环境目录中。如果搭配gunicorn和supervisor则会出现严重问题,因为gunicorn和supervisor会去识别系统目录里的python,而不会去找这个虚拟环境目录里的python,不知道这是不是OpenSUSE的bug。

所以如果创建虚拟环境,则不能再使用系统自带的gunicorn等工具,而是要在虚拟环境中安装单独的gunicorn,这也就是说不能使用系统自带的python包,而必须使用pip全新安装所有必须的包。这样就意味着操作系统自带的包只能通过在程序中添加到sys.path,然后再import来使用了;直接import就会载入虚拟环境中的包,而不是系统自带的包。

cd ~
mkdir venv #所有的虚拟环境都放在这个目录下
virtualenv my_env -p /usr/bin/python3

等待几分钟系统建立好虚拟环境

进入虚拟环境

source ./myenv/bin/activate #进入虚拟环境

退出虚拟环境

deactivate

使用pip在虚拟环境中安装第三方包时,尽可能使用国内的pip镜像,这样速度比较快

pip config set global.index-url https://repo.huaweicloud.com/repository/pypi/simple

或者也可以直接编辑~/.pip/pip.conf文件

[global]
index-url = https://mirrors.huaweicloud.com/repository/pypi/simple
trusted-host = mirrors.huaweicloud.com
timeout = 120 

然后根据需要安装自己需要的第三方包

pip install geomdl

要特别注意的是,有些包在安装过程中需要编译,此时需要一些c++编译器、python头文件或者其他的头文件之类,比如psycopg[C]就需要postgresql-server-devel包

还有一种方法是直接使用系统自带的包,优点是这样的包非常的稳定,不易出错。但是如果系统没有自带,而需要安装额外的包时,就只能使用pip来安装,此时pip默认安装到系统目录下,这样不好。可以使用--user参数安装到用户目录下

pip install --user geomdl

这样的缺点就是不能创建多个环境以满足不同的需求。

另外系统自带的包往往不是最新的,而且往往也不会更新大版本,尤其是服务器操作系统,所以如果需要新版本的包,就只能采用虚拟环境+pip安装的方法。

还有一种方法就是使用Anaconda发行版,但是这个发行版的很多包兼容性不太好,就是说一更新,结果不同的包版本之间就会出现不兼容,比如django一更新,很可能psycopg2就不能用了。

以我的需求而言,使用系统自带的包最好,因为软件开发本身需要一定的周期,开发伊始选择了最新版本的包,开发过程中如果遇到小版本升级还可以跟上,如果遇到大版本升级就只能停下了,所以等到开发完成时,我们开发的软件所依赖的包往往也不是最新版了,而这个版本和Linux发行版自带的包的版本往往相兼容。之所以选择openSUSE就是因为这个发行版自带的包对我的需求匹配度最高,只需要pip安装最少的包,而且还是纯源代码的不需要编译的包。

gunicorn服务

在openSUSE,gunicorn软件rpm安装默认是安装到/usr/bin,所以在虚拟环境中仍然会被识别为系统的gunicorn,然后如果通过gunicorn启动Django程序的话,启动的依然是系统的python环境,而不是虚拟环境。所以为了让gunicorn启动虚拟环境里的python及其附加的包,不能使用openSUSE自带的gunicorn rpm包,而是要在虚拟环境中通过pip安装gunicorn

或者只能放弃虚拟环境,而是通过

pip install --user geomdl

的方式,直接把openSUSE没有自带的包安装上,然后直接在用户环境使用。因为放弃了虚拟环境,所以灵活性受到限制。

gunicorn使用非常简单,cd进入django程序目录后

gunicorn -w 进程数量 -b ip:端口 -k 运行模式 项目名.wsgi:application
 
# 样例
gunicorn -w 2 -b localhost:8080 -k gevent teacher.wsgi:application

gevent模式响应比较快

老版的gunicorn要求用户在最后给的运行程序一定要加上application类名,不过新版已经不需要了,只要写到wsgi就可以了

当然也可以创建配置文件,然后用-c参数读入配置文件来启动

gunicorn mysite:wsgi -c gunicorn_config.py

supervisor监控程序

如果gunicorn进程意外中断,会导致网站访问受阻。为了避免这个问题,可以使用supervisor监控,一旦gunicorn进程终端,supervisor会自动再次启动gunicorn

参考

https://www.jianshu.com/p/bbd0b4cfcac9

然后针对自己的网站程序编辑一个配置文件

[program:web]
command=gunicorn -w 4 -b 127.0.0.1:8000 -k gevent webProgram.wsgi:application
directory=/home/user/webProgram
autostart=true

然后用

supervisord -c custome.conf

启动supervisor就可以

另外也可以在主配置文件中增加

[include]
file=/home/username/anaconda3/envs/yourenv/etc/supervisord/supervisord.conf

来增加其他的配置文件以增加配置内容

直接使用命令

supervisord

即可启动,还可以使用

supervisorctl restart webProgram

或者其他supervisorctl命令来对supervisor服务进行调用

nginx转发配置和django路由配置的协调

外网用户访问时正常访问到80端口,nginx会监听这个端口,然后根据用户请求分别转发给gunincorn的django程序或者直接从硬盘上读取静态文件,也可以转发给其他服务程序比如php-fpm。作为网关nginx性能很好,为后面的其他服务程序提供了不错的屏蔽,提高了服务器的安全性。

/etc/nginx/nginx.conf文件是nginx的主配置文件,其中通过include引用了/etc/nginx/vhosts.d这个目录内的文件。管理员可以在其他的目录,存放各个不同站点的配置文件,将其复制到vhosts.d目录即可启用,将其移出则可以关闭,这样就可以调整nginx实际运行中的站点。

所以在/etc/nginx/vhosts.d目录创建配置文件,一般为了方便管理都是把文件名设定成域名

server {
    listen 80;
    server_name domainname.com;
    root /home/developer/mysite/public;
    index index.html;
 
    location /api {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Referer $http_referer;
        proxy_set_header X-Forwarded-Proto $scheme;
        #proxy_redirect localhost/ /api/;
        #proxy_redirect ~^/(.+)$ /api/$1;
    }
    location /static {
        alias  /home/developer/mysite/static;
    }
}

这个配置就是说遇到“/static/...”访问路径时会直接从硬盘上读取/home/developer/mysite/static目录下的文件;而如果遇到了"/api/..."访问路径则会转发给http://localhost:8080也就是gunicorn监听的端口

在这里我遇到了一点问题,就是localhost这个域名是需要解析的,也可以是其他域名,但是只要解析就涉及到IP协议版本,现在正在推广IP v6,默认情况下localhost会被解析为[::1],如果gunicorn那边没有开启相关支持,则会访问失败。解决方法就是不写域名而是直接给IP地址127.0.0.1

关于转发规则的匹配,有些地方需要注意

https://blog.csdn.net/u011789653/article/details/78822419

location后面的路径是否以“/”结尾,以及proxy_pass是否以“/”结尾都对匹配模式有影响

另外后台的django+gunicorn返回的内容中也要注意,如果仅仅是返回json,那不需要担心,浏览器直接处理即可

如果返回的是html页面内容,其中还包含了链接,尤其是涉及到css、js、图片等自动加载的链接,浏览器还会再次访问这个链接,如果这个链接拼接的有问题,就会在载入的时候出错。

在django中,通过配置STATIC_URL可以控制其生成的html页面中包含的静态资源的路径,如果这个路径和nginx的配置一致,不论是访问到nginx server root配置的目录下的静态文件,还是访问到location /static目录下的文件,都可以正常使用。但如果这个链接是访问到其他目录就会出问题。

如果django返回的是HTTP 302 redirect重定向,nginx对此提供了proxy_redirect配置参数来控制,可以通过字符串匹配来处理,也可以使用正则表达式来匹配。具体可以参考

http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect

实际上nginx仅仅会修改响应的Location字段,不会修改response body的内容。所以如果重定向以后返回的内容又包含了html页面内容,那还是要注意内部包含的链接的问题。

我遇到的问题是django-admin页面不能正常工作,因为里面的链接都需要额外处理,而这个处理只能在django完成,nginx是不能处理的。而django-admin又十分封闭

所以最后为了保证访问index时,不会和django的访问api/url冲突,我还是修改了django的url路由配置,尤其是django-admin的部分,给它们统统加上了"/api"前缀。

对于django而言,其url为

http://domainname/url/param/

红色的部分。这部分却不会直接和urls.py中的urlpatterns匹配,而是要去掉最前面的'/',用剩下的url/param/来匹配urlpatterns

所以可以在django中添加一个BASE_URL为'api/',这个变量用于匹配urlpatterns,所以没有起始的'/',然后在根目录的urls.py中的urlpatterns里都加上BASE_URL前缀,其他的url通过include作为根目录下url的子集,就可以确保从nginx传递过来的url和django返回的带有链接的html内容都没有问题。如果仅考虑静态文件,通过django的STATIC_URL来设置也可以,不过对于django-admin却会出问题,为了保证django-admin不出问题,还是用这个方法比较稳妥。

另外在其他代码——最常见的是权限控制——中涉及到url访问权限的判别,一定要用红色的带有起始'/'的url——也就是/url/param/——来判别,因为这是常规字符串处理,不是django自己的路由匹配会自动删除起始的'/'

如果需要使用vue中的history模式,那么nginx还要再调整配置,不过我不希望服务器的负载增大,所以关闭了vue的history模式。虽然这样会在浏览器地址栏留下一个难看的#符,不过毕竟减轻了服务器的负担。

如果nginx配置中把django的转发url设定了前缀,那么vue里也要加上这个前缀。vue中涉及到django后台交互的url必须要保留起始的'/',所以前缀是'/api',和django的BASE_URL不同。

为了让nginx能够读取静态文件,需要对文件系统权限做些调整。nginx默认的运行用户是www-data,可以修改/etc/nginx/nginx.conf文件,修改user行,修改为静态文件目录的所有人。应该说把静态文件存放目录的访问权限调整为www-data也可访问也是可以的,不过未来如果再有新的文件或者目录,调整起来会非常困难,所以简便的方法还是替换nginx的启动用户。ubuntu没有selinux,如果是红帽系的带了selinux,那修改目录的用户权限会更难,还是替换nginx的启动用户简便一些。不过安全性上稍微差一些,主要差异就在于目录所有人是可以登录的,但是www-data用户是不可以登录的,所以一旦用户登录信息被窃取,损失比www-data用户要大。

不过默认情况下的文件访问权限是其他用户可以读,如果是时髦的前后段分离的模式,nginx提供的静态文件仅仅包涵一些静态的html、css、javascript、图片……给用户呈现首页,然后由用户端载入的javascript通过ajax访问nginx以实现其他更复杂的功能,这个时候nginx主要职责是转发给Django,这样的话文件系统权限的问题应该不大。

如果仅仅读倒文件还好,如果涉及到写,一般都不通过nginx来写文件,而是通过后台的Python或者php来写。而只要php不是通过模块嵌入的,那不论Python也好还是php也好都是独立的进程,和nginx之间是通过进程通讯来实现调用的。所以只要那个独立进程有对应目录的写权限就够了。

然后测试一下nginx的配置文件

nginx -T  # 检查nginx语法问题

如果有问题会提示具体在哪一行,没问题就通过。如果提示的问题是访问权限,可以用sudo临时用户测试一下

sudo -u correctuser nginx -T

因为nginx测试的时候是不会用配置文件里的user行的内容来测试的,而是用当前用户来测试的。

通过以后重新启动nginx,使新配置生效

sudo systemctl restart nginx.service

此时通过浏览器访问

http://localhost/api/

就能看到用户请求通过nginx转发给gunicorn运行的django网站程序,然后程序返回的数据通过gunicorn和nginx返回浏览器。

django网站程序静态资源配置

根据django的官方文档

https://docs.djangoproject.com/zh-hans/2.2/howto/static-files/

尽管django自带的开发服务器程序支持静态文件,但是官方不鼓励在生产环境通过django来访问静态文件

通过STATIC_URL这个变量可以用于在程序中为涉及到静态文件的地方做相关配置,这是用于生成访问静态文件的url的,浏览器得到这个url后,会再访问nginx来请求静态文件,此时就不需要django来处理了

通过django.contrib.staticfiles这个类提供了一些处理静态文件的工具,这个工具通过命令
 

python manage.py collectstatic

可以把项目相关的静态文件全部收集到STATIC_ROOT这个变量指定的目录里,以供生产环境部署,也就是复制到nginx配置文件中指定的那个"/static"所alias的目录下,其中"/static"对应STATIC_URL,alias目录对应STATIC_ROOT,这样所有的对静态文件的访问就都通过nginx来处理,不需要django来运算

根据官方文档

https://docs.djangoproject.com/zh-hans/2.2/ref/contrib/staticfiles/

在urls.py中增加
 

from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
# ... the rest of your URLconf here ...
 
urlpatterns += staticfiles_urlpatterns()

可以让django提供访问静态文件的能力,不过官方还是不推荐这么做。经过实践检验,gunicorn搭配这种配置不能提供静态文件的访问功能,至少不能提供django-admin模块所需的静态文件如css之类的访问功能。

所以没必要调整django来支持静态文件的访问,而是在nginx中配置,"/static"对应STATIC_URL,alias目录对应STATIC_ROOT就好。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;