本文将介绍建立一个用Django编写的markdown编辑器应用程序,并将其运行在备受关注和讨论的Docker中。Docker具有传统虚拟机的所有优点,例如,一个独立的系统与你的开发机隔离,并消除了许多缺点,如系统资源消耗、设置时间和维护。
在构建网络应用程序时,你可能已经达到了一个地步,你希望以更接近生产环境的方式运行你的应用程序。Docker允许你设置你的应用程序的运行时间,使其以与生产环境完全相同的方式运行,在相同的操作系统上,使用相同的环境变量,以及你需要的任何其他配置和设置。
在文章结束时,你将能够:
- 理解什么是Docker以及它是如何被使用的
- 建立一个简单的Python Django应用程序,以及
- 创建一个简单的
Dockerfile,建立一个运行Django网络应用程序服务器的容器 - 设置一个持续集成和交付(CI/CD)管道来自动构建和测试Docker镜像
Docker到底是什么?
Docker的主页对Docker做了如下描述:
"Docker是一个用于构建、运输和运行分布式应用的开放平台。它为程序员、开发团队和运营工程师提供了他们所需的通用工具箱,以利用现代应用程序的分布式和网络化性质"。
简单地说,Docker让你有能力在一个受控的环境中运行你的应用程序,即所谓的容器,根据你定义的指令来构建。容器利用你的机器的资源,很像传统的虚拟机(VM)。然而,在系统资源方面,容器与传统的虚拟机有很大不同。传统的虚拟机使用Hypervisors操作,Hypervisors管理底层硬件对虚拟机的虚拟化。这意味着它们在系统要求方面是很大的:
- Docker不需要将整个操作系统安装到VirtualBox或VMWare等虚拟机上这种经常耗时的过程。
- 你用几个命令创建一个容器,然后通过Docker文件在上面执行你的应用程序。
- Docker为你管理大部分的操作系统虚拟化,所以你可以继续编写应用程序,并根据你的要求在你建立的容器中运送它们。
- Docker文件可以被共享,以便其他人建立容器,并通过在现有容器镜像的基础上扩展其中的指令。
- 容器也是高度可移植的,无论在哪个主机操作系统上执行,都会以同样的方式运行。可移植性是Docker的一个巨大优势。
前提条件
在你开始本教程之前,请确保你的系统已经安装了以下内容:
设置一个Django网络应用程序
让我们直接跳到我们要进行docker化的应用程序。我们将从Martor项目开始,它为Django实现了一个实时的markdown编辑器。
- 进入django-martor-editor仓库,并叉开它。
agusmakmun / django-markdown-editor
- 克隆该仓库到你的本地机器上。
让我们来看看项目结构,我省略了一些我们今天不会访问的文件和文件夹:
.
├── requirements.txt # < Python module list
└── martor_demo # < Django Project root
├── app # < App code
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── migrations
│ ├── models.py
│ ├── templates
│ ├── urls.py
│ └── views.py
├── manage.py # < Django management tool
└── martor_demo # < Django main settings
├── settings.py
├── urls.py
└── wsgi.py
你可以在官方网站上阅读更多关于Django的结构。你使用manage.py 脚本来控制应用程序的开发。
不过在我们运行它之前,我们需要下载和所有的依赖项。
首先,创建一个Python虚拟环境:
$ python -m venv venv
$ echo venv/ >> .gitignore
$ source venv/bin/activate
接下来,添加一些我们需要的Python模块。
$ echo martor >> requirements.txt
$ echo gunicorn >> requirements.txt
用来安装所有的模块:
$ pip install -r requirements.txt
将改动推送到GitHub:
$ git add .gitignore requirements.txt
$ git commit -m "added martor and gunicorn"
$ git push origin master
并启动开发服务器,你可以在http://127.0.0.1:8000,访问你的应用程序:
$ cd martor_demo
$ python manage.py runserver

