仓库源文站点原文


title: "Docker 简要: 部署 Python 应用简例" categories: Tech updated: 2022-10-19 comments: true

mathjax: false

Introduction

Why did Docker decide to go with a whale for their logo? Apparently to express its product's values of expedition, automation, encapsulation and simplification. As they explain "the whale is carrying a stack of containers on its way to deliver those to you".

来自官网首页

下面改编自 官方教程. 总得来说, Dockerfile 类似脚本, 记录了构建镜像 (文件) 的指令. 运行着的镜像称为容器 (类似进程). 而 docker-compose.yml 记录了运行镜像的参数配置 (类似用 shell 脚本记录命令行).

<!-- more -->

Build images

<details><summary><b>Enable BuildKit</b><font color="deepskyblue"> (Show more »)</font></summary> <p>BuildKit allows you to build Docker images efficiently. See <a href="https://docs.docker.com/language/python/build-images/#enable-buildkit">here</a>.</p></details>

Sample application

示例 是 python 的 Flask 应用.

python-docker
|____ app.py
|____ requirements.txt
|____ Dockerfile

Create a Dockerfile

Dockerfile 是文本文件, 存储了用 docker build 命令构建 Docker 镜像时的指令. 推荐用默认名 Dockerfile 命名这个文件.

<details><summary><b>The optional first line: parser directive</b><font color="deepskyblue"> (Show more »)</font></summary> <p>The first line to add to a Dockerfile is a <a href="https://docs.docker.com/engine/reference/builder/#syntax"><code># syntax</code> parser directive</a>. While <em>optional</em>, this directive instructs the Docker builder what syntax to use when parsing the Dockerfile, and allows older Docker versions with BuildKit enabled to upgrade the parser before starting the build. <a href="https://docs.docker.com/engine/reference/builder/#parser-directives">Parser directives</a> must appear before any other comment, whitespace, or Dockerfile instruction in your Dockerfile, and should be the first line in Dockerfiles.</p> <pre><code class="language-docker"># syntax=docker/dockerfile:1 </code></pre> <p>We recommend using <code>docker/dockerfile:1</code>, which always points to the latest release of the version 1 syntax.</p></details>

基础镜像.

FROM python:3.8-slim-buster

名字为 -slim 的镜像只装了 minimal packages, 所以用之前记得测试一下.

然后可以写

ENV PYTHONUNBUFFERED 1

Setting PYTHONUNBUFFERED to a non-empty value different from 0 ensures that the python output i.e. the stdout and stderr streams are sent straight to terminal (e.g. your container log) without being first buffered and that you can see the output of your application (e.g. django logs) in real time.

设置 working directory. Docker 后续操作都以 WORKDIR 为默认路径, 使用相对路径即可. 如果 WORKDIR 不存在会自动创建.

WORKDIR /app

复制文件. 第一个参数是本地文件系统中的路径, 第二个参数是 Docker 容器文件系统中的路径 (相对于 WORKDIR).

COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt  # 运行命令
COPY . .  # 全部复制过去

镜像在容器中运行时, 执行的指令. 只能有一个 CMD. The main purpose of a CMD is to provide defaults for an executing container.

# CMD ["executable","param1","param2"]
CMD ["python3", "-m" , "flask", "run", "--host=0.0.0.0"]

Build an image

# 主参数是路径, --tag (-t) 给镜像命名 `name:tag`
docker build --tag python-docker . 
docker images  # 查看所有镜像

# Name components may contain lowercase letters, digits and separators. 
# A separator is defined as a period, one or two underscores, or one or more dashes. 
# A name component may not start or end with a separator.
docker tag python-docker:latest python-docker:v1.0.0

上述操作导致同一镜像 (相同 image id) 有两个名字, 去掉 (untagged) 一个.

docker rmi python-docker:v1.0.0

Run your image as a container

A container is a normal operating system process except that this process is isolated in that it has its own file system, its own networking (不能直接通过 localhost 对应端口访问), and its own isolated process tree separate from the host.

docker run 命令中加上 --publish (-p).

# [host port]:[container port]
docker run --publish 8000:5000 python-docker

如上容器内暴露的端口是 5000 (Flask 默认), 对应主机 (localhost) 暴露的端口是 8000, 此时才能通过 localhost:8000 访问到.

