文中涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库python
以前一系列繁琐的部署步骤让咱们感到痛苦。这些痛苦包括:linux
那么咱们有没有办法,让本地开发环境和线上环境保持一致?这样咱们在部署上线前,就能够在本地进行验证,只要验证没问题,咱们就有 99% 的把握保证部署上线后也没有问题(1%保留给程序玄学)。nginx
这个办法就是使用 Docker。git
Docker 是一种容器技术,能够为咱们提供一个隔离的运行环境。要使用 Docker,首先咱们须要编排一个镜像,镜像就是用来描述这个隔离环境应该是什么样子的,它须要安装哪些依赖,须要运行什么应用等,能够把它类比成一搜货轮的制造图。github
有了镜像,就能够在系统中构建出一个实际隔离的环境,这个环境被称为容器,就比如根据设计图,工厂制造了一条船。工厂也能够制造无数条这样的船。sql
容器造好了,只要启动它,隔离环境便运行了起来。因为事先编排好了镜像,所以不管是在本地仍是线上,运行的容器内部环境都同样,因此保证了本地和线上环境的一致性,大大减小了由于环境差别致使的各类问题。docker
因此,咱们首先来编排 Docker 镜像。shell
相似于分离 settings.py 文件为 local.py 和 production.py,咱们首先创建以下的目录结构,分别用于存放开发环境的镜像和线上环境的镜像:数据库
HelloDjango-blog-tutorial\
blog\
...
compose\
local\
production\
django\
nginx\
...
复制代码
local 目录下存放开发环境的 Docker 镜像文件,production\ 下的 django 文件夹存放基于本项目编排的镜像,因为线上环境还要用到 Nginx,因此 nginx 目录下存放 Nginx 的镜像。django
咱们先来在 production\django 目录下编排博客项目线上环境的镜像文件,镜像文件以 Dockerfile 命名:
FROM python:3.6-alpine
ENV PYTHONUNBUFFERED 1
RUN apk update \ # Pillow dependencies && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev
WORKDIR /app
RUN pip install pipenv -i https://pypi.douban.com/simple
COPY Pipfile /app/Pipfile COPY Pipfile.lock /app/Pipfile.lock RUN pipenv install --system --deploy --ignore-pipfile
COPY . /app
COPY ./compose/production/django/start.sh /start.sh RUN sed -i 's/\r//' /start.sh RUN chmod +x /start.sh 复制代码
首先咱们在镜像文件开头使用 FROM python:3.6-alpine
声明此镜像基于 python:3.6-alpine 基础镜像构建。alpine 是一个 Linux 系统发行版,主打小巧、轻量、安全。咱们程序运行须要 Python 环境,所以使用这个小巧但包含完整 Python 环境的基础镜像来构建咱们的应用镜像。
ENV PYTHONUNBUFFERED 1
设置环境变量 PYTHONUNBUFFERED=1
接下来的一条 RUN 命令安装图像处理包 Pilliow 的依赖,由于若是使用 django 处理图片时,会使用到 Pillow 这个Python 库。
接着使用 WORKDIR /app 设置工做目录,之后在基于此镜像启动的 Docker 容器中执行的命令,都会以这个目录为当前工做目录。
而后咱们使用命令 RUN pip install pipenv
安装 pipenv,-i 参数指定 pypi 源,国内通常指定为豆瓣源,这样下载 pipenv 安装包时更快,国外网络能够省略 -i 参数,使用官方的 pypi 源便可。
而后咱们将项目依赖文件 Pipfile 和 Pipfile.lock copy 到容器里,运行 pipenv install 安装依赖。指定 --system 参数后 pipenv 不会建立虚拟环境,而是将依赖安装到容器的 Python 环境里。由于容器自己就是个虚拟环境了,因此不必再建立虚拟环境。
接着将这个项目的文件 copy 到容器的 /app 目录下(固然有些文件对于程序运行是没必要要的,因此一下子咱们会设置一个 dockerignore 文件,里面指定的文件不会被 copy 到容器里)。
而后咱们还将 start.sh 文件复制到容器的 / 目录下,去掉回车符(windows 专用,容器中是 linux 系统),并赋予了可执行权限。
start.sh 中就是启动 Gunicorn 服务的命令:
#!/bin/sh
python manage.py migrate
python manage.py collectstatic --noinput
gunicorn blogproject.wsgi:application -w 4 -k gthread -b 0.0.0.0:8000 --chdir=/app
复制代码
咱们会让容器启动时去执行此命令,这样就启动了咱们的 django 应用。--chdir=/app 代表以 /app 为根目录,这样才能找到 blogproject.wsgi:application。
在项目根目录下创建 .dockerignore 文件,指定不 copy 到容器的文件:
.*
_credentials.py
fabfile.py
*.sqlite3
复制代码
线上环境使用 Nginx,一样来编排 Nginx 的镜像,这个镜像文件放到 compose\production\nginx 目录下:
FROM nginx:1.17.1
# 替换为国内源
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak COPY ./compose/production/nginx/sources.list /etc/apt/ RUN apt-get update && apt-get install -y --allow-unauthenticated certbot python-certbot-nginx
RUN rm /etc/nginx/conf.d/default.conf COPY ./compose/production/nginx/HelloDjango-blog-tutorial.conf /etc/nginx/conf.d/HelloDjango-blog-tutorial.conf 复制代码
这个镜像基于 nginx:1.17.1 基础镜像构建,而后咱们更新系统并安装 certbot 用于配置 https 证书。因为要安装大量依赖, nginx:1.17.1 镜像基于 ubuntu,因此安装会比较慢,咱们将软件源替换为国内源,这样稍微提升一下安装速度。
最后就是把应用的 nginx 配置复制到容器中 nginx 的 conf.d 目录下。里面的内容和直接在系统中配置 nginx 是同样的。
upstream hellodjango_blog_tutorial {
server hellodjango_blog_tutorial:8000;
}
server {
server_name hellodjango-blog-tutorial-demo.zmrenwu.com;
location /static {
alias /apps/hellodjango_blog_tutorial/static;
}
location / {
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 X-Forwarded-Proto $scheme;
proxy_pass http://hellodjango_blog_tutorial;
}
listen 80;
}
复制代码
相比以前直接在宿主机配置 Nginx,这里使用了 Nginx 的 upstream 模块,实际上就是作一个请求转发。Nginx 将全部请求转发给上游 hellodjango_blog_tutorial 模块处理,而 hellodjango_blog_tutorial 这个模块的服务实际就是运行 django 应用的容器 hellodjango_blog_tutorial(接下来会运行这个容器)。
镜像编排完毕,接下来就能够经过镜像构建容器并运行容器了。可是先等一等,咱们有两个镜像,一个是 django 应用的,一个是 Nginx 的,这意味着咱们须要构建 2 次容器,而且启动容器 2 次,这会比较麻烦。有没有办法一次构建,一条命令运行呢?答案就是使用 docker-compose。
docker-compose 将各个容器的镜像,以及构建和运行容器镜像时的参数等编写在一个 ymal 文件里。这样咱们只须要一条 build 命令就能够构建多个容器,使用一条命令 up 就能够启动多个容器。
咱们在项目根目录建一个 production.yml 文件来编排 django 容器和 nginx 容器。
version: '3'
volumes:
static:
database:
services:
hellodjango_blog_tutorial:
build:
context: .
dockerfile: compose/production/django/Dockerfile
image: hellodjango_blog_tutorial
container_name: hellodjango_blog_tutorial
working_dir: /app
volumes:
- database:/app/database
- static:/app/static
env_file:
- .envs/.production
ports:
- "8000:8000"
command: /start.sh
nginx:
build:
context: .
dockerfile: compose/production/nginx/Dockerfile
image: hellodjango_blog_tutorial_nginx
container_name: hellodjango_blog_tutorial_nginx
volumes:
- static:/apps/hellodjango_blog_tutorial/static
ports:
- "80:80"
- "443:443"
复制代码
version: '3'
声明 docker-compose 为第三代版本的语法
volumes:
static:
database:
复制代码
声明了 2 个命名数据卷,分别为 static 和 database。数据卷是用来干吗的呢?因为 docker 容器是一个隔离环境,一旦容器被删除,容器内的文件就会一并删除。试想,若是咱们启动了博客应用的容器并运行,一段时间后,容器中的数据库就会产生数据。后来咱们更新了代码或者修改了容器的镜像,这个时候就要删除旧容器,而后从新构建新的容器并运行,那么旧容器中的数据库就会连同容器一并删除,咱们辛苦写的博客文章付之一炬。
因此咱们使用 docker 的数据卷来管理须要持久存储的数据,只要数据被 docker 的数据卷管理起来了,那么新的容器启动时,就能够从数据卷取数据,从而恢复被删除容器里的数据。
咱们有 2 个数据须要被数据卷管理,一个是数据库文件,一个是应用的静态文件。数据库文件容易理解,那么为何静态文件也要数据卷管理呢?启动新的容器后使用 python manage.py collectstatic 命令从新收集不就行了?
答案是不行,数据卷不只有持久保存数据的功能,还有跨容器共享文件的功能。要知道,容器不只和宿主机隔离,并且容器之间也是互相隔离的。Nginx 运行于独立容器,那么它处理的静态文件从哪里来呢?应用的静态文件存放于应用容器,Nginx 容器是访问不到的,因此这些文件也经过数据卷管理,nginx 容器从数据卷中取静态文件映射到本身的容器内部。
接下来定义了 2 个 services,一个是应用服务 hellodjango_blog_tutorial,一个是 nginx 服务。
build:
context: .
dockerfile: compose/production/django/Dockerfile
复制代码
告诉 docker-compose,构建容器是基于当前目录(yml 文件所在的目录),且使用的镜像是 dockerfile 指定路径下的镜像文件。
image 和 container_name 分别给构建的镜像和容器取个名字。
working_dir 指定工做目录。
volumes:
- database:/app/database
- static:/app/static
复制代码
同时这里要注意,数据卷只能映射文件夹而不能映射单一的文件,因此对咱们应用的数据库来讲,db.sqlite3 文件咱们把它挪到了 database 目录下。所以咱们要改一下 django 的配置文件中数据库的配置,让它正确地将数据库文件生成在项目根目录下的 database 文件夹下:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'database', 'db.sqlite3'),
}
}
复制代码
env_file:
- .envs/.production
复制代码
容器启动时读取 .envs/.production文件中的内容,将其注入环境变量。
咱们建立一下这个文件,把 secret_key 写进去。
DJANGO_SECRET_KEY=2pe8eih8oah2_2z1=7f84bzme7^bwuto7y&f(#@rgd9ux9mp-3
复制代码
注意将这些包含敏感信息的文件加入版本控制工具的忽略列表里,防止一不当心推送到公开仓库供大众观光。
ports:
- "8000:8000"
复制代码
暴露容器内的 8000 端口而且和宿主机的 8000 端口绑定,因而咱们就能够经过宿主机的 8000 端口访问容器。
command: /start.sh 容器启动时将执行 start.sh,从而启动 django应用。
nginx 服务容器也相似,只是注意它从数据卷 static 中取静态文件并映射到 nginx 容器内的 /apps/hellodjango_blog_tutorial/static,因此咱们在 nginx 的配置中:
location /static {
alias /apps/hellodjango_blog_tutorial/static;
}
复制代码
这样能够正确代理静态文件。
万事具有,在本地执行一下下面的两条命令来构建容器和启动容器。
docker-compose -f production.yml build
docker-compose -f production.yml up
复制代码
此时咱们能够经过域名来访问容器内的应用,固然,因为 Nginx 在本地环境的容器内运行,须要修改一下 本地 hosts 文件,让域名解析为本地 ip 便可。
若是本地访问没有问题了,那么就能够直接在服务器上执行上面两条命令以一样的方式启动容器,django 应用就顺利地在服务上部署了。
既然线上环境都使用 Docker 了,不妨开发环境也一并使用 Docker 进行开发。开发环境的镜像和 docker-compose 文件比线上环境简单一点,由于不用使用 nginx。
开发环境的镜像文件,放到 compose\local 下:
FROM python:3.6-alpine
ENV PYTHONUNBUFFERED 1
RUN apk update \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev
WORKDIR /app
RUN pip install pipenv -i https://pypi.douban.com/simple
COPY Pipfile /app/Pipfile
COPY Pipfile.lock /app/Pipfile.lock
RUN pipenv install --system --deploy --ignore-pipfile
COPY ./compose/local/start.sh /start.sh
RUN sed -i 's/\r//' /start.sh
RUN chmod +x /start.sh
复制代码
要注意和线上环境不一样的是,咱们没有把整个代码 copy 到容器里。线上环境代码通常比较稳定,而对于开发环境,因为须要频繁修改和调试代码,若是咱们把代码 copy 到容器,那么容器外作的代码修改,容器内部是没法感知的,这样容器内运行的应用就无法同步咱们的修改了。因此咱们会把代码经过 Docker 的数据卷来管理。
start.sh 再也不启动 gunicorn,而是使用 runserver 启动开发服务器。
#!/bin/sh
python manage.py migrate
python manage.py runserver 0.0.0.0:8000
复制代码
而后建立一个 docker-compose 文件 local.yml(和 production.yml 同级),用于管理开发容器。
version: '3'
services:
djang_blog_tutorial_v2_local:
build:
context: .
dockerfile: ./compose/local/Dockerfile
image: django_blog_tutorial_v2_local
container_name: django_blog_tutorial_v2_local
working_dir: /app
volumes:
- .:/app
ports:
- "8000:8000"
command: /start.sh
复制代码
注意咱们将整个项目根目录下的文件挂载到了 /app 目录下,这样就能容器内就能实时反映代码的修改了。
若是容器在本地运行没有问题了,线上环境的容器运行也没有问题,由于理论上,咱们在线上服务器也会构建和本地测试用的容器如出一辙的环境,因此几乎能够确定,只要咱们服务器有 Docker,那么咱们的应用就能够成功运行。
首先在服务安装 Docker,安装方式因系统而异,方式很是简单,咱们以 CentOS 7 为例,其它系统请参考 Docker 的官方文档。
首先安装必要依赖:
$ sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
复制代码
而后添加仓库源:
$ sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
复制代码
最后安装 Docker:
$ sudo yum install docker-ce docker-ce-cli containerd.io
复制代码
启动 Docker:
$ sudo systemctl start docker
复制代码
(境外服务器忽略)设置 Docker 源加速(使用 daocloud 提供的镜像源),不然拉取镜像时会很是慢
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.m.daocloud.io
复制代码
在 docker 中运行一个 hello world,确认 docker 安装成功:
$ sudo docker run hello-world
复制代码
docker 安装成功了,还要安装一下 docker-compose。实际上是一个 python 包,咱们直接经过 pip 安装就能够了:
$ pip install docker-compose
复制代码
为了不运行一些 docker 命令时可能产生的权限问题,咱们把系统当前用户加入到 docker 组里:
$ sudo usermod -aG docker ${USER}
复制代码
添加组后要重启一下 shell(ssh 链接的话就断开重连)。
万事俱备,只欠东风了!
开始准备让咱们的应用在 docker 容器里运行。因为以前咱们把应用部署在宿主机上,首先来把相关的服务停掉:
# 停掉 nginx,由于咱们将在容器中运行 nginx
$ sudo systemctl stop nginx
# 停掉博客应用
$ supervisorctl stop hellodjango-blog-tutorial -c ~/etc/supervisord.conf
复制代码
接下来拉取最新的代码到服务器,进入项目根目录下,建立线上环境须要的环境变量文件:
$ mkdir .envs
$ cd .envs
$ vi .production
复制代码
将线上环境的 secret key 写入 .production 环境变量文件,
DJANGO_SECRET_KEY=2pe8eih8oah2_2z1=7f84bzme7^bwuto7y&f(#@rgd9ux9mp-3
复制代码
保存并退出。
回到项目根目录,运行 build 命令构建镜像:
$ docker-compose -f prodcution.yml build
复制代码
而后咱们能够开始启动根据构建好的镜像启动 docker 容器,不过为了方便,咱们的 docker 进程仍然由 supervisor 来管理,咱们修改一下博客应用的配置,让它启动时启动 docker 容器。
打开 ~/etc/supervisor/conf.d/hellodjango-blog-tutorial.ini,修改成以下内容:
[program:hellodjango-blog-tutorial]
command=docker-compose -f production.yml up --build
directory=/home/yangxg/apps/HelloDjango-blog-tutorial
autostart=true
autorestart=unexpected
user=yangxg
stdout_logfile=/home/yangxg/etc/supervisor/var/log/hellodjango-blog-tutorial-stdout.log
stderr_logfile=/home/yangxg/etc/supervisor/var/log/hellodjango-blog-tutorial-stderr.log
复制代码
主要就是把以前的使用 Gunicorn 来启动服务换成了启动 docker。
修改 ini 配置 要记得 reread 使配置生效:
$ supervisorctl -c ~/etc/supervisord.conf
> reread
> start
复制代码
docker 容器顺利启动,访问咱们的博客网站。抛掉镜像编排的准备工做,至关于咱们只执行了一条构建容器并启动容器的命令就部署了咱们的博客应用。若是换台服务器,也只要再执行一下镜像构建和启动容器的命令,服务就又能够起来!这就是 docker 的好处。
因为开发 django 用的最多的 IDE Pycharm 也能很好地集成 Docker,我如今开发工做已经全面拥抱 Docker 了,史无前例的体验,史无前例的方便和稳定,必定要学着用起来!
最后,因为 Nginx 在新的容器里运行,因此须要从新申请和配置 https 证书,这和以前是同样,只是此前 Nginx 在宿主机上,此次咱们在容器里运行 certbot 命令。编排 nginx 镜像时已经安装了 certbot,直接执行命令便可,在 docker 容器内执行命令以下:
咱们首先经过 docker ps 命令查看正在运行的容器,记住 nginx 容器的名字,而后使用 docker exec -it 容器名 命令的格式在指定容器内执行命令,因此咱们执行:
$ docker exec -it nginx certbot --nginx
复制代码
根据提示输入信息便可,过程和上一节在宿主机上部署如出一辙,这里再也不重复。
fabric 无需修改,来尝试本地执行一下:
pipenv run fab -H server_ip --prompt-for-login-password -p deploy
复制代码
完美!至此,咱们的博客已经稳定运行于线上,陆陆续续会有更多的人来访问咱们的博客,让咱们来继续完善它的功能吧!
『讲解开源项目系列』——让对开源项目感兴趣的人再也不畏惧、让开源项目的发起者再也不孤单。跟着咱们的文章,你会发现编程的乐趣、使用和发现参与开源项目如此简单。欢迎留言联系咱们、加入咱们,让更多人爱上开源、贡献开源~