/Web-Full-Stack-Practice

Web Full Stack Practice for Beginners:Docker + uWSGI + Celery + Django + Supervisor + React + Nginx + HTTPS + Postgres + Redis

Primary LanguageJavaScriptBSD 2-Clause "Simplified" LicenseBSD-2-Clause

Web Full Stack Practice:Docker + uWSGI + Celery + Django + Supervisor + React + Nginx + Https + Postgres + Redis

本项目主要介绍基于 Docker 的 Web 开发和部署(开发要求在改动代码时服务或页面能够实时发生变化)全流程,来源于日常项目,后端以 Django 为例,前端以 React 为例,使用到的其他模块也可以换成同类产品,比如 uWSGI 可以换成 Gunicorn,数据库可以换成 Mysql 等。我们将通过一个案例前后端分离介绍,这样容易理解。

目标

  • docker-compose 启动前后端同时开发
  • 本地开发 + 正式部署 Https
  • Supervisor + uWSGI + Nginx 部署

特别说明:在前后端联合调试时比较方便,如果单个开发后端或前端,直接本地很多时候会更方便。

关于本地 Https,很多框架本来就是支持的,就不要像本文这么麻烦了。

环境

  • MacOS Mojave 10.14
  • Docker Desktop Community Version 2.0.0.2
    • Engine:18.09.1
    • Compose:1.23.2

产品

作为一名 NLP 算法工程师,我们决定做一个简单的 Language Model 的 Demo,前端用户输入一个词,返回一段自动生成的文本。

模型参考:递归神经网络 | TensorFlow,使用张爱玲作品集的句子作为训练集,800 个句子,52750 字,跑了 150 个 epoch。

后端

后端部分主要包括:uWSGI、Celery、Django、Supervisor、Postgres 和 Redis,首先分别简单介绍一下这些模块的功能:

Step1: Postgres

首先把 db 设置好,如果这里需要用到 redis 也需要一并设置。

我们可以使用 docker-compose up db(在 docker-compose.yml 所在目录执行)只启动 db,然后在本地登陆 db 去创建用户,当然本地也可以直接使用 postgres 作为用户。需要注意的是:host 地址是本机的 IP 地址,Mac 可以使用 ifconfig 查看。

psql -h 192.168.0.103 -U postgres,密码就是启动时创建的超级用户的密码,登陆后最好是更改一下 postgres 的密码,因为环境变量的那个只是启动时用一下。

# create user
create user demo with password "demopassword";
# create db
create database demo;
# grant privileges
grant all privileges ON database demo to demo;

当然,你也可以直接使用 docker run -it --rm --name mypsql -e POSTGRES_PASSWORD=password4superuser -p 5432:5432 -v ~/docker_volume/pg9.5:/var/lib/postgresql/data postgres:9.5 启动 db,启动后可以更改 postgres 用户的密码。

这一步的主要目的就是创建用户和 db ,然后把 data 都映射出来,这样我们后端启动时,就可以通过配置文件连接到 db 了。

Step2: Django

这块主要针对 Django 的流程和注意事项,熟悉或者不需要的可以跳过。

# 初始化项目
mkdir demo && cd "$_"
django-admin startproject demo_backend
cd demo_backend
# 创建一个 app
python manage.py startapp text_generator