如果你检查前面命令的输出,你会看到这个信息。
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
Django打印了这个警告,因为它检测到数据库还没有被初始化。
要初始化一个本地测试数据库并摆脱这条消息,请运行:
$ python manage.py makemigrations
$ python manage.py migrate
在Django中测试
在本节中,让我们为应用程序添加一些测试。测试是我们防止bug的第一道防线。
Django使用标准的Unittest库,所以我们可以马上开始编写测试。
创建一个名为app/testPosts.py 的文件:
# app/testPosts.py
from django.test import TestCase
from app.models import Post
class PostTestCase(TestCase):
def testPost(self):
post = Post(title="My Title", description="Blurb", wiki="Post Body")
self.assertEqual(post.title, "My Title")
self.assertEqual(post.description, "Blurb")
self.assertEqual(post.wiki, "Post Body")
该代码是一个普通单元测试的说明:
- 从应用程序中导入
Post模型。 - 用一些初始值创建一个
post对象。 - 检查这些值是否符合预期。
运行该测试案例:
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
Django提供的另一个测试是部署检查表。这些是检查有潜在危险的安全设置的脚本。
要运行检查表:
$ python manage.py check --deploy
你可能会看到一些警告。对于演示来说,我们可以忍受这些警告。一旦你进入生产阶段,你可能想仔细看看这些信息和它们的含义。
静态与动态文件
在我们继续之前,我们只需要做一个修改。Django有静态文件的概念。这些文件没有任何Python代码,它们通常是图片、CSS样式表或JavaScript。
一旦我们发布到生产中,静态和动态之间的区别是很重要的。动态文件的代码必须在每个请求中进行评估,所以它们的运行成本很高。静态文件不需要任何执行,它们不需要大量的资源来提供,并且可以通过代理和CDN进行缓存。
要配置静态文件的位置:
- 编辑文件
martor_demo/settings.py - 找到
STATIC_ROOT和MEDIA_ROOT变量,用这些行来替换
# martor_demo/settings.py
. . .
STATIC_ROOT = os.path.join(BASE_DIR, "static")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
Django将所有静态文件收集在一个目录中:
$ python manage.py collectstatic
风格检查器
我们要做的最后一项测试是样式检查。Python有严格的形式,可以用Flake8这个静态分析工具进行验证。
安装并运行该工具以检查项目中是否有样式错误:
$ pip install flake8
$ flake8 . --max-line-length=127
持续集成
在继续之前,将所有的修改推送到GitHub:
$ git add martor_demo/settings.py app/testPosts.py
$ git add static
$ git commit -m "add unit test and static files"
$ git push origin master
有了最初的应用程序和一些测试,现在是时候专注于使用持续集成(CI),在一个干净、可重复的环境中构建和测试代码了。
在Semaphore中设置CI/CD管道只需几分钟,一旦设置完毕,Semaphore会在每次更新时为你运行测试,如果没有错误,会自动构建Docker镜像:
- 访问Semaphore,使用GitHub的注册按钮注册一个免费账户。
- 使用Projects旁边的+(加号)按钮,找到你的GitHub仓库。

- 点击你的仓库旁边的选择:

- 点击 "继续 "进入工作流程设置
- 选择单项工作模板,首先点击自定义

这将打开工作流程生成器。

该生成器的主要元素是:
- 管道:管道是由从左到右执行的块组成的。管道通常有一个特定的目标,如构建和测试代码。
- 区块:区块将可以并行执行的工作分组。一个块中的工作通常有类似的命令和配置。一旦一个块中的所有工作完成,下一个块就开始了。
- 作业:作业定义了执行工作的命令。它们从父块中继承其配置。
- 晋升:我们可以定义多个管道,并将它们与促销活动连接起来,以获得复杂的多阶段工作流程。
第一个块必须下载Python模块并建立项目:
- 点击第一个块,将其名称设为 "Build"
- 在作业命令块中输入以下内容
sem-version python 3.9
checkout
mkdir .pip_cache
cache restore
pip install --cache-dir .pip_cache -r requirements.txt
cache store

构建工作
- 单击 "Run the Workflow"。
- 设置分支为master。
- 点击 "开始"。

保存你的改动
在Semaphore的内置工具箱中,我们有三个命令:
- sem-version激活一个支持的语言的特定版本。在Python的情况下,它还设置了一个虚拟环境。
- checkout使用git来克隆正确的代码修订版。
- cache在整个项目的缓存中存储和恢复文件。缓存可以计算出它需要保留哪些文件和目录。我们可以用它来避免每次都要下载Python包。
最初的CI管道将立即开始,几秒钟后,它应该没有错误地完成:

构建阶段工作
添加第二个块来运行测试:
- 点击编辑工作流程
- 点击+添加块。
- 将该块的名称设为 "测试"
- 打开 "序幕"部分,输入以下命令。序幕将在区块中的每个作业之前执行
sem-version python 3.9
checkout
cache restore
pip install --cache-dir .pip_cache -r requirements.txt
cd martor_demo
python manage.py makemigrations
python manage.py migrate
python manage.py test
- 添加第二个作业,称为 "检查表",并添加以下命令:
cd martor_demo
python manage.py check --deploy
- 这是一个添加一些样式检查的好地方。添加第三个作业,名为 "样式检查",并添加以下命令。我们正在使用flake8来检查代码的样式:
pip install flake8
flake8 martor_demo/ --max-line-length=127

测试块
- 单击 "运行工作流"并开始:

CI流水线
Dockerizing the Application
你现在有了一个简单的Web应用程序,可以进行部署了。到目前为止,你一直在使用Django内置的开发网络服务器。
现在是时候对项目进行设置了,以便在Docker中使用一个更强大的Web服务器来运行该应用程序,该服务器是为处理生产级别的流量而建立的。
- Gunicorn:Gunicorn是一个用于Python的HTTP服务器。这个Web服务器很强大,可以处理生产级别的流量,而Django的开发服务器更多的是为了在你的本地机器上进行测试。它可以处理所有的动态文件。
- Ngnix:是一个通用的HTTP服务器,我们将使用它作为一个反向代理来提供静态文件。
在普通的服务器上,设置应用程序将是一项艰苦的工作;我们需要安装和配置Python和Ngnix,然后在防火墙中打开相应的端口。Docker为我们节省了所有这些工作,它创建了一个单一的镜像,并配置了所有的文件和服务,随时可以使用。我们将创建的镜像可以在任何运行Docker的系统上运行。
安装Docker
Docker的关键目标之一是可移植性,因此它能够安装在各种操作系统上。
在Windows和OSX上安装Docker Desktop。
对于Linux,Docker几乎普遍存在于所有主要发行版中。
编写Docker文件
下一个阶段是为你的项目添加一个Dockerfile 。这将允许Docker构建它将在你刚刚创建的Docker机器上执行的镜像。编写一个Dockerfile 是相当简单的,有许多元素可以重复使用和/或在网上找到。Docker提供了很多你构建镜像所需的功能。如果你需要在你的项目上做一些更多的定制,Dockerfiles是足够灵活的,你可以这样做。
Dockerfile 的结构可以被认为是一系列关于如何构建你的容器/图像的说明。例如,绝大多数的Docker文件都会以引用Docker提供的基础镜像开始。一般来说,这将是一个最新的Ubuntu版本或其他Linux操作系统的普通镜像。在那里,你可以设置目录结构、环境变量、下载依赖性和许多其他标准的系统任务,然后再执行将运行你的网络应用的进程。
开始Dockerfile ,在你的项目根部创建一个名为Dockerfile 的空文件。然后,在Dockerfile 中添加第一行,指示在哪个基础镜像上构建。你可以创建你自己的基础镜像,并将其用于你的容器,这对于有许多团队想要以相同方式部署他们的应用程序的部门来说是有益的。
我们将在我们项目的根目录下创建Docker文件,往上走一个目录:
$ cd ..
创建一个名为nginx.default 的新文件。这将是我们对nginx的配置。我们将监听端口8020 ,为/opt/app/martor_demo/static 目录下的静态文件提供服务,并将其余的连接转发到端口8010 ,Gunicorn将在那里进行监听:
# nginx.default
server {
listen 8020;
server_name example.org;
location / {
proxy_pass http://127.0.0.1:8010;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /static {
root /opt/app/martor_demo;
}
}
创建一个名为start-server.sh 的服务器启动脚本。这是一个Bash脚本,用于启动Gunicorn和Ngnix:
#!/usr/bin/env bash
# start-server.sh
if [ -n "$DJANGO_SUPERUSER_USERNAME" ] && [ -n "$DJANGO_SUPERUSER_PASSWORD" ] ; then
(cd martor_demo; python manage.py createsuperuser --no-input)
fi
(cd martor_demo; gunicorn martor_demo.wsgi --user www-data --bind 0.0.0.0:8010 --workers 3) &
nginx -g "daemon off;"
然后,你将gunicorn 命令的第一个参数定为martor_demo.wsgi 。这是一个对Django为我们生成的wsgi 文件的引用,这是一个Web服务器网关接口文件,是Web应用程序和服务器的Python标准。在不深入了解WSGI的情况下,该文件只是定义了应用程序的变量,而Gunicorn知道如何与该对象交互以启动Web服务器。
然后,你向命令传递两个标志,bind ,将运行中的服务器连接到端口8020 ,你将使用该端口通过HTTP与运行中的Web服务器通信。最后,你指定workers ,这是处理进入你的应用程序的请求的线程数量。Gunicorn建议将这个值设置为(2 x $num_cores) + 1 。你可以在Gunicorn的文档中阅读更多关于配置的内容。
让脚本可执行:
$ chmod 755 start-server.sh
创建Docker文件:
FROM python:3.9-buster
. . .
值得注意的是,我们使用的是一个专门为处理Python 3.9应用程序而创建的基础镜像,以及一组指令,它将在你的其他部分之前自动运行Dockerfile 。
接下来,在容器内添加Nginx安装命令和COPY 配置文件:
. . .
RUN apt-get update && apt-get install nginx vim -y --no-install-recommends
COPY nginx.default /etc/nginx/sites-available/default
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
. . .
现在是在容器内复制源文件和脚本的时候了。我们可以使用COPY 命令来复制文件,使用RUN 命令来在构建时执行程序。
我们还将复制Python包并安装它们。最后,我们确保所有的文件都有正确的所有者:
. . .
RUN mkdir -p /opt/app
RUN mkdir -p /opt/app/pip_cache
RUN mkdir -p /opt/app/martor_demo
COPY requirements.txt start-server.sh /opt/app/
COPY .pip_cache /opt/app/pip_cache/
COPY martor_demo /opt/app/martor_demo/
WORKDIR /opt/app
RUN pip install -r requirements.txt --cache-dir /opt/app/pip_cache
RUN chown -R www-data:www-data /opt/app
. . .
服务器将在端口8020 上运行。因此,你的容器必须被设置为允许访问这个端口,以便你可以通过HTTP与运行中的服务器通信。要做到这一点,请使用EXPOSE 指令,使该端口可用:
. . .
EXPOSE 8020
STOPSIGNAL SIGTERM
CMD ["/opt/app/start-server.sh"]
你的Dockerfile 的最后一部分是执行前面添加的启动脚本,这将使你的Web服务器在端口8020 上运行,等待通过HTTP接收请求。你可以使用CMD 指令来执行这个脚本。
有了所有这些,你的最终Dockerfile 应该是这样的:
# Dockerfile
FROM python:3.9-buster
# install nginx
RUN apt-get update && apt-get install nginx vim -y --no-install-recommends
COPY nginx.default /etc/nginx/sites-available/default
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
# copy source and install dependencies
RUN mkdir -p /opt/app
RUN mkdir -p /opt/app/pip_cache
RUN mkdir -p /opt/app/martor_demo
COPY requirements.txt start-server.sh /opt/app/
COPY .pip_cache /opt/app/pip_cache/
COPY martor_demo /opt/app/martor_demo/
WORKDIR /opt/app
RUN pip install -r requirements.txt --cache-dir /opt/app/pip_cache
RUN chown -R www-data:www-data /opt/app
# start server
EXPOSE 8020
STOPSIGNAL SIGTERM
CMD ["/opt/app/start-server.sh"]
现在你已经准备好构建容器镜像,然后运行它,看它是否能正常工作。
构建和运行容器
一旦你的系统有了Docker,构建容器就非常简单了。下面的命令将寻找你的Docker文件,并下载所有必要的层,让你的容器镜像运行。之后,它将运行Dockerfile 中的说明,并留下一个可以启动的容器。
为了构建你的容器,你将使用docker build 命令,并为容器提供一个标签或名称,这样你以后想运行它时就可以引用它。命令的最后部分告诉Docker要从哪个目录构建:
$ mkdir -p .pip_cache
$ docker build -t django-markdown-editor .
在输出中,你可以看到Docker处理你的每一条命令,然后输出容器的构建完成。它将给你一个容器的唯一ID,这个ID也可以在命令中与标签一起使用。
最后一步是使用Docker运行你刚刚构建的容器:
$ docker run -it -p 8020:8020 \
-e DJANGO_SUPERUSER_USERNAME=admin \
-e DJANGO_SUPERUSER_PASSWORD=sekret1 \
-e DJANGO_SUPERUSER_EMAIL=admin@example.com \
django-markdown-editor
Superuser created successfully.
[2022-05-04 17:49:43 +0000] [11] [INFO] Starting gunicorn 20.1.0
[2022-05-04 17:49:43 +0000] [11] [INFO] Listening at: http://0.0.0.0:8010 (11)
[2022-05-04 17:49:43 +0000] [11] [INFO] Using worker: sync
[2022-05-04 17:49:43 +0000] [16] [INFO] Booting worker with pid: 16
[2022-05-04 17:49:43 +0000] [17] [INFO] Booting worker with pid: 17
[2022-05-04 17:49:43 +0000] [18] [INFO] Booting worker with pid: 18
该命令告诉Docker运行该容器,并将暴露的8020端口转发到你本地机器上的8020端口。通过-e ,我们设置环境变量,自动创建一个管理员用户。
在你运行这个命令后,你应该能够在你的浏览器中访问http://localhost:8020 和 http://localhost:8020/admin 来访问该应用程序。
持续部署
在手动验证了应用程序在Docker中的行为符合预期之后,下一步就是部署。
我们将用一个新的CI管道来扩展我们的CI管道,运行构建命令并将镜像上传到Docker Hub。
你需要一个Docker Hub的登录名才能继续:
- 前往Docker Hub
- 使用 "开始 "按钮进行注册
- 回到你的Semaphore账户
- 在左边的导航菜单上,点击配置下的秘密

