我们有一个特定的目标,即创建一个能够运行我们的微服务 ThoughtsBackend 的容器。为此,我们有几个要求:
- We need to copy our code to the container.
- The code needs to be served through a web server.
因此,概括地说,我们需要创建一个带有 Web 服务器的容器,添加我们的代码,配置它以运行我们的代码,并在启动容器时提供结果。
We will store most of the configuration files inside subdirectories in the
./docker directory.
作为 Web 服务器,我们将使用 uWSGI (https://uwsgi-docs.readthedocs.io/ zh/最新/)。 uWSGI 是一个 Web 服务器,能够通过 WSGI 协议为我们的 Flask 应用程序提供服务。 uWSGI 非常可配置,有很多选项,并且能够直接提供 HTTP。
A very common configuration is to have NGINX in front of uWSGI to serve static files, as it's more efficient for that. In our specific use case, we don't serve many static files, as we're running a RESTful API, and, in our main architecture, as described in
Chapter 1,
Making the Move – Design, Plan, and Execute, there's already a load balancer on the frontend and a dedicated static files server. This means we won't be adding an extra component for simplicity. NGINX usually communicates to uWSGI using the
uwsgi protocol, which is a protocol specifically for the uWSGI server, but it can also do it through HTTP. Check the NGINX and uWSGI documentation.
我们来看看docker/app/Dockerfile文件.有两个阶段;第一个是编译依赖项:
此阶段执行以下步骤:
- Names the stage compile-image, inheriting from Alpine.
- Installs python3.
- Installs the build dependencies, including the gcc compiler and Python headers (python3-dev).
- Creates a new virtual environment. We will install all the Python dependencies here.
- The virtual environment gets activated.
- Installs uWSGI. This step compiles it from code.
You can also install the included uWSGI package in the Alpine distribution, but I found the compiled package to be more complete and easier to configure, as the Alpine
uwsgi package requires you to install other packages such as
uwsgi-python3,
uwsgi-http, and so on, then enable the plugin in the uWSGI config. The size difference is minimal. This also allows you to use the latest uWSGI version and not depend on the one in your Alpine distribution.
- Copy the requirements.txt file and install all the dependencies. This will compile and copy the dependencies to the virtual environment.
第二阶段是准备运行的容器。让我们来看看:
它执行以下操作:
- Labels the image as runtime-image and inherits from Alpine, as previously.
- Installs Python and other requirements for the runtime.
请注意,需要安装编译所需的任何运行时。比如我们安装
libffi 在运行时和
libffi-dev 编译,需要
密码学包。尝试访问(不存在的)库时,不匹配将引发运行时错误。这
dev 库通常包含运行时库。
- Copy the uWSGI configuration and script to start the service. We'll take a look at that in a moment.
- Create a user to run the service, and set it as the default using the USER command.
This step is not strictly necessary as, by default, the root user will be used. As our containers are isolated, gaining root access in one is inherently more secure than in a real server. In any case, it's good practice to not configure our public-facing service accessing as root and it will remove some understandable warnings.
- Copy the virtual environment from the compile-image image. This installs all the compiled Python packages. Note that they are copied with the user to run the service, to have access to them. The virtual environment is activated.
- Copy the application code.
- Define the run parameters. Note that port 8000 is exposed. This will be the port we will serve the application on.
If running as root, port
80 can be defined. Routing a port in Docker is trivial, though, and other than the front-facing load balancer, there's not really any reason why you need to use the default HTTP port. Use the same one in all your systems, though, which will remove uncertainty.
请注意,应用程序代码复制到文件末尾。应用程序代码很可能是最常更改的代码,因此这种结构利用 Docker 缓存并仅重新创建最后几层,而不必从头开始。在设计 Dockerfile 时要考虑到这一点。
Also, keep in mind that there's nothing stopping you from changing the order while developing. If you're trying to find a problem with a dependency, and so on, you can comment out irrelevant layers or add steps later once the code is stable.
现在让我们构建我们的容器。看到创建了两个图像,但只有一个被命名。另一个是编译映像,它更大,因为它包含编译器,依此类推:
现在我们可以运行容器了。为了能够访问内部端口 8000,我们需要使用 -p 选项对其进行路由:
访问我们的本地浏览器到 127.0.0.1 显示我们的应用程序。您可以在标准输出中看到访问日志:
您可以使用 docker exec 从不同的终端访问正在运行的容器并执行新的 shell。请记住添加 -it 以保持终端打开。使用 docker ps 检查当前运行的容器以找到容器 ID:
您可以使用 Ctrl + C 停止容器,或者更优雅地从另一个终端停止它:
日志将显示 graceful stop:
正确捕获 SIGTERM 并优雅地停止我们的服务对于避免突然终止服务很重要。我们将看到如何在 uWSGI 中配置它,以及其他元素。