到这一步基本的框架就有了,之后就是项目配置和具体代码的编写。一些 Keypoints:

  • Python 环境及开发包管理:pypa/pipenv: Python Development Workflow for Humans.

    • 创建虚拟环境:pipenv --python 3.6.5 # 我的本地环境是 python 3.6.5
    • pipenv shell 可以进入虚拟环境,或使用 pipenv run python xxx.py 等于直接在虚拟环境中运行 python xxx.pypipenv install xx 可以安装需要的依赖,或安装指定版本,比如本例:pipenv install tensorflow==1.12.0
    • 使用之前,将 Pipfile 中的 url 改成 https://pypi.douban.com/simple 或其他速度快的源,Pipfile 能看到所有已安装的包
  • 关于 settings:

    • 将 setting 分为 prod 和 dev 两个文件,分别设置开发和正式环境的参数,需要修改 wsgi.py, manage.py 以便 Django 能够找到配置文件
    • 一般每个 APP 下面都可能有配置文件,编写代码时最好改成可以统一在项目的 settings 这里覆盖。比如训练好的模型文件(详见配置文件 settings/base.py)可以映射出来,这样不但可以方便我们随时更新模型,而且也能减小 image 的大小。
  • 关于 database:

    • 使用配置文件,这样本地开发完部署时只要在服务器用正式的配置文件即可
    • 如果使用 docker-compose,数据库的 host 是:192.168.65.2,而不是 127.0.0.1 或本机地址

涉及到代码相关或相应文件的,项目中均有注释,可直接查看相应文件。

Step3: Celery+Redis

首先代码需要做相应的修改,可以直接查看代码文件。

本地开发环境配置方面需要注意的是:

  • wsgi.py 同目录(settings文件夹同目录)新建 celery.py 并配置,然后修改 __init__.py,再修改 settings/base.py 中的配置
  • 启动 redis,运行:celery -A demo_backend worker -l info 即可

正式环境需要将其作为 daemon 启动,需要配置 conf:

  • demo_backend/celery 目录下,conf 最重要的两个配置是:CELERY_APP="demo_backend"CELERYD_CHDIR="/demo-backend",后者一般是 Dockerfile 后端 server 的根目录,也就是整个后端项目的根目录(settings 文件夹和 celery.py 的上级目录);sh 主要就是把DEFAULT_USER 改成和 conf 一致。

    需要说明的是:这两个文件可以放在任何地方,因为它们最终都是要放到 /etc/ 下面的。

  • user 和 group 一般就选择 root,当然也可以自己创建 user 和 group,官方建议非 root,不过因为我们是在 docker 里面,所以 root 也没有太多问题。

    这里的原因是,root 用户出错时可能会对系统造成一些意想不到的错误,如果系统有其他服务可能会出问题,但我们的 docker 服务是隔离和相对独立的,所以个人觉得使用 root 问题不大;同样的问题在 uWSGI 那里也有。

这里需要说明的是:本例直接等后端返回结果再传给前端,没有使用异步,因为速度不是特别慢。如果需要异步,可以在 task.get() 之前马上将 task id 返给前端,然后由前端根据 task id 获取最终的结果,这样就变成了异步操作。

Step4: uWSGI

按照配置文件配置,需要注意的是,这里不用配置 daemon。可以设置 server 为:socket=app.sock 或直接使用 http(docker 中不能使用 app.sock,因为文件不在一个容器内)。

需要注意的是,这个是在正式环境下使用的,如果在本地开发环境,直接用 python manage.py runserver 0.0.0.0:8000 启动服务即可。

相关参数详细说明可以参考:

Step5: Supervisor

主要目的是把多个服务放在一个容器内启动,官方文档 Run multiple services in a container | Docker Documentation 也有相应的介绍。关于 stopsignal 参数的说明,可以参考:How to use supervisor fo start/stop uWSGI application? - Stack Overflow

Step6: Dockerfile

接下来就是编写 Dockerfile 了,我们可以用一个 Dockerfile 同时满足本地开发和正式部署,主要通过 supervisor 不同的配置文件来实现,本地开发时还需要把整个目录映射出去,这样当文件内容发生变化时,服务会自动刷新。