Run in detached mode

# 查看容器, 加上 --all (-a) 显示停止的容器
# 返回一张表格, 其中 command 是之前写的 CMD
# ports 列出端口
# names 如果没有给定会随机生成, 比如 funny_brahmagupta
docker ps

# --detach (-d) 后台运行
docker run -d -p 8000:5000 python-docker

# pass the name of the container or the container ID
docker stop funny_brahmagupta

When you restart a container, it starts with the same flags or commands that it was originally started with.

docker restart funny_brahmagupta
docker stop funny_brahmagupta

When you remove a container, it is no longer running, nor it is in the stopped status, but the process inside the container has been stopped and the metadata for the container has been removed.

# 可以传入多个容器
docker rm funny_brahmagupta blahblah

给容器命名.

docker run -d -p 8000:5000 --name rest-server python-docker

Use containers for development

<details><summary><b>Using volumes</b><font color="deepskyblue"> (Show more »)</font></summary> <p><a href="https://docs.docker.com/storage/volumes/">Document</a></p> <p>Volumes are the preferred mechanism for persisting data generated by and used by Docker containers. While <a href="https://docs.docker.com/storage/bind-mounts/">bind mounts</a> are dependent on the directory structure and OS of the host machine, volumes are completely managed by Docker.</p> <p>In addition, volumes are often a better choice than persisting data in a container's writable layer, because a volume does not increase the size of the containers using it, and the volume's contents exist outside the lifecycle of a given container.</p></details>

Run a database in a container

创建两个 volumes.

docker volume create mysql
docker volume create mysql_config

docker volume ls
# docker volume rm mysql

Now we'll create a network that our application and database will use to talk to each other. The network is called a user-defined bridge network and gives us a nice DNS lookup service which we can use when creating our connection string.

docker network create mysqlnet

docker run --rm -d -v mysql:/var/lib/mysql \
  -v mysql_config:/etc/mysql -p 3306:3306 \
  --network mysqlnet \
  --name mysqldb \
  -e MYSQL_ROOT_PASSWORD=p@ssw0rd1 \
  mysql  # 镜像名
<details><summary><b>参数</b><font color="deepskyblue"> (Show more »)</font></summary> <ul> <li><code>--rm</code>: Automatically remove the container when it exits.</li> <li><code>--volume</code> (<code>-v</code>): Consists of three fields, separated by colon characters (<code>:</code>).<ul> <li>In the case of named volumes, the first field is the name of the volume, and is unique on a given host machine. For anonymous volumes, the first field is omitted. volume 名.</li> <li>The second field is the path where the file or directory are mounted in the container. 挂载路径, 用于存储和共享持久化数据.</li> <li>The third field is optional. 看文档.</li> </ul> </li> <li><code>--network</code>: Connect a container to a network.</li> <li><code>--env</code> (<code>-e</code>): Set environment variables.</li> </ul></details>

Now, let's make sure that our MySQL database is running and that we can connect to it.

docker exec -ti mysqldb mysql -u root -p
<details><summary><b>参数</b><font color="deepskyblue"> (Show more »)</font></summary> <p>docker exec: Run a command in a running container.</p> <pre><code> docker exec [OPTIONS] CONTAINER COMMAND [ARG...] </code></pre> <p>COMMAND will run in the default directory (WORKDIR) of the container.</p> <ul> <li><code>--tty</code> (<code>-t</code>): Allocate a pseudo-TTY. <ul> <li><a href="https://unix.stackexchange.com/questions/21147/what-are-pseudo-terminals-pty-tty">What are pseudo terminals (pty/tty)?</a></li> <li><a href="https://stackoverflow.com/questions/30137135/confused-about-docker-t-option-to-allocate-a-pseudo-tty">Confused about Docker -t option to allocate a pseudo-TTY</a>. It basically makes the container start look like a terminal connection session.</li> </ul> </li> <li><code>--interactive</code> (<code>-i</code>): Keep STDIN open even if not attached. 交互式</li> </ul> <p>多个 flags 可以写在一起, 成为 <code>-ti</code> 或者 <code>-it</code>. <a href="https://stackoverflow.com/questions/30172605/how-do-i-get-into-a-docker-containers-shell">How do I get into a Docker container's shell?</a></p> <ul> <li><code>-user</code> (<code>-u</code>): Username or UID (format: <name|uid>[:<group|gid>]).</li> <li><code>-p</code> 没查到, <code>--privileged</code>?</li> </ul></details>