- 点击创建 新的秘密。
- 用你的Docker Hub账户的用户名和密码创建一个名为 "dockerhub "的秘密。

保存Docker Hub的密码
- 单击 "保存 秘密":
Semaphore秘密将你的证书和其他敏感数据存储在GitHub仓库之外,并在激活时使它们作为环境变量在你的作业中可用。
Dockerize管道
- 在Semaphore上打开CI管道,再次点击编辑工作流程。
- 使用+添加首次推广的虚线按钮,创建一个与主管道相连的新管道。
- 将新管道称为:"Dockerize"
- 确保勾选 "启用自动推广"选项,以便新管道能够自动启动。
- 点击新管道上的第一个块。将其名称设为 "Docker build"。
- 打开 "序幕",输入以下命令。序幕从缓存中恢复包,并准备好数据库。
sem-version python 3.9
checkout
cache restore
mkdir -p .pip_cache
pip install --cache-dir .pip_cache -r requirements.txt
cd martor_demo
python manage.py makemigrations
python manage.py migrate
cd ..
- 在作业命令框中键入以下命令。该作业拉取最新的镜像(如果存在),构建一个较新的版本,并将其推送到Docker Hub。
--cache-from选项告诉Docker尝试重用一个较早的镜像以加快进程。
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker pull $DOCKER_USERNAME/django-markdown-editor:latest || true
docker build --cache-from=$DOCKER_USERNAME/django-markdown-editor:latest -t $DOCKER_USERNAME/django-markdown-editor:latest .
docker push $DOCKER_USERNAME/django-markdown-editor:latest
- 在秘密部分,选中dockerhub的秘密:

- 点击运行工作流程并开始:

最后的管道
CI/CD流水线自动启动。一旦所有测试通过,Dockerize管道将创建一个新的Docker镜像并推送到Docker Hub。

Docker化的应用程序
你可以把镜像拉到你的机器上,像往常一样运行它。
$ docker pull YOUR_DOCKER_USERNAME/django-markdown-editor
接下来的步骤
我们已经准备了一个Docker镜像,其中包含了试用该应用程序所需的一切。你可以在任何提供Docker工作负载的机器或云服务上运行这个镜像(它们都有)。
下一步是选择一个持久性数据库。我们的Docker镜像使用的是本地的SQLite文件,因此,每次容器重新启动时,所有数据都会丢失。
有很多选择:
- 使用云供应商提供的管理数据库服务。
- 在一个虚拟机内运行数据库。
- 创建第二个带有数据库的容器,使用卷来保存数据。
无论你选择哪种方案,你都必须:
- 配置Django以连接到数据库。
- 在Semaphore上创建一个带有数据库连接密码的新秘密。
- 在启动Docker容器时,将数据库连接参数作为环境变量传递。
总结
在本教程中,你已经学会了如何建立一个简单的Python Django网络应用程序,将其包裹在一个生产级的网络服务器中,并创建一个Docker容器来执行你的网络服务器进程。
如果你喜欢通过这篇文章工作,请随时分享,如果你有任何问题或评论,请在下面的部分留言。我们将尽最大努力回答他们,或为你指出正确的方向。
让你的应用程序运行是Kubernetes之路的第一步。有了Kubernetes,你可以大规模地运行你的应用程序,并提供无延迟的更新。