运行 docker-compose build app 单独 build app,build 完成后可以通过 docker-compose up db redis app 来启动 db、redis 和后端服务,docker-compose stop 停止服务。需要注意的是:

  • backend.local.env 中的 db host 和 redis host 都需要改为 192.168.65.2,这是 docker 服务的默认地址,否则无法连接到 db 和 redis。db host 也可以直接使用 docker-compose.yml 中 db 的 name(如本例中是:db)。
  • 本地开发时,需要把整个项目目录映射出去。但在测试正式环境时不需要(注释掉 docker-compose.yml line 29),因为映射后容器里面目录的内容会被清空,以映射出来的目录为准了,而我们本地并没有设置 Celery 的 deamon;而且 uWSGI 也可能会报错,因为我们没有设置虚拟环境的目录。
  • 本地开发时,command 要写成本地的 Supervisor 配置文件以替换 Docker 里面的正式配置文件。但在测试正式环境时要记得注释掉(docker-compose.yml line 40)。
  • 容器启动后,需要通过 docker exec -it app bash(app 可以替换为 container id)进入后端容器内部执行系列命令,包括:
    • python manage.py makemigrations 生成 db 相关数据
    • python manage.py migrate 将生成的数据 migrate 到 db
    • python manage.py createsuperuser 可以创建管理后台的管理员
    • python manage.py collectstatic 自动输出静态文件到项目根目录
  • 本地开发时,log 会直接输出到屏幕。但在测试正式环境时,日志会映射出来到映射的目录(如本例的 ~/docker_volume/log/),可以直接通过目录文件查看。

到这一步,后端部分就已经完成了,我们可以通过 http://127.0.0.1:8000/admin/ 登陆管理员,也可以通过 http://127.0.0.1:8000/api/ 查看 Rest Framework,或者通过调用 http://127.0.0.1:8000/api/generate/ 生成。后端代码修改后,服务会自动刷新。

前端

刚刚后端的访问是直接通过 ip 地址 + 端口执行的,正式环境中需要用 Nginx 做转发;开发环境下,我们只需用 localhost 直接访问即可。这里稍微有点麻烦的是本地 Https 的配置。

React

前端我们使用 Facebook 的 Create React App · Set up a modern web app by running one command.

npx create-react-app demo_frontend
cd demo_frontend
npm start

上面的代码即可创建一个 App 并启动前端页面,安装依赖直接用 npm i xxx 即可,如果只是在开发环境下使用,可以 npm i xxx --save-dev,然后完成代码编写。

Https

要想在本地开发时使用 https 有两个重要的步骤:

  • 生成证书相关文件
  • 配置 React

生成证书相关文件需要借助:dakshshah96/local-cert-generator: 🚀 A set of scripts to quickly generate a HTTPS certificate for your local development environment.

  • 前三步是让你的本机成为一个 “证书颁发机构”,将第二步生成的 rootCA.pem 双击添加后,都改为 “信任” 即可。如图所示:

  • 第二步有几个需要注意的地方:

    • Enter pass phrase for rootCA.key: 输入一个自己定义的密码(要输三次),要记住,以后每次为域名生成证书时都需要输入
    • 后面的除了 Common Name 都可以回车跳过,这个是证书颁发机构的名称,输入 Local CertMy PC Cert 之类的都可以
  • 然后运行第四步生成 localhost 的证书,或者使用仓库中的 g_ssl_for_domain.sh./g_ssl_for_domain.sh localhost /path/to/store/ssl/file/,两个参数分别是你网站的域名和生成 ssl 文件的存储目录。我们需要 server.crtserver.key 就可以了,为了方便之后的操作,我把这两个文件放到了 demo_frontend/ssl.localhost 目录下。

然后要配置 React,这里我们需要安装 timarney/react-app-rewired: Override create-react-app webpack configs without ejectingnpm i react-app-rewired --save-dev

  • 首先在项目根目录下创建一个 config-overrides.js 的文件,配置将在里面进行,详见配置文件。这里我们用了一下环境变量,指定 server.crt, server.key 的位置
  • 然后根据官方说明,修改 package.json,将相应的 react-scripts 改为 react-app-rewired

最后就是 Dockerfile 的编写了,由于本地开发,所以这里会比较简单,只要有 node 环境即可。

然后我们就可以使用 docker-compose up redis db app web 来启动所有服务了,然后通过 https://localhost:3000 访问前端,我们可以看下证书,没错,是我们的 My PC Cert 颁发的。