下面会提示输入密码, 登录 MySQL 数据库. Press CTRL-D to exit the MySQL interactive terminal.

Connect the application to the database

修改了 Python 程序, 见 原文.

docker build --tag python-docker-dev .
docker run \
  --rm -d \
  --network mysqlnet \
  --name rest-server \
  -p 8000:5000 \
  python-docker-dev

This allows us to access the database by its container name.

Use Compose to develop locally

用 YAML 文件 docker-compose.dev.yml 存储配置, 不必每次都 docker run 一堆参数.

version: '3.8'

services:
  web:
    build:
      context: .
    ports:
      - 8000:5000
    volumes:
      - ./:/app

  mysqldb:
    image: mysql
    ports:
      - 3306:3306
    environment:
      - MYSQL_ROOT_PASSWORD=p@ssw0rd1
    volumes:
      - mysql:/var/lib/mysql
      - mysql_config:/etc/mysql

volumes:
  mysql:
  mysql_config:

We expose port 8000 so that we can reach the dev web server inside the container. We also map our local source code into the running container to make changes in our text editor and have those changes picked up in the container.

docker-compose -f docker-compose.dev.yml up --build
<details><summary><b>参数</b><font color="deepskyblue"> (Show more »)</font></summary> <p>docker compose up: Create and start containers</p> <p>新版 Compose V2 把 <code>docker-compose</code> 替换为 <code>docker compose</code></p> <ul> <li><code>--file</code> (<code>-f</code>): Specify an alternate compose file (default: docker-compose.yml). 新版是 compose.yaml, 但也兼容旧的默认名.</li> <li><code>--build</code>: Build images before starting containers.</li> <li><code>--detach</code> (<code>-d</code>)</li> </ul></details>
# 假设用默认名
docker-compose stop
docker-compose up --build
docker-compose up -d

Compose file

The Compose file is a YAML file defining version (DEPRECATED), services (REQUIRED), networks, volumes, configs and secrets.

Computing components of an application are defined as Services. Services communicate with each other through Networks. Services store and share (主机和容器共享) persistent data into Volumes. A Secret is a specific flavor of configuration data for sensitive data that SHOULD NOT be exposed without security considerations. Configs 先不管.

services:
  web:  # 服务的名字, 可以随便起
    # specifies the build configuration for creating container image from source
    build:
      # defines either a path to a directory containing a Dockerfile
      # When the value supplied is a relative path, it MUST be interpreted as relative to the Compose file’s parent folder.
      context: .

    ports:
      # SHOULD always be specified as a (quoted) string, to avoid conflicts with yaml base-60 float. 下面加上双引号了.
      # 其他例子: "127.0.0.1:5000-5010:5000-5010" 加上 IP 和端口 range.
      - "8000:5000"

    volumes:
      # VOLUME:CONTAINER_PATH
      # VOLUME: MAY be either a host path on the platform 
      # hosting containers (bind mount) or a volume name
      - ./:/app

    # 环境变量
    environment:
      - MYSQL_ROOT_PASSWORD=p@ssw0rd1

    # container's network stack is not isolated from the Docker host (the container shares the host's networking namespace), and the container does not get its own IP-address allocated. 如果用 host, 则前面的 ports 端口映射也不再适用.
    network_mode: host

其中 build 可以写为

build: .

To reuse a volume across multiple services, a named volume MUST be declared in the top-level volumes key.

...
    volumes:
      - mysql:/var/lib/mysql
      - mysql_config:/etc/mysql

volumes:
  mysql:
  mysql_config:

上述例子没指定主机路径则默认挂载到 /var/lib/docker/volumes/...

Further reading

优化 Dockerfile

Best practices for writing Dockerfiles

Because an image is built during the final stage of the build process, you can minimize image layers by leveraging build cache.

For example, if your build contains several layers, you can order them from the less frequently changed (to ensure the build cache is reusable) to the more frequently changed:

依赖相比其他项目代码更不容易改变, 因此先复制 requirements.txt 安装依赖, 再把其他代码拷贝到容器里.

Miscs