尝试生成一句:

我们可以修改前端代码,由于目录映射页面会重新编译、自动刷新。最后记得 npm run build 生成静态文件。

Nginx

NGINX | High Performance Load Balancer, Web Server, & Reverse Proxy 一般会用在产品部署上,作为代理和静态资源服务器,接下来我们主要介绍如何在本地调试 Nginx。主要有以下几步:

  • 生成域名的证书,我们随便用一个名字,比如 naivegenerator.com
  • 编写 Nginx 配置文件
  • 编写 Dockerfile 并 build image
  • 修改 Host

生成域名证书时,需要将 v3.ext 中的 DNS.1 = naivegenerator.com 修改掉,然后执行 sh createSelfSigned.sh 即可生成新域名的证书,我们将其放在前端根目录的 ssl 目录下。

Nginx 配置文件我们主要编写 nginx.proj.conf 即可,各配置详细说明可以直接看文件,有个地方需要注意下,如果 location 块使用 /xxx/ 时,proxy_pass 后面加斜杠和不加斜杠结果会不一样,举个例子:

server
{
    listen 80;
    server_name: www.naivegenerator.com;
    location /api/ {
        proxy_pass http://127.0.0.1:8000; # 配置1
        proxy_pass http://127.0.0.1:8000/; # 配置2
    }
}

使用配置 1 时,请求 http://www.naivegenerator.com/api/generate/ 时会被成功转到:http://127.0.0.1:8000/api/generate/,而使用配置 2 时,则会被转到:http://127.0.0.1:8000/generate/。请求静态文件也是一样,所以这里需要稍微注意下。

然后是编写 Dockerfile(最好创建并编写一下 .dockerignore 将 node modules 忽略掉),这里面有四个地方要强调一下:

  • 正式部署时,一般需要先在服务器上放一个验证文件,能点击下载后才能获得 server.keyserver.crt,比如 SSL For Free - Free SSL Certificates in Minutes,但我们本地因为是自己电脑给的证书,所以这一步不需要。当然如果你采用其他的证书授予商,也可能有不同的要求。除了上面那个免费的 SSl 外,还有很多,大家可以上网搜一搜,比如:Getting Started - Let's Encrypt - Free SSL/TLS Certificates
  • 关于 ARG 和 ENV 详细情况大家可以看一下官网,build image 时可以使用 arg。本例中,我们使用 ENV BACKEND_HOST=${backend_host} 设置了一个环境变量,变量值从 docker-compose 中 nginx build 的 arg 中取变量名为 {backend_host} 的变量,然后将该环境变量传入配置文件,让其生效。这么做的目的主要是因为本地 docker-compose up 后 Nginx 访问后端服务需要使用 192.168.65.2,而不是 127.0.0.1,正式部署时在 k8s 上可以使用后者。
  • 前端的静态文件在运行 npm run build 后会自动生成 build 文件夹,我们在 nginx.proj.conf 中将其设置为主页的根目录;后端(admin 和 rest)的静态文件则需要后端用 python manage.py collectstatic 收集后在 Dockerfile 中复制到前端某个地址。需要说明的是,因为我们将这个 static 目录共享了,所以虽然我们把本地目录整个映射出去了,但登陆 app 容器运行上面的命令后本地依然看不到 static 下的文件。因为 docker 会自动创建一个 volume(只要运行 docker volume ls 就看到了),文件就在这共享的 volume 里(本例中位 demo_app_static)。那怎么办呢?有三种办法:
    • 注释掉共享的目录,重新启动运行;

    • 本地生成;

    • 找到文件的实际位置然后复制出来,关于 volume 的详细情况可以通过 docker volume inspect demo_app_static 查看,运行 screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty 然后进入 inspect 的目录就可以看到了。注意需要运行一个 docker 才能复制,步骤如下:

      • docker run -it --rm -v volume_name:/home_or_other_dir container /bin/bash
      • docker cp container_name:/home_or_other_dir /your_local_path

      因为这里的 volume 并没有在你 docker-compose up 起来的容器里,所以我们需要先将它映射到一个容器里再复制。

  • 关于 Dockerfile 中 ENV 替换到 nginx 配置文件需要特别注意,可以参考这里:configuration - nginx 'invalid number of arguments in "map" directive' - Stack Overflow

最后别忘了修改 host:sudo vim /etc/hosts,添加一行:127.0.0.1 naivegenerator.com,这样当我们访问 naivegenerator.com 时等于是访问了 127.0.0.1

最终效果

重新 docker-compose up redis db app nginx 后我们可以打开 https://naivegenerator.com,然后输入 “爱情” 让模型随机生成,如图所示:

admin 网址:https://naivegenerator.com/admin,如图所示:

rest 网址:https://naivegenerator.com/api, 如图所示:

注意事项

  • 如果 docker-compose stop 后还想把生成的 container 也删掉(因为各种报错我们可能会不停地 build,有时候之前的需要彻底删除),可以使用这个命令:docker stop $(docker ps -a -q); docker rm $(docker ps -a -q); docker volume rm $(docker volume ls -qf dangling=true)

  • 由于本例把所有操作整合在一起了,有些童鞋如果想要了解每一步的细节,可以在本地把环境设置好,使用 docker-compose 单独 up 启动 db 和 redis,然后在本地操作后端交互。无论是 up 还是 stop,都须在 docker-compose.yml 所在目录执行。

  • docker-compose 中 build 之外的字段对 build 没有影响,build 主要受 Dockerfile 的影响;Dockerfile 如果名字不是 Dockerfile 需要指定文件名。

  • 本例把 backend.local.envfrontend.local.env 一起上传了,但在正式项目中,大家务必把 *.env 添加到 .gitignore 中,这样信息就不会泄露了。

  • 本例使用了 Celery 但后端代码并没有写成异步;同样使用了 Redis 但并未用在后端服务,而是用作了 Celery 的 Broker。

  • 本例中的各个模块(Docker)除外都可以替换为同类其他产品,前后端自不必说,uWSGI 有 Guniron,Postgres 有 Mysql、MongoDB 等等,但我觉得基本思路是类似的,我们的目的是 Docker 化。

  • 关于 Docker 操作的,**省一个同胞写的 twtrubiks/docker-tutorial: Docker 基本教學 - 從無到有 Docker-Beginners-Guide 教你用 Docker 建立 Django + PostgreSQL 📝 还不错,另外官方文档也非常赞,所以我就没有废话了。我在他另一个项目里也学到了一些(第一个参考文献),推荐新手看看。

  • 本项目可优化之处还有很多,比如 Redis 用于后端服务、比如异步操作、比如 uWSGI socket 方式等等,不一而足,还请大家结合自己的实际情况灵活使用。另外,各个部分深入后都会有另外一些坑,比如 postgres create db 的编码问题,我会在其他文章中分享,欢迎大家关注。

小结

本项目主要介绍基于 Docker 的 Web 全栈开发系列,看似东西很多,但其实每个地方都没有过多深入,项目也非常简单,所以适合 beginners。如果大家觉得看起来好像非常繁琐那也是正常的,因为确实会有一点繁琐,但只要仔细点理清每个地方其实并不难。我们最后把整个项目的目录列出来并整体总结一下:

tree -L 2
.
├── Dockerfile_local				# 前端本地的 Dockerfile
├── Dockerfile_nginx				# 前端 Nginx 的 Dockerfile
├── Dockerfile_server				# 后端 Server 的 Dockerfile
├── backend.local.env				# 后端 Server 在 docker-compose.yml 中的环境变量
├── demo_backend				# 后端项目目录
│   ├── Pipfile					# 项目依赖管理文件
│   ├── Pipfile.lock				# 同上,lock 文件
│   ├── celery					# Celery 的配置文件,可以放在任何地方
│   ├── demo_backend				# 项目配置文件和入口
│   ├── manage.py				# 本地开发入口文件
│   ├── static					# 后端 admin 和 rest 静态文件
│   ├── supervisor-master.zip			# supervisor repo,build 时下载太慢,采用本地安装
│   ├── supervisord.conf			# Supervisor 正式环境配置文件
│   ├── supervisord.local.conf			# Supervisor 开发环境配置文件
│   ├── supervisord.log				# Supervisor 运行时的 log 文件
│   ├── text_generator				# 后端的 App,本例只有一个
│   └── uwsgi.ini				# uWSGI 配置文件,用于正式环境
├── demo_frontend				# 前端项目目录
│   ├── build					# npm run build 后生成的静态文件
│   ├── config-overrides.js			# 覆盖配置 (本地 https) 使用的配置文件
│   ├── node_modules				# node modules
│   ├── package-lock.json			# package lock 文件
│   ├── package.json				# package 依赖
│   ├── public					# public 文件
│   ├── src					# 前端源代码
│   ├── ssl					# 域名 host ssl 证书相关文件
│   └── ssl.localhost				# localhost ssl 证书相关文件
├── docker-compose.yml				# docker-compose 配置文件
├── frontend.local.env				# 前端开发在 docker-compose.yml 中的环境变量
├── nginx.conf					# nginx 默认配置文件
└── nginx.proj.conf				# nginx 项目配置文件

后端的 supervisor 和 celery 文件夹以及前端的 ssl 和 ssl.localhost 理论上是可以放在其他地方的,放在这些位置只是方便开发。

很多文件或文件夹是可以 ignore 的,比如前端的 build,后端的 supervisord.log 以及模型文件等,不过为了更加方便大家查看就都推上去了。

这样我们就把前后端用 docker 完全地整合在一起,在全栈开发时可以前后端同时开发调试,正式上线时也可以快速完成部署。

另外,把映射的日志文件目录也放在这里:

cd ~/docker_volume/log
tree -L 2
.
├── celery
│   ├── err.log
│   ├── out.log
│   ├── worker1-1.log
│   ├── worker1-2.log
│   ├── worker1-3.log
│   ├── worker1-4.log
│   ├── worker1-5.log
│   ├── worker1-6.log
│   ├── worker1-7.log
│   ├── worker1-8.log
│   └── worker1.log
├── nginx
│   ├── access.log
│   ├── demo.access.log
│   ├── demo.error.log
│   └── error.log
└── uwsgi
    ├── demo.uwsgi.log
    ├── err.log
    └── out.log

分别是 celery、uwsgi 和 nginx 的日志文件,nginx 的 demo.* 就是我们针对项目做得配置,celery 的 err.logout.log 是我们在 Supervisor 中做得配置,其余的则是 celery 的 conf 文件做得配置(celeryd.conf line 24:CELERYD_OPTS="--time-limit=300 --concurrency=8")。建议把一个项目的日志放(或映射)在一个地方,无论是本地开发还是正式部署。

有些 log 是没必要的,比如 uwsgi,如果配置本身设置了的话,supervisor 那里可以不用设置。

参考文献和资源

以下主要罗列使用过的参考文献和一些还不错的资源,简单的归了下类,大家按需取用。

后记

这篇文章包括这个项目花了一天时间才完成,这还是在已经对各模块都有一些经验的情况下。很多地方都踩过坑,说这篇文章的内容 “坑坑洼洼” 也不夸张,尤其是我提到要注意或者特别强调的点。在解决这些坑的过程中得到不少同事的帮助,尤其是 scottming (Scott Ming) 在本地 Https 和 Dockerfile 相关的配置中给予了很多启发和提示。最后,希望这个 demo 项目和文章能对大家有所帮助,如果有 Google、Stackoverflow 没有找到答案且与此项目相关的问题或本项目疏漏的地方,欢迎大家 Issue。

CHANGELOG

  • 20190303 创建