hedzr

hedzr 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

hedzr 发布了文章 · 7月18日

docker-compose 编排指南 (v3.8)

缘起

关于 docker-compose 的安装,关于 docker 的基本介绍,不在本文的指导范围内。

这篇文章基本上是 docker-compose YAML 文件格式的严格的英译中。这么做,缘起于昨天想起扫描一下 docker-compose 编排中怎么使用 ${PWD} 的问题,结果中文没有一点帮助,还是官网最终解决了我的模糊之处。因此我觉得还是应该做一篇比较严谨的译文以及说明,来阐释 docker-compose 编排的各项细节。

以下,我们主要是介绍 docker-compose 编排文件格式版本3 的各项细节。

阅读本文,你应该有 docker-compose 的基本认识,至少有基本的早期(版本2)编排格式的了解。

关于授权

译文从属于原文 https://docs.docker.com/compo...

译文 https://github.com/hedzr/docker-compose-file-format 本身以 MIT 方式(忽略 hedzr.github.io 站台级许可申明,遵循 repo 本身的申明)分发。

v3.8 说明

上一次我做了一个旧的译文:docker-compose 编排指南 (v3.7)。这是基于 v3.7 的。今次的译文是对其的一个更新。不得不说,这种查漏补缺挺烦人的。

编排格式版本3

历史

版本3是自 docker-engine 1.13 推出以来 docker-compose 所支持的格式。这之前 docker 在 1.12 中推出了 swarm mode 来构建一个虚拟网络中的虚拟计算资源,同时也大幅度改进了 docker 的网络以及存储的支持。

对于 docker-compose 编排格式与 docker-engine 之间的关系,下面这张表(摘自官网)有清晰的对照。

Compose file formatDocker Engine release
3.819.03.0+
3.718.06.0+
3.618.02.0+
3.517.12.0+
3.417.09.0+
3.317.06.0+
3.217.04.0+
3.11.13.1+
3.01.13.0+
2.417.12.0+
2.317.06.0+
2.21.13.0+
2.11.12.0+
2.01.10.0+
1.01.9.1.+

特别添加

编排文件结构与示例

这是一个版本3+的典型文件结构样本:

version: "3.7" # 适用于 v3.8 没问题
services:

  redis:
    image: redis:alpine
    ports:
      - "6379"
    networks:
      - frontend
    deploy:
      replicas: 2
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

  db:
    image: postgres:9.4
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend
    deploy:
      placement:
        constraints: [node.role == manager]

  vote:
    image: dockersamples/examplevotingapp_vote:before
    ports:
      - "5000:80"
    networks:
      - frontend
    depends_on:
      - redis
    deploy:
      replicas: 2
      update_config:
        parallelism: 2
      restart_policy:
        condition: on-failure

  result:
    image: dockersamples/examplevotingapp_result:before
    ports:
      - "5001:80"
    networks:
      - backend
    depends_on:
      - db
    deploy:
      replicas: 1
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

  worker:
    image: dockersamples/examplevotingapp_worker
    networks:
      - frontend
      - backend
    deploy:
      mode: replicated
      replicas: 1
      labels: [APP=VOTING]
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
      placement:
        constraints: [node.role == manager]

  visualizer:
    image: dockersamples/visualizer:stable
    ports:
      - "8080:8080"
    stop_grace_period: 1m30s
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    deploy:
      placement:
        constraints: [node.role == manager]

networks:
  frontend:
  backend:

volumes:
  db-data:

在这个样本中,顶级结构由 versionservicesnetworksvolumes 等等标签构成。这与版本2并没有什么翻天覆地的区别。

services 章节中,你可以定义若干个服务,每个服务通常运转着一个容器,这些服务构成了一个整体的设施栈,又或是服务群。

一般来说我们会把一堆拉拉杂杂的东西,例如一堆微服务什么的,编排成一个服务栈,让他们整体对外服务,从而避免细节外露,也可以加强架构设计弹性和对整个服务栈进行伸缩(而不是面对大批微服务去逐个处理)。

[Dockerfile] 关于 ENTRYPOINTCMD

Docker RUN vs CMD vs ENTRYPOINT - Go in Big Data

In a nutshell
  • RUN executes command(s) in a new layer and creates a new image. E.g., it is often used for installing software packages.
  • CMD sets default command and/or parameters, which can be overwritten from command line when docker container runs.
  • ENTRYPOINT configures a container that will run as an executable.

在 Dockerfile 中讨论这三条命令:

首先,RUN 不考虑,其用途可以被视作执行一条Shell指令。

CMDENTRYPOING 相似但实际上区别较大:CMD 指定默认命令及其命令行参数、或者命令行参数的结尾的部分,并且能够在启动容器时被覆盖(通过外部命令行指定的方式),ENTRYPOINT 指定该容器被运行时的启动命令,可以附带命令行参数,这条命令在被实际执行时还会附加 CMD 所指定的内容作为其命令行的结尾部分。

典型用法

所以一个惯用法是:

ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]

然后执行该容器的效果类似于:

$ docker run -it test-container
Hello world
$ docker run -it test-container David
Hello David

用于你的容器

所以对于自定义容器来说,你的主要应用程序的路径可以被用作 ENTRYPOINT 而在 CMD 中提供默认参数:

ENTRYPOINT ["/usr/local/app/my-app"]
CMD ["--help"]

这将会包装你的 my-app 像一个外露的工具,默认时显示其帮助屏,你可以指定参数去运行 my-app:

$ docker run -it my-app-container
[... help screen here ...]
$ docker run -it my-app-container server run --foreground
[ 等同于执行 my-app server run --foreground ]

END

[Dockerfile] 多遍构建

多遍构建被典型地用于CI/CD。

例如步骤0可以被命名为 builder,负责从源码编译到目标文件,而步骤1则从步骤0中抽出目标文件用于部署打包,并生成最终的容器镜像,随后步骤0的中间层则被抛弃(仅指不被打包在结果容器中,实际上这些中间层在 Docker 的构建缓存中仍然是存在且可以被重用的),这些中间层不会出现在最终容器镜像中,从而有效地缩减了最终容器镜像的尺寸,而这个结果也是从语义上、从逻辑上被自洽的。

FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

编排格式手册 - service 配置参考

接下来会是一个参考手册应有的章节结构,我们按照字母顺序列列举出了服务编排的指令,例如 portsvolumescmdentry 等等。

Compose 文件是一个 YAML格式的文本文件,其中定义了 service、networks 以及 volumes。缺省时 docker-compose 使用和检索 ./docker-compose.yml 文件并解释之。

Tip: 你总是可以使用 .yml.yaml 作为该脚本文件的后缀,它们都会正确工作。

service (服务)的配置包含若干定义,它们指明了如何将一个容器运行为服务,这些定义实际上会被传递给 docker run 作为其命令行参数的一部分。同样的道理,networks、volumes 等等的定义也采用同样的原理去影响诸如 docker network create,或者 docker volume create 等命令的实际运行。

你可以使用在配置定义值中使用环境变量,它们有类似于 BASH 变量替代的语法,你可以以 ${VARIABLE},请参阅 变量替换 小节的深入探讨。

接下来本章节中列举所有有效的服务配置项。

build

该选项被用于构建。

build 可以是一个指向构建上下文的路径字符串,例如:

version: "3.8"
services:
  webapp:
    build: ./dir

也可以是一个更详细的定义。这包括了 context 项所指定的路径,以及可选的 dockerfile 文件和构建参数 args

version: "3.8"
services:
  webapp:
    build:
      context: ./dir
      dockerfile: Dockerfile-alternate
      args:
        buildno: 1

如果你在指定了 build 的同时还指定了 image,那么构建的结果会被标记为相应的名字,这就好像 docker build -t container-name:tag dir 做的那样:

    build: "./dir"
    image: "company/webapp:v1.1.9"
对于 YAML 而言,避免歧义的安全做法是对字符串加上引号包围。

上面这个例子,会找到 ./dir 文件夹中的构建上下文(默认是寻找到 Dockerfile)并完成构建,最后将其标记为 company/webapp 的名字,以及 v1.1.9 的 Tag。

NOTE 当用在docker stack部署场景时:

build 选项会被忽略。

context

可以是一个包含 Dockerfile 的文件夹,也可以是一个指向 git repository 的 URL。

如果指定了一个相对路径,那么这个路径是相对于 docker-compose.yml 文件的。这个路径也会被传送给 Docker daemon 用于进行构建。

docker-compose 发动构建动作和标记构建结果(按照image名字),在那之后按照对应的名字使用它。

build:
  context: ./dir

dockerfile

可以指定不同于默认名称 Dockerfile 的其它文件名用于构建。注意同时必须指定路径到 context

    build:
      context: .
      dockerfile: Dockerfile-alternate

args

指定构建参数。通常是指用于构建时的参数(参见 Dockerfile 中的 ARG)。

以下是一个简要的概述:

首先,在 Dockerfile 指定参数:

ARG buildno
ARG gitcommithash

RUN echo "Build number: $buildno"
RUN echo "Based on commit: $gitcommithash"

然后指定构建参数的实际值(传入Map或者数组都是可以的):

  build:
    context: .
    args:
      buildno: 1
      gitcommithash: cdc3b19

或:

build:
  context: .
  args:
    - buildno=1
    - gitcommithash=cdc3b19
NOTE: 在 Dockerfile中,如果在 FROM 之前指定 ARG,那么这个 ARG 对于其后的 FROM 闭包是无效的。

多个 FROM 分别切割出了多个构建的闭包。

想要 ARG 在每个 FROM 闭包中都有效,你需要在每个闭包中都指定它。

Understand how ARGS and FROM interact 中有更详细的相关讨论。

你可以略过显式指定构建参数。此时,该参数的实际值取决于构建时运行环境。

  args:
    - buildno
    - gitcommithash
NOTE: YAML的布尔量(true, false, yes, no, on, off)必须用引号包围,以便 docker-compose正确处理。

cache_from

since v3.2

指定一个映像列表,用于缓存的解决。

build:
  context: .
  cache_from:
    - alpine:latest
    - corp/web_app:3.14

labels

since v3.3

向构建的映像中添加元数据标签,可以是一个数组或一个字典。

我们推荐使用反向DNS标注性前缀以防止你的标签和使用者的标签相冲突:

build:
  context: .
  labels:
    com.example.description: "Accounting webapp"
    com.example.department: "Finance"
    com.example.label-with-empty-value: ""

# anothor example
build:
  context: .
  labels:
    - "com.example.description=Accounting webapp"
    - "com.example.department=Finance"
    - "com.example.label-with-empty-value"

network

Since v3.4

设置在 RUN 构建过程中要链接的网络,该网络也将被用于查询和提取依赖的容器。

build:
  context: .
  network: host
build:
  context: .
  network: custom_network_1

设为 none 则在构建时禁用网络查询和提取:

build:
  context: .
  network: none

shm_size

since v3.5

设置构建容器时的 /dev/shm 分区大小。整数格式按字节表示,但也可以使用字符串格式(byte value):

build:
  context: .
  shm_size: '2gb'

build:
  context: .
  shm_size: 10000000

target

since v3.4

构建定义与 Dockerfile 中的特定的步骤(Stage),参阅 multi-stage build docs

build:
  context: .
  target: prod

cap_add, cap_drop

添加或者移除容器的 Linux 能力。完整的清单可以参阅 man 7 capabilities

cap_add:
  - ALL

cap_drop:
  - NET_ADMIN
  - SYS_ADMIN
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

Linux 能力机制很大程度上是一种安全机制。具体含义、用途和引申属于 Linux 操作系统范畴,不再赘述。

cgroup_parent

可选地为容器指定一个上级 cgroupcgroup 也是 Linux 容器化实现的最重要的基本概念之一。

cgroup_parent: m-executor-abcd
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

command

覆盖容器内默认的 command.

command: bundle exec thin -p 3000

command 也可以被指定为一个列表。实际上这也是更被推荐的方式,无歧义而且安全,而且和 dockerfile 中的格式具有一致性:

command: ["bundle", "exec", "thin", "-p", "3000"]

configs

为每个服务提供具体化的访问 config 的权限。

一个 config 包含一系列的配置信息,这些信息可能被通过多种途径所创建。容器的部署引用这些配置时,可以更好地区格诸如生产环境参数这样的问题。另一方面,敏感信息也因此可以被单独隔离到一个安全的区域中,在一定程度上减少了泄露的可能性。

NOTE: 指定的配置必须已经存在,或者已经被定义在顶级 configs 中了(defined in the top-level configs configuration)。否则整个容器栈的部署将会失败。

支持两个不同的语法变体格式。更详尽的信息应参考 configs

短格式

只指定配置名。容器因此可以访问配置 /<config_name 和挂载它(挂载的源和目标均为该配置名)。

version: "3.8"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    configs:
      - my_config
      - my_other_config
configs:
  my_config:
    file: ./my_config.txt
  my_other_config:
    external: true

上面的例子使用短格式在 redis 容器的服务中定义了 my_configmy_other_config 的引用。这里的 my_config 指定为一个宿主机文件 ./my_config.txt,而 my_other_config 被指定为外部的(资源),这意味着相应的资源已经在 Docker 中被定义了,或许是通过 docker config create 建立的,又或者是被别的容器栈部署所建立的。

如果外部资源找不到,那么容器栈的部署将会失败,并且抛出一个 config not found 的错误。

Note: config 定义仅在 v3.3 及更高版本的 docker-compose 格式中被支持。

长格式

长格式提供更多的信息来表述一个 config 在哪儿,如何被找到,怎么被使用:

  • source: 配置名
  • target: 该配置将被挂载到容器中的路径。默认为 /<source>
  • uid & gid: 数字值的 Linux/Unix UIDGID,如果没有指定则为0。Windows中不支持。
  • mode: 8进制的文件权限。默认值为 0444

    配置是不可写的,因为它们被挂载于临时的文件系统中。因此如果你设置了写许可,这将被忽略。

    可执行位是可以被设置的。

下面的例子类似于短格式的例子:

version: "3.8"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    configs:
      - source: my_config
        target: /redis_config
        uid: '103'
        gid: '103'
        mode: 0440
configs:
  my_config:
    file: ./my_config.txt
  my_other_config:
    external: true

在这里,redis 容器的服务并未访问 my_other_config

你可以授权一个服务访问多个配置,你也可以混用长短格式。

(在顶级)定义一个配置(config)并未隐含着某个服务就能访问到它。

container_name

指定一个自定义的容器名,而不是由 docker-compose 自己生成一个默认的。

container_name: my-web-container

由于 Docker 容器名必须是唯一的,所以你无法伸缩一个自定义了容器名的服务。

NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

credential_spec

since v3.3

从 v3.8 开始支持被用于组管理服务账户 gMSA(group Managed Service Account)方式。

为受控服务账户配置凭据。这个选项只被用于 Windows 容器服务。credential_spce 只能使用格式 file://<filename> or registry://<value-name>

当使用 file: 时,被参考的文件必须被置于 Docker 数据文件夹(通常是 C:\ProgramData\Docker\)的 CredentialSpec 子目录之下。下面的例子将会从 C:\ProgramData\Docker\CredentialSpecs\my-credential-sp 载入凭据信息:

credential_spec:
  file: my-credential-spec.json

当使用 registry: 时,凭据信息将会被从 Docker daemon 主机的 Windows Registry 中读入。一个注册表表项必须位于:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs

之中。

下面的例子读入 my-credential-spec 注册表项值:

credential_spec:
  registry: my-credential-spec

gMSA 配置例子

当为一个服务配置 gMSA 凭据时,参考如下的例子:

version: "3.8"
services:
  myservice:
    image: myimage:latest
    credential_spec:
      config: my_credential_spec

configs:
  my_credentials_spec:
    file: ./my-credential-spec.json|

depends_on

表示服务之间的依赖关系。服务依赖引发如下的行为:

  • docker-compose up 按照依赖顺序依次启动服务。在下面的例子中,dbredis 先于 web 被启动。
  • docker-compose up SERVICE 自动包括了 SERVICE 的依赖项。在下面的例子中,docker-compose up web 将会自动启动 dbredis
  • docker-compose stop 按照依赖顺序依次停止服务。在下面的例子中,web 将会被先于 dbredis 被停止。

简单的示例如下:

version: "3.8"
services:
  web:
    build: .
    depends_on:
      - db
      - redis
  redis:
    image: redis
  db:
    image: postgres

使用 depends_on 时应该注意的几件事:

  • depends_on 并不意味着等待 dbredis 就绪后才启动 web,而是在它们被启动后就会接着启动 web。如果要想等到服务就绪可用,应该参阅 Controlling startup order
  • 版本 3 不再支持 condition 表述。
  • depends_on 选项在部署到 swarm mode 时被忽略。

    也参阅 deploying a stack in swarm mode

deploy

Version 3 only.

指定和部署以及运行相关的配置。

只会对部署到一个使用 docker stack deployswarm 有影响。

docker-compose updocker-compose run 时被忽略。

version: "3.8"
services:
  redis:
    image: redis:alpine
    deploy:
      replicas: 6
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

几个子选项是可用的:

endpoint_mode

swarm.

Since Version 3.2.

指定外部客户端连接到一个 swarm 集群时使用的服务发现方法。

  • endpoint_mode: vip - Docker 为服务请求一个虚拟IP(VIP)用于访问。

    Docker在客户端和服务的有效的工作节点之间对请求进行自动负载均衡路由。客户端无需知道服务有多少可用节点,也不必知道服务节点的IP地址和端口号。

    这是默认方式。

  • endpoint_mode: dnsrr - 使用 DNS round-robin (DNSRR) 算法进行服务发现。Docker会为服务设置一个DNS条目,因而在进行对应的 DNS 解析时通过服务名称会返回一个IP地址清单。客户端因此直接选择一个具体的端点进行访问。
version: "3.8"

services:
  wordpress:
    image: wordpress
    ports:
      - "8080:80"
    networks:
      - overlay
    deploy:
      mode: replicated
      replicas: 2
      endpoint_mode: vip

  mysql:
    image: mysql
    volumes:
       - db-data:/var/lib/mysql/data
    networks:
       - overlay
    deploy:
      mode: replicated
      replicas: 2
      endpoint_mode: dnsrr

volumes:
  db-data:

networks:
  overlay:

endpoint_mode 的选项也被同样地用作 swarm mode 命令行选项(参阅 docker service create)。至于 docker swarm命令的一个快捷清单,可以查阅 Swarm mode CLI commands

要学习更多关于 swarm mode 的网络模型已经服务发现机制 的知识,请查看 Configure service discovery

labels

为服务指定标签。这些标签只被作用于对应的服务,而不是被应用到服务的容器或者容器实例上。

version: "3.8"
services:
  web:
    image: web
    deploy:
      labels:
        com.example.description: "This label will appear on the web service"

要为容器设置标签的话,在 deploy 之外为服务指定 labels

version: "3.8"
services:
  web:
    image: web
    labels:
      com.example.description: "This label will appear on all containers for the web service"

mode

可以是 globalreplicatedglobal 表示严格地一个 swarm node 跑一个服务,replicated 表示可以跑多个容器实例。默认是 replicated

参阅 swarm 主题下的 Replicated and global services

version: "3.8"
services:
  worker:
    image: dockersamples/examplevotingapp_worker
    deploy:
      mode: global

placement

指定 constaints 和 preferences。

参阅docker服务建立的相关文档以了解更多的关于 constraints 和 [preferences 的相关信息,包括相应的语法,可用的类型等等的完整描述。

version: "3.8"
services:
  db:
    image: postgres
    deploy:
      placement:
        constraints:
          - node.role == manager
          - engine.labels.operatingsystem == ubuntu 14.04
        preferences:
          - spread: node.labels.zone

max_replicas_per_node

Since version 3.8.

如果一个服务是可副本的 replicated (这是默认的), max_replicas_per_node 将会 限制副本数量(limit the number of replicas)

当太多任务申请新的任务节点且超出了 max_replicas_per_node 限制值时,一个 no suitable node (max replicas per node limit exceed) 错误将会被抛出。

version: "3.8"
services:
  worker:
    image: dockersamples/examplevotingapp_worker
    networks:
      - frontend
      - backend
    deploy:
      mode: replicated
      replicas: 6
      placement:
        max_replicas_per_node: 1

replicas

如果服务是 replicated 的,replicas 则为其指定一个数值,此数值指示了一个 swarm 节点上最多可以跑多少个容器实例。

version: "3.7"
services:
  worker:
    image: dockersamples/examplevotingapp_worker
    networks:
      - frontend
      - backend
    deploy:
      mode: replicated
      replicas: 6

resources

配置资源约束。

NOTE: 对于非 swarm mode 而言,这个表项替换 older resource constraint options(诸如 cpu_shares, cpu_quota, cpuset, mem_limit, memswap_limit, mem_swappiness 等在版本3之前版本中的表项)。

Upgrading version 2.x to 3.x 中有相应的描述。

这些资源约束表项都具有一个单一值,相当于 docker service create 中的等价物。

在如下的例子中,redis 服务被约束为不可使用超出50M的内存,单核50%的CPU使用率,同时也保留 20M 内存以及 25%的CPU使用率作为基准值。

version: "3.8"
services:
  redis:
    image: redis:alpine
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
        reservations:
          cpus: '0.25'
          memory: 20M

以下的主题描述 swarm 场景下的服务或容器资源约束的可用选项。

Out Of Memory Exceptions (OOME)

企图在你的服务和容器实例中使用超过系统拥有的内存,那么将得到 Out Of Memory Exception (OOME) 。此时,容器实例,或者 Docker daemon,可能会被内核的 OOM 管理器所清除。

要防止这样的情况发生,请确定你的应用程序合法有效地使用内存。对于这样的风险,查阅 Understand the risks of running out of memory 以获知进一步的评估须知。

restart_policy

指示当容器实例退出时,如何重启。替换 restart

  • condition: 可以为 none, on-failureany (默认为 any)
  • delay: 在尝试重启之前的等候时长(默认为0)。应该为其指定一个 duration
  • max_attempts: 试图尝试重启多少次后放弃重启的尝试。默认为不放弃。
  • window: 要确定一次重启是否成功,需要等候的时长。默认为无等待立即认定为已成功。应该为其指定一个 duration
version: "3.8"
services:
  redis:
    image: redis:alpine
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

rollback_config

Version 3.7 file format and up

在滚动更新失败的场景下服务应该如何回滚:

  • parallelism:同时回滚的容器的数量值。如果设置为0,所有容器将被同时回滚。
  • delay: 每个容器组被回滚前的等待时长(默认为0)
  • failure_action: 一个回滚失败时应当执行的动作。可以是 continuepause(默认为pause
  • monitor: 失败的回滚状态被更新到监视器的周期(ns|us|ms|s|m|h)默认为 0s
  • max_failure_ratio: 回滚时失败的可容忍的比例(默认为0)
  • order: 回滚的操作顺序。可以为 stop-firststart-first(默认为 stop-first

update_config

指示服务应该如何被更新。这对于配置为滚动更新时有用:

  • parallelism:同时更新的容器的数量值。如果设置为0,所有容器将被同时回滚。
  • delay: 每个容器组被更新前的等待时长(默认为0)
  • failure_action: 一个更新失败时应当执行的动作。可以是 continue, rollbackpause(默认为pause
  • monitor: 失败的更新状态被更新到监视器的周期(ns|us|ms|s|m|h)默认为 0s
  • max_failure_ratio: 更新时失败的可容忍的比例(默认为0)
  • order: 更新的操作顺序。可以为 stop-firststart-first(默认为 stop-first
NOTEorder 只在 v3.4 及之后有效。
version: "3.8"
services:
  vote:
    image: dockersamples/examplevotingapp_vote:before
    depends_on:
      - redis
    deploy:
      replicas: 2
      update_config:
        parallelism: 2
        delay: 10s
        order: stop-first

DOCKER STACK DEPLOY 不支持者

下列的子选项(为 docker-compose updocker-compose run 所支持)是在 docker stack deploy 中不被支持的:

Tip: 请参阅 如何为 service 服务模式和 swarm 集群模式使用卷而配置 docker-stack.yml 文件 - how to configure volumes for services, swarms, and docker-stack.yml files 章节。 Volumes(卷)在 swarms 模式和 services 模式中被支持的,但你只能采用命名卷,又或是与受约束于有权访问必需卷的节点的服务关联.

devices

要被映射的设备清单。其用法和 docker 命令的 --device 相同。

devices:
  - "/dev/ttyUSB0:/dev/ttyUSB0"
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

dns

自定义 DNS 服务器列表。可以指定单一值或一个清单。

dns: 8.8.8.8
dns:
  - 8.8.8.8
  - 9.9.9.9

dns_search

自定义DNS搜索域名。可以指定单一值或一个清单。

dns_search: example.com
dns_search:
  - dc1.example.com
  - dc2.example.com

entrypoint

覆盖 dockerfile 中定义的默认的 entrypoint 值。

entrypoint: /code/entrypoint.sh

入口点也可以是一个清单:

entrypoint:
    - php
    - -d
    - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so
    - -d
    - memory_limit=-1
    - vendor/bin/phpunit
NOTE: 设置一个 entrypoint 不但覆盖 Dockerfile 中的任何 ENTRYPOINT 默认值,还会清理 Dockerfile 中的任何 CMD 默认值。所以 Dockerfile 中的 CMD 将会被忽略。

env_file

从给定的文件中引入环境变量值。可以是单一值或一个清单。

env_file: .env
env_file:
  - ./common.env
  - ./apps/web.env
  - /opt/secrets.env

对于 docker-compose -f FILE 来说,env_file 的路径是相对于 FILE 所在文件夹的。

environment 中声明的环境变量将会覆盖掉这里所引入的值。

对应的文件中,每一行应该使用 VAR=VAL 格式定义一个环境变量。行首为 # 表示为注释行,和空白行一样被忽略。

# Set Rails/Rack environment
RACK_ENV=development
NOTE: 如果服务定义了 build 项,在构建过程中,由 env_file 所定义的环境变量并不可见。只能使用 build 的子选项 args 去定义构建时环境变量值。

VAL 的值被原样照用,且不能被修改。例如如果值由引号所包围,那么值的表示量中也包含引号。

环境变量文件的顺序也需要被注意。位置靠后的环境变量文件中所定义的变量值会覆盖掉早前定义的旧值。

:原文竟然说了这么多!

Keep in mind that the order of files in the list is significant in determining the value assigned to a variable that shows up more than once. The files in the list are processed from the top down. For the same variable specified in file a.env and assigned a different value in file b.env, if b.env is listed below (after), then the value from b.env stands. For example, given the following declaration in docker-compose.yml:

services:
  some-service:
    env_file:
      - a.env
      - b.env

And the following files:

# a.env
VAR=1

and

# b.env
VAR=hello

$VAR is hello.

environment

添加环境变量。可以使用一个数组或者一个字典。任何布尔量:true, false, yes, no 等等都必须用引号包围为字符串字面量。

只有key值的环境变量的value值依赖于 docker-compose 运行时的主机环境,这对于防止敏感信息泄露是有用的。

environment:
  RACK_ENV: development
  SHOW: 'true'
  SESSION_SECRET:

# 或
environment:
  - RACK_ENV=development
  - SHOW=true
  - SESSION_SECRET
NOTE: 如果服务定义了 build 项,在构建过程中,由 env_file 所定义的环境变量并不可见。只能使用 build 的子选项 args 去定义构建时环境变量值。

expose

暴露端口到链接的服务。这些端口并不会被发布到宿主机。只能指定内部端口被用于暴露。

expose:
 - "3000"
 - "8000"

external_links

将在 docker-compose.yml 之外启动的容器链接到给定服务上。

和遗留选项 links 有相似的语义。

external_links:
 - redis_1
 - project_db_1:mysql
 - project_db_1:postgresql
Note

在 docker-compose.yml 之外建立的容器(The externally-created containers)必须至少连接到一个相同的网络,才能与一个定义在 docker-compose.yml 中的服务相链接 。Links 选项已经过时了,我们推荐使用 networks 来代替它。

NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

更推荐的做法是通过 networks 构造一个子网以进行容器之间的链接。

extra_hosts

添加主机名映射。这些映射关系会被添加到 /etc/hosts 中。此功能等价于命令行参数 --add-host

extra_hosts:
 - "somehost:162.242.195.82"
 - "otherhost:50.31.209.229"

对于这个服务来说,在容器中的 /etc/hosts 文件中,相应的主机名及其IP将被建立为一个条目。例如:

162.242.195.82  somehost
50.31.209.229   otherhost

healthcheck

since v2.1

用于确认一个服务是否是“健康”的。参阅 HEALTHCHECK Dockerfile instruction

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]
  interval: 1m30s
  timeout: 10s
  retries: 3
  start_period: 40s

interval, timeoutstart_period 应该被指定为 durations.

Note: start_period 只在 v3.4 及以后可用。

test 必须是一个单一字符串值或一个列表。如果是一个列表,那么第一项必须是 NONE, CMD, CMD-SHELL 之一。如果是一个字符串,隐含地表示一个 CMD-SHELL 前缀。

# Hit the local web app
test: ["CMD", "curl", "-f", "http://localhost"]

如上述示例,但隐含调用 /bin/sh,和以下的形式是等效的。

test: ["CMD-SHELL", "curl -f http://localhost || exit 1"]
test: curl -f https://localhost || exit 1

要禁用任何在映像内指定的缺省的健康检查向,可以使用 disable: true。这和指定 test: ["NONE"] 是等效的。

healthcheck:
  disable: true

image

指定映像的名称。

image: redis
image: ubuntu:14.04
image: tutum/influxdb
image: example-registry.com:4000/postgresql
image: a4bc65fd

如果映像在宿主机不存在,Compose 会尝试下拉它,除非你也指定了 build 项。

init

since v3.7

在容器中运行一个 init 进程并转发信号。设置为 true 为服务使能这个特性。

version: "3.8"
services:
  web:
    image: alpine:latest
    init: true
缺省的 init 进程使用二进制执行文件 Tini,在需要时它将被安装于 daemon主机的位置 /usr/libexec/docker-init 。你也可以配置 daemon 使用一个不同的二进制文件,通过 init-path,参阅 configuration option

isolation

指定一个容器的隔离层级/技术。在 Linux 中,仅支持 default 值。在 Windows 中,可以接受的值有:default, processhyperv

labels

为容器添加元数据标签,参考 Docker labels。可以为其指定一个数组或一个字典。

我们建议你采用反向DNS标注方式来定义你的标签,这可以有效地避免标签名称的冲突。

labels:
  com.example.description: "Accounting webapp"
  com.example.department: "Finance"
  com.example.label-with-empty-value: ""
labels:
  - "com.example.description=Accounting webapp"
  - "com.example.department=Finance"
  - "com.example.label-with-empty-value"

links

已经是一个遗留特征了。在不久的未来将被移除。

按:所以我也不精确翻译了:) 太啰嗦了。

链接另一个服务到本容器。可以同时制定服务名称和链接别名(SERVICE:ALIAS),也可以略过链接别名。

web:
  links:
   - db
   - db:database
   - redis

已经被链入的服务将会是主机名(即链接别名 ALIAS)可访问的。

对于服务间通讯来说,链接并不是必须的。默认情况下,任何服务都可以通过服务名访问到其他服务。参阅 Links topic in Networking in Compose

链接也表示一个依赖关系,但这已经是 depends_on 的任务了,所以链接也并不必要了。

logging

为服务指定日志转发配置。

logging:
  driver: syslog
  options:
    syslog-address: "tcp://192.168.0.42:123"

driver 指定了驱动名称,这和 --log-driver 是等效的。

缺省值为 json-file

driver: "json-file"
driver: "syslog"
driver: "none"
Note

使用 docker-compose updocker-compose logs 检索日志时,只有 json-filejournald 驱动格式的日志才能被打印到控制台,其他日志驱动程序会将日志转发到对应的目的地,本地将不会检索到任何日志输出信息。

可用的转发驱动器可以参考 https://docs.docker.com/confi...

使用 option 指定驱动器的选项,如同 --log-opt 那样。例如为 syslog 这样指定:

driver: "syslog"
options:
  syslog-address: "tcp://192.168.0.42:123"

缺省的日志转发驱动为 json-file。对此可以指定日志切割尺寸以及最多保持的日志历史文件个数:

options:
  max-size: "200k"
  max-file: "10"

上面显示的这个例子中,日志的文件存储将被限制为 200kB,并且在超出时会被截断到旧历史日志,这样的历史文件将被限制为不超过 10 个,更旧的历史文件将被抛弃。

这里有一个完整的 docker-compose.yml 文件实例,演示了如何限制日志存储空间:

version: "3.8"
services:
  some-service:
    image: some-service
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"
有效可用的日志选项有赖于具体使用的日志驱动程序。

上面的控制日志文件个数及其大小的日志选项,适用于 json-file driver。它们可能并不适用于别的日志驱动程序。要了解完整的针对每个日志驱动程序可用的日志选项,请查阅 logging drivers 文档。

network_mode

网络模型。

--network 的取值相同。但额外支持 service:[service name] 模式。

network_mode: "bridge"
network_mode: "host"
network_mode: "none"
network_mode: "service:[service name]"
network_mode: "container:[container name/id]"
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

NOTE: network_mode: "host" 不能和 links 混用。

networks

要加入的网络。目标网络是在 docker-compose.yml 顶级的 networks 项中定义的。

services:
  some-service:
    networks:
     - some-network
     - other-network

ALIASES

指定网络中该服务的别名(也即主机名)。相同网络中别的容器可以使用服务名或者服务别名来连接到该服务的容器实例。

既然 aliases 是网络范围(网络域)内的,同一个服务在不同网络中可以有不同的别名。

Note

一个网络域别名,可以被多个容器、甚至是多个服务所共享——你可以在不同容器、不同服务之间使用重名的别名。如果你这么做了,别名会被解决为哪个确定的容器是无法保证的。

别名定义的格式如同这样:

services:
  some-service:
    networks:
      some-network:
        aliases:
         - alias1
         - alias3
      other-network:
        aliases:
         - alias2

一个更复杂而完整的例子:下面提供了三个服务:`web, worker, 和 db,分别属于两个网络:newlegacy。通过 new 网络中的主机名 dbdatabase 来访问,或者 legacy 网络的主机名 dbmysql,都可以访问到 db 服务。

version: "3.8"

services:
  web:
    image: "nginx:alpine"
    networks:
      - new

  worker:
    image: "my-worker-image:latest"
    networks:
      - legacy

  db:
    image: mysql
    networks:
      new:
        aliases:
          - database
      legacy:
        aliases:
          - mysql

networks:
  new:
  legacy:

IPV4_ADDRESS, IPV6_ADDRESS

指定一个静态IP地址。

注意相应的顶级网络配置中,必须有 ipam 块对子网进行了配置且静态IP地址符合该子网定义。

If IPv6 addressing is desired, the enable_ipv6 option must be set, and you must use a version 2.x Compose file. IPv6 options do not currently work in swarm mode.

一个例子是:

version: "3.8"

services:
  app:
    image: nginx:alpine
    networks:
      app_net:
        ipv4_address: 172.16.238.10
        ipv6_address: 2001:3984:3989::10

networks:
  app_net:
    ipam:
      driver: default
      config:
        - subnet: "172.16.238.0/24"
        - subnet: "2001:3984:3989::/64"

pid

pid: "host"

设置服务使用主机的 PID 模式。这使得容器内的服务进程和宿主机操作系统级能够共享 PID 地址空间。这是一个典型的 Linux/Unix 操作系统概念,因此这里不再展开叙述了。这样的共享的作用,可以使能安全的、借助PID地址空间的 IPC 通讯。

ports

暴露端口到宿主机。

Note: 端口暴露功能和 network_mode: host 不能兼容。

短格式

可以同时指定宿主机和容器端口 (HOST:CONTAINER) 以完成映射,也可以仅指定容器端口以自动映射为相同的主机端口一个临时端口(从32768开始)。

Note

当采用 HOST:CONTAINER 格式来映射端口时,如果使用的容器端口小于60的话,你可能会接受到一个错误。这是因为 YAML 解析器将格式 xx:yy 视为一个 60 进制的数值。这挺荒谬的,是不是?由于这个原因,我们不得不推荐你在端口号这里总是使用引号做包围,令其成为一个 string 值,以免收到不如预期的反应。

ports:
 - "3000"
 - "3000-3005"
 - "8000:8000"
 - "9090-9091:8080-8081"
 - "49100:22"
 - "127.0.0.1:8001:8001"
 - "127.0.0.1:5000-5010:5000-5010"
 - "6060:6060/udp"

长格式

允许进行冗长的定义:

  • target: 指定容器内的端口号
  • published: 指定暴露给 docker 宿主机的端口号
  • protocol: 协议 (tcp or udp)
  • mode: host 表示每个节点的端口号都发布为宿主机端口,ingress 专用于 swarm 集群,所有节点的端口会被负载均衡为宿主机端口。
ports:
  - target: 80
    published: 8080
    protocol: tcp
    mode: host
意义明显,所以略过解说。

NOTE:长格式仅在 v3.2 及之后有效。

restart

no 是默认的重启策略。此时无论容器怎么退出、怎么失败也不会被自动重启。

指定 always 时任何情况下容器都会被重启。

on-failure 策略可在容器失败退出时才重启。

restart: "no"
restart: always
restart: on-failure
restart: unless-stopped
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。(此时可以使用 restart_policy 达到目的)

也参阅 deploying a stack in swarm mode

secrets

从每个服务配置中,授权访问顶级 secrets 定义的表项。支持长短两个格式。

短格式

短格式仅指定敏感内容的名字。这使得容器能够挂载对应内容到 /run/secrets/<secret_name> 位置并访问它。

下面的例子使用短格式,让 redis 能够访问 my_secretmy_other_secretmy_secret 的具体内容被定义在 ./my_secret.txtmy_other_secret 被定义为外部资源,例如通过 docker secret create 方式预先定义。如果找不到对应的外部资源,stack部署将会失败并抛出一个 secret not found 错误。

version: "3.8"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    secrets:
      - my_secret
      - my_other_secret
secrets:
  my_secret:
    file: ./my_secret.txt
  my_other_secret:
    external: true

长格式

长格式可以更精细地定义敏感内容如何被用于 stack 内容器。

  • source: 敏感内容在 Docker 中所被定义的名字。
  • target: 被挂载到容器内 /run/secrets/ 中的文件名。如果没指定则使用 source 名。
  • uid & gid: 容器内挂载的文件的 UID 和 GID。如未指定则为0。在 Windows 中无效。
  • mode: 容器内挂载的文件的八进制许可权限。在 Docker 1.13.1 中默认值为 0000,但在更新的版本中为 0444。挂载的文件是不可写的。执行位可以被设置,但一般情况下没有意义。

下面是一个例子:

version: "3.8"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    secrets:
      - source: my_secret
        target: redis_secret
        uid: '103'
        gid: '103'
        mode: 0440
secrets:
  my_secret:
    file: ./my_secret.txt
  my_other_secret:
    external: true

长短格式时可以被混用的,如果你在定义多个敏感内容。

security_opt

为每个容器覆盖掉默认的标签语义。

security_opt:
  - label:user:USER
  - label:role:ROLE

通常这和 seccomp 有关,这会是与安全配置有关的一个冗长的话题,故而此处不做展开。

NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

stop_grace_period

指定一个等待时长,如果容器未能拦住 SIGTERM 信号(或者通过 stop_signal 定义的别的信号)正常地关闭自己,则在此时长之后强制清除容器实例的相应进程(通过 SIGKILL 信号)。

stop_grace_period: 1s
stop_grace_period: 1m30s

默认时,将会等候 10s 。

stop_signal

设置一个替代信号以正常关闭容器实例。默认时使用 SIGTERM 信号.

stop_signal: SIGUSR1

sysctls

为容器设定内核参数。可以使用一个数组或字典。

sysctls:
  net.core.somaxconn: 1024
  net.ipv4.tcp_syncookies: 0
sysctls:
  - net.core.somaxconn=1024
  - net.ipv4.tcp_syncookies=0
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

tmpfs

v2 的解说在 v3.8 的原文里已经被删除了:

since v2

挂载一个临时文件系统到容器中。可以是一个单一值或一个列表。

tmpfs: /run
tmpfs:
  - /run
  - /tmp
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

since v3.6

挂载一个临时文件系统到容器中。可以使用一个单一值或一个数组。

tmpfs: /run

tmpfs:
  - /run
  - /tmp
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

挂载一个临时文件系统到容器中。Size参数可以指定文件系统尺寸的字节大小。默认值为无限。

 - type: tmpfs
     target: /app
     tmpfs:
       size: 1000

ulimits

覆盖容器内指定的默认的 ulimits 值。可以指定一个整数作为单一的 limit 限制,或者指定一个 mapping 以分别表示 soft/hard limit 限制。

ulimits:
  nproc: 65535
  nofile:
    soft: 20000
    hard: 40000

userns_mode

userns_mode: "host"

禁用用户名字空间。如果 Docker daemon 被配置运行在一个 user namespace 之中的话。

NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

volumes

挂载宿主机路径或者命名卷。

可以挂载一个主机路径到一个服务中,此时无需在顶级 volumes 中对其进行定义。

如果想要重用一个卷到多个服务,那么应该在顶级 volumes 中定义它并命名它。

可以在 services, swarms, and stack files 中使用命名卷。

NOTE: 在顶级 volumes 中定义一个命名卷,并在一个服务的 volumes 列表中引用它。

早期的 volumes_from 不再使用了。

可以参阅 Use volumes and Volume Plugins

下面的例子示意了一个命名卷 my_data ,且被用于 web 服务。在 web 中也使用一个主机文件夹 ./static 到容器内的挂载;在 db 中挂载了一个主机文件到容器内的对应文件,并使用了另一个命名卷 dbdata

version: "3.8"
services:
  web:
    image: nginx:alpine
    volumes:
      - type: volume
        source: mydata
        target: /data
        volume:
          nocopy: true
      - type: bind
        source: ./static
        target: /opt/app/static

  db:
    image: postgres:latest
    volumes:
      - "/var/run/postgres/postgres.sock:/var/run/postgres/postgres.sock"
      - "dbdata:/var/lib/postgresql/data"

volumes:
  mydata:
  dbdata:
Note

关于卷的更多信息,请参阅 use volumesvolume plugins 章节。

短格式

可以使用 HOST:CONTAINER 格式,或者附带一个访问模式 HOST:CONTAINER:ro

短格式的语法为 [SOURCE:]TARGET[:MODE]SOURCE 可以是一个主机路径,也可以是一个卷名。TARGET 是一个容器内路径 T,主机路径将被挂载到该路径 T。MODE 可以是 ro 或者 rw,分别代表着 只读 和 可读写。

可以挂载一个主机中的相对路径,此路径是相对于 docker compose 文件而被展开的。相对路径应该总是以 . 或者 .. 开始。

volumes:
  # Just specify a path and let the Engine create a volume
  - /var/lib/mysql

  # Specify an absolute path mapping
  - /opt/data:/var/lib/mysql

  # Path on the host, relative to the Compose file
  - ./cache:/tmp/cache

  # User-relative path
  - ~/configs:/etc/configs/:ro

  # Named volume
  - datavolume:/var/lib/mysql

长格式

长格式可以进行更精细的控制。

  • type: 挂载类型 为 volume, bind, tmpfsnpipe
  • source:挂载的源位置。可以是一个主机路径,一个定义于顶级 volumes 中的卷名称,等等。如果是挂载 tmpfs 则此参数无意义。
  • target: 容器内的挂载点路径。
  • read_only: 布尔值以设定卷的可写性。
  • bind: 配置附加的 bind 选项。

    • propagation: 用于 bind 的传播模式。
  • volume: 配置附加的 卷 选项

    • nocopy:布尔量以禁用数据复制(默认时当卷被首次创建时,容器内的内容将被复制到卷内)
  • tmpfs: 配置附加的 tmpfs 选项

    • size: tmpfs的容量,按字节数。
  • consistency:挂载的一致性要求:consistent 主机和容器有同样的视图,cached 读操作被缓冲,主机视图为主体,delegated 读写操作被缓冲,容器视图为主体。
version: "3.8"
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - type: volume
        source: mydata
        target: /data
        volume:
          nocopy: true
      - type: bind
        source: ./static
        target: /opt/app/static

networks:
  webnet:

volumes:
  mydata:
长格式在 v3.2 之后可用

Note

在做绑定主机目录并挂载到容器操作时,长格式语法需要你提前将主机文件夹准备就绪。使用短格式的话,对应文件夹将被就地创建,如果它尚未存在的话。详情请参阅 bind mounts documentation

VOLUMES FOR SERVICES, SWARMS, AND STACK FILES

当工作在 services, swarms, 或 docker-stack.yml 场景下,要注意一个服务在 swarm 中可能被部署到任意节点,而每当服务被更新之后再次启动时,可能已经不再在原来的节点上了。

当指定名称的卷并不存在时,Docker会自动创建一个匿名卷用于引用的服务。匿名卷是不可持久化的,因此当关联的容器实例退出并被移除时,匿名卷也会被销毁。

如果想要持久化你的数据,采用命名卷以及选择恰当的卷驱动程序,这个驱动应该是跨主机的,这样数据才能在不同的主机之间漫游。否则的话,你应该设置服务的约束条件以便该服务只会被部署到特定的节点上,这些节点上有相应的卷服务在正确工作。

作为一个例子,votingapp sample in Docker Labsdocker-stack.yml 文件定义了 db 服务,运行着 postgresql。它使用了一个命名卷 db-data 来持久化数据库数据,这个卷被通过swarm约束在只能运行在 manager 这个节点上,因此一切疑难都不存在了。下面是源码:

version: "3.8"
services:
  db:
    image: postgres:9.4
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend
    deploy:
      placement:
        constraints: [node.role == manager]

CACHING OPTIONS FOR VOLUME MOUNTS (DOCKER DESKTOP FOR MAC)

在 Docker 17.04 CE Edge 以及其后版本中(乃至于 17.06CE Edge 和 Stable 版本),你可以配置容器和主机之间卷如何被同步的一致性约束参数。这些标志包括:

  • consistent 完全一致。主机和容器有同样的视图,这是默认的策略。
  • cached 宿主机为准。对卷的读操作被缓冲,主机视图为主体,
  • delegated 容器为准。对卷的读写操作被缓冲,容器视图为主体。

这是专为 Docker Desktop for Mac 而适配的。由于已经知道的 osxfx 的关于文件共享特性原因,合理的设置一致性标志能够改善容器内外访问挂载卷时的性能问题。

下面是一个cached卷的例子:

version: "3.7"
services:
  php:
    image: php:7.1-fpm
    ports:
      - "9000"
    volumes:
      - .:/var/www/project:cached

对于读写操作均被缓冲的情况,即使容器中发生了什么修改(对于向PHP Website这样的典型架构来说,./config.php 经常是可能被写入的),也不会立即体现到宿主机中来,容器中的写入将会被堆积。

卷在容器内外的一致性问题,应该参考 Performance tuning for volume mounts (shared filesystems)

在这里我未能原样翻译,因为那样会带来较长的篇幅,我尚未能就此问题组织好语言。

domainname, hostname, ipc, mac_address, privileged, read_only, shm_size, stdin_open, tty, user, working_dir

这些配置具有单一值。和 docker run 的相应命令行参数相对应。mac_address 已经是被遗弃的设定。

user: postgresql
working_dir: /code

domainname: foo.com
hostname: foo
ipc: host
mac_address: 02:42:ac:11:65:43

privileged: true


read_only: true
shm_size: 64M
stdin_open: true
tty: true

指定时间段 duration

有的配置选项,例如 interval 或者 timeout(都是 check 的子选项),接受一个字符串风格的时间周期或时间段的参数值。它们应该具有这样的格式:

2.5s
10s
1m30s
2h32m
5h34m56s

可以为数值附加的后缀单位有 us, ms, s, m, 以及 h

含义自明。

指定字节值

有的配置选项,例如 build 的子选项 shm_size,接受一个字符串分隔的容量尺寸参数值。它们应该具有这样的格式:

2b
1024kb
2048k
300m
1gb

有效的后缀单位包括 b, k, mg。此外,kb, mbgb 也是合法的。纯粹的十进制数值并不合法。

卷编排格式手册 - volumes

顶级的 volumes 章节可以声明和创建命名卷(无需使用 volume_from),这些卷能够被用于在 service 章节下的 volumes 小节中被引用。所以我们可以重用它们,甚至能够跨越多个 services。docker命令的 docker volume 子命令有更多的参考信息。

关于卷的使用,也可以参考 Use volumesVolume Plugins

这里有一个示例,包含了两个服务,数据库的数据存储文件夹在两个服务之间被共享,因而数据库可以使用这个存储文件夹,而备份服务同样可以操作它以完成备份任务:

version: "3.8"

services:
  db:
    image: db
    volumes:
      - data-volume:/var/lib/db
  backup:
    image: backup-service
    volumes:
      - data-volume:/var/lib/backup/data

volumes:
  data-volume:

顶级 volumes 章节下的条目可以是空,无需指定细节,这样的话,默认的卷驱动程序将被应用(通常都会是 local 卷驱动)。

但你也可以通过下面的参数对其进行定制:

driver

指定哪一个卷驱动程序会被采用。一般来说,默认值会是 local。如果卷驱动程序无效、或不能工作,在 docker-compose up 时 Docker Engine将会返回一个错误。

driver: foobar

driver_opts

可选地指定一组键值对参数集,这些参数将被传递给卷驱动程序。所以这些参数集是和卷驱动程序相关的,请参考卷驱动程序的有关文档。

volumes:
  example:
    driver_opts:
      type: "nfs"
      o: "addr=10.40.0.199,nolock,soft,rw"
      device: ":/docker/example"

external

如果设置为 true,表示相应的卷是在 compose 编排文件之外被创建就绪的。此时 docker-compse up 将不会尝试创建这个卷,而如果该卷尚未存在则会返回一个错误。

对于 v3.3 以及更低的 compose 编排格式版本而言,external 不可以被用于与其他卷配置参数组合使用,例如 driver, driver_opts, labels 等等。但对于 v3.4 以及更高版本来说不再有此限制。

下面的示例中,Compose 查找一个名为 data 的外部卷并挂载它到 db 服务中,而不是尝试创建一个名为 [projectname]_data 的新卷。

version: "3.8"

services:
  db:
    image: postgres
    volumes:
      - data:/var/lib/postgresql/data

volumes:
  data:
    external: true
external.name 在 v3.4+ 已被废弃,这之后直接使用 name

你也可以单独指定卷名字(这时,data 被认为是该卷在当前编排文件中被引用时的 卷别名):

volumes:
  data:
    external:
      name: actual-name-of-volume
External volumes are always created with docker stack deploy

在使用 docker stack deploy 部署到 swarm 中时,外部卷如果不存在,则总是自动被创建。进一步的有关信息请参阅 moby/moby#29976

labels

使用 Docker labels 为容器添加元数据。可以是数组格式或者字典格式。

我们建议你使用反向DNS标注方式,为你的元数据表键添加反向域名前缀,从而避免潜在的和其它应用的相同名字的表键发生冲突:

labels:
  com.example.description: "Database volume"
  com.example.department: "IT/Ops"
  com.example.label-with-empty-value: ""
labels:
  - "com.example.description=Database volume"
  - "com.example.department=IT/Ops"
  - "com.example.label-with-empty-value"

name

since v3.4+

为卷指定一个自定义的名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。

version: "3.8"
volumes:
  data:
    name: my-app-data

name 可以被与 external 相组合:

version: "3.8"
volumes:
  data:
    external: true
    name: my-app-data

网络编排格式手册 - networks

顶级章节 networks 使得你可以配置想要创建和使用的网络(Compose内网)。

driver

指定该网络的驱动程序。

缺省的驱动程序由 Docker Engine 的启动参数所指定。通常情况下,启动参数内置为在单节点宿主机上使用 bridge 驱动,而在 swarm mode 中使用 overlay 驱动。

如果驱动程序不可用,Docker Engine 将会返回一个错误。

driver: overlay

bridge

缺省时 Docker 在每个宿主机节点上使用 bridge 驱动。有关桥接网络是如何工作的,可以参考 Docker Labs 的和网络相关的辅导用例:Bridge networking

overlay

overlay 驱动在多个 swarm mode 节点之间建立一个命名子网,这是一个跨主机的虚拟网络。

hostnone

使用主机网络栈,或者不使用网络。

和命令行参数 --net=host 以及 --net=none 是等价的。

这两种驱动及网络模型只能被用于 docker stack 之中。如果你正在使用 docker compose 相关指令,请使用 network_mode 来指定它们。

If you want to use a particular network on a common build, use [network] as mentioned in the second yaml file example.

使用内建的网络模型,例如 hostnone,语法上有一点点需要注意的地方:如果用 hostnone 这样的名字定义一个外部网络(注意你并不需要真的创建他们,这两者都属于Docker内置的网络模型),那么在 Compose 编排文件中引用它们时你需要使用 hostnetnonet,如同这样:

version: "3.8"
services:
  web:
    networks:
      hostnet: {}

networks:
  hostnet:
    external: true
    name: host

---
services:
  web:
    ...
    build:
      ...
      network: host
      context: .
      ...
services:
  web:
    ...
    networks:
      nonet: {}

networks:
  nonet:
    external: true
    name: none

driver_opts

指定一组键值对表示的选项集,以传递给网络驱动程序。它们是和驱动程序密切相关的,所以具体的可用参数应该参考对应的驱动程序文档。

driver_opts:
  foo: "bar"
  baz: 1

attachable

since v3.2+

只能用于 driver: overlay 的场景。

如果被设置为 true,独立运行的容器也能被附着在该网络之中。如果独立运行的容器实例被附着到了一个 overlay 网络中,那么容器中的服务与单独的容器实例之间能够互相通讯。请注意你甚至可以附着其他 Docker daemon 中的容器实例到本 overlay 网络中。

networks:
  mynet1:
    driver: overlay
    attachable: true

enable_ipv6

在该网络/子网中启用 IPv6。

在 v3+ 中不被支持。

enable_ipv6 需要你使用 v2 的编排格式,而且也不能被用于 swarm mode 中。

ipam

自定义 IPAM 配置。每一项子配置都是可选参数。

  • driver: 自定义 IPAM 驱动程序,而不使用默认值
  • config: 一个列表,包含一到多个配置块。每个配置块具有下列子参数:

    • subnet: CIDR格式的子网定义,以划定一个网段。

一个完整的例子:

ipam:
  driver: default
  config:
    - subnet: 172.28.0.0/16
NOTE:附加IPAM如 gateway 只在 v2 中可用。

internal

默认时,Docker也会连接到一个桥接网络以提供外部可连接性。如果你想建立一个外部的隔离的 overlay 网络,请设置本选项为 true

labels

使用 Docker labels 为容器添加元数据。可以是数组格式或者字典格式。

我们建议你使用反向DNS标注方式,为你的元数据表键添加反向域名前缀,从而避免潜在的和其它应用的相同名字的表键发生冲突:

labels:
  com.example.description: "Financial transaction network"
  com.example.department: "Finance"
  com.example.label-with-empty-value: ""
labels:
  - "com.example.description=Financial transaction network"
  - "com.example.department=Finance"
  - "com.example.label-with-empty-value"

external

如果设置为 true,那么本网络是在 Compose 编排文件之外被创建和管理的。此时 dockercompose up 不会试图创建它,如果网络并不存在则返回一个错误。

对于 v3.3 以及更低版本的格式,external 不可与 driver, driver_opts, ipam, internal 等连用。此限制在 v3.4+ 之后被取消。

下面的例子里,proxy 是一个外部世界中的网关,Compose将会寻找通过 docker network create outside 所建立的 outside 外部网络,而不是试图去自动建立一个名为 [projectname]_outside 的新网络:

version: "3.8"

services:
  proxy:
    build: ./proxy
    networks:
      - outside
      - default
  app:
    build: ./app
    networks:
      - default

networks:
  outside:
    external: true
external.name 在 v3.5 及之后已经被废弃,请改用 name

你也可以单独指定一个网络名用于在Compose编排文件内被引用。

name

since v3.5

为网络设置一个自定义名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。

version: "3.8"
networks:
  network1:
    name: my-app-net

name 可以与 external 一起连用:

version: "3.8"
networks:
  network1:
    external: true
    name: my-app-net

配置项编排格式手册 - configs

顶级的 configs 章节声明,定义了一个配置项或者其参考,该配置项可以被授权给栈内服务使用。配置项的来源可以是 fileexternal

  • file: 配置项的内容在一个宿主机文件中。
  • external: 如果设置为 true,表示该配置项已经创建就绪了。Docker将不会试图建立它,而是在起不存在时生成一个 config not found 错误。
  • name: 该配置项在 Docker 中的名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。
  • driver and driver_opts: The name of a custom secret driver, and driver-specific options passed as key/value pairs. Introduced in version 3.8 file format, and only supported when using docker stack.
  • template_driver: The name of the templating driver to use, which controls whether and how to evaluate the secret payload as a template. If no driver is set, no templating is used. The only driver currently supported is golang, which uses a golang. Introduced in version 3.8 file format, and only supported when using docker stack. Refer to use a templated config for a examples of templated configs.

下面的例子中,当作为栈的一部分被部署时,my_first_config 会被自动创建并命名为 <stack_name>_my_first_config,至于 my_second_config 是已经存在的。

configs:
  my_first_config:
    file: ./config_data
  my_second_config:
    external: true

另一种变化是外部配置项带有 name 定义的情况,此时该配置项在 Compose 中可以被以 redis_config 为名进行参考和使用:

configs:
  my_first_config:
    file: ./config_data
  my_second_config:
    external:
      name: redis_config

你仍需在栈内为每个服务声明 configs 章节以获得访问配置项的权利,参考 grant access to the config

敏感信息项编排格式手册 - secrets

顶级的 secrets 章节声明,定义了一个敏感信息项或者其参考,该敏感信息项可以被授权给栈内服务使用。敏感信息项的来源可以是 fileexternal

  • file: 敏感信息项的内容在一个宿主机文件中。
  • external: 如果设置为 true,表示该敏感信息项已经创建就绪了。Docker将不会试图建立它,而是在起不存在时生成一个 secret not found 错误。
  • name: 该敏感信息项在 Docker 中的名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。
  • template_driver: The name of the templating driver to use, which controls whether and how to evaluate the secret payload as a template. If no driver is set, no templating is used. The only driver currently supported is golang, which uses a golang. Introduced in version 3.8 file format, and only supported when using docker stack.

下面的例子中,当作为栈的一部分被部署时,my_first_secret 会被自动创建并命名为 <stack_name>_my_first_secret,至于 my_second_secret 是已经存在的。

secrets:
  my_first_secret:
    file: ./secret_data
  my_second_secret:
    external: true

另一种变化是外部配置项带有 name 定义的情况,此时该配置项在 Compose 中可以被以 redis_secret 为名进行参考和使用。

Compose File v3.5 及更高版本

secrets:
  my_first_secret:
    file: ./secret_data
  my_second_secret:
    external: true
    name: redis_secret

Compose File v3.4 和更低版本

my_second_secret:
    external:
      name: redis_secret

你仍需在栈内为每个服务声明 secret 章节以获得访问敏感信息项的权利,参考 grant access to the secret

变量替换

Compose编排文件中可以使用环境变量。当 docker-compose 运行时,Compose 从 Shell 环境变量中抽取变量值。例如,假设操作系统的环境变量中包含 POSTGRES_VERSION=9.3 的定义,那么以下定义

db:
  image: "postgres:${POSTGRES_VERSION}"

等价于

db:
  image: "postgres:9.3"

如果环境变量并不存在或者为空串,那么它就被当做是空字符串。

你可以通过 .env 文件来为环境变量设定缺省值。Compose 将会自动查找当前文件夹中的 .env 文件以获得环境变量的值。

IMPORTANT: 注意 .env 文件只在 docker-compose up 场景中有效,而在 docker stack deploy 时它并不会被使用。

两种语法 $VARIABLE${VARIABLE} 都是可用的。此外在 v2.1 格式中,类似于 Shell 语法的如下形式也可以被使用:

  • ${VARIABLE:-default} 将会返回 default,如果环境变量 VARIABLE 为空字符串或未被设置的话。
  • ${VARIABLE-default} 将会返回 default,如果环境变量 VARIABLE 未被设置的话。

类似地,下面的语法有助于指定一个明确的值:

  • ${VARIABLE:?err} 将会产生错误信息 err,如果环境变量 VARIABLE 为空或未被设置的话。
  • ${VARIABLE?err} 将会产生错误信息 err,如果环境变量 VARIABLE 未被设置的话。

其他的 Shell 语法特性并未被支持,例如 ${VARIABLE/foo/bar}

如果需要一个美元符号的话,请使用 $$`。此时 `$$ 不再参与环境变量替换的解释。如下例:

web:
  build: .
  command: "$$VAR_NOT_INTERPOLATED_BY_COMPOSE"

如果你忘记了这个规则而使用了一个 $ 单个字符的话,Compose 会警告你:

The VAR_NOT_INTERPOLATED_BY_COMPOSE is not set. Substituting an empty string.

扩展字段

since v3.4

通过扩展字段,能够重用编排配置片段。它们可以是自由的格式,前提是你将它们定义在 yaml 文档的顶级,并且其章节名以 x- 开头:

version: '3.4'
x-custom:
  items:
    - a
    - b
  options:
    max-size: '12m'
  name: "custom"

NOTE

从 v3.7 开始(对于 3.x 系列),或者从 v2.4 开始(对于 2.x 系列),扩展字段也可以被放在 服务,卷,网络,配置项以及敏感信息项顶级章节之下的第一级。

如同这样:

version: '3.7'
services:
redis:
 # ...
x-custom:
 items:
      - a
      - b
    options:
      max-size: '12m'
    name: "custom"

所谓的自由格式,是指这些定义并不被 Compose 所解释。然而当你在某个地方插入它们的引用时,它们会被展开到插入点,然后再结合上下文被 Compose 解释具体的语义。这使用了 YAML anchors 语法。

例如,如果你的多个服务都会使用相同的日志记录选项:

logging:
  options:
    max-size: '12m'
    max-file: '5'
  driver: json-file

你可以这样定义:

x-logging:
  &default-logging
  options:
    max-size: '12m'
    max-file: '5'
  driver: json-file

services:
  web:
    image: myapp/web:latest
    logging: *default-logging
  db:
    image: mysql:latest
    logging: *default-logging

通过 YAML merge type 语法,你也可以在插入扩展字段定义是覆盖某些子选项。例如:

version: '3.4'
x-volumes:
  &default-volume
  driver: foobar-storage

services:
  web:
    image: myapp/web:latest
    volumes: ["vol1", "vol2", "vol3"]
volumes:
  vol1: *default-volume
  vol2:
    << : *default-volume
    name: volume02
  vol3:
    << : *default-volume
    driver: default
    name: volume-local

Compose 文档参考

结束

🔚

查看原文

赞 0 收藏 0 评论 0

hedzr 发布了文章 · 7月6日

Ubuntu Server 安装提要

Ubuntu 已经(早已,2020-04-23)发布了 20.04 LTS 版本,代号为 Focal Fossa 1

随这个版本而来的是 QEMU 4.2libvirt 6.0PHP 7.4Ruby 2.7GCC 9.3Python 3.8NGINX 1.17 等等更的软件包,一个新的服务器安装文本界面(非常好用,但自动化安装的支持还很弱)。

但这个版本也有其问题,官方推荐的最低系统是 双核4GBRAM。这在云上未免会带来价格上的更审慎的评估。

提要

下面是新版服务器安装之后应该有必要被解决的例行操作的提要,并不是大全,只是今天刚好想到这些而已,姑且先成一篇再说吧。

启用网卡

从 Ubuntu18.04 (Bionic)开始,他们使用了netplan 作为网络配置工具。老实说,这个方案我喜欢的很,比起以前的方法来讲,现在的网卡特性配置、桥接、虚拟都简单得多了,以一种我的思维模式很顺畅的方式来做事情。

更多详细信息,请看:

netplan 的配置文件为 YAML 格式,放在 /etc/netplan 之中。

为了启用第二块网卡,我们可以这样配置:

root@u18svr:~# cat /etc/netplan/01-netcfg.yaml
# This file describes the network interfaces available on your system
# For more information, see netplan(5).
network:
  version: 2
  renderer: networkd
  ethernets:
    enp0s3:
      dhcp4: yes
    enp0s8:
      dhcp4: yes
      dhcp6: yes

root@u18svr:~# netplan apply

如果你使用了系统安装时的网络定制(for Ubuntu 20.04+)的话,相应的文件名可能会是 /etc/netplan/00-installer-config.yaml。无论如何,任何在 /etc/netplan 中的 yaml 文件都是相关配置的一部分,你需要对该文件自行探索后找到正确的编辑位置。

enp0s8 是什么鬼?

这种编目方式,来自于 systemd 的所谓 Predictable Network Interface naming,大概从 Ubuntu 15.04 开始启用并代替了旧的 eth0 命名方式5

如何找到我的网卡的名字?

ip a 可以看,例如:

hz@u18svr:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:c6:9c:d9 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 75444sec preferred_lft 75444sec
    inet6 fe80::a00:27ff:fec6:9cd9/64 scope link
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:5f:d2:d7 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.18/24 brd 192.168.0.255 scope global dynamic enp0s8
       valid_lft 161846sec preferred_lft 161846sec
    inet6 fe80::a00:27ff:fe5f:d2d7/64 scope link
       valid_lft forever preferred_lft forever

启用免密 sudo

一般来说,出于管理目的,我们可能会为 Linux 账户增加用户组所属关系:

sudo usermod -aG staff,sambashare hz
# for Ubuntu 20.04, this following line might be better
sudo usermod -aG staff,sambashare,systemd-journal,systemd-timesync hz
#systemd-network
#systemd-resolve
# 如果你没有安装 samba 服务,考虑安装它(如有必要):sudo apt install samba

为了令指定用户能够无需密码即可sudo操作,我们可以建立一个 power 用户组:

root@u18svr:~# sudo groupadd -g 201 power

然后将想要授信的用户加入到这个组中:

root@u18svr:~# sudo usermod -aG power hz

接下来,我们将整个power组的sudoer特性进行提权:

echo "%power   ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/power
sudo chmod 440 /etc/sudoers.d/power

如果你只是想针对单一账户进行授信,可以使用 visudo 直接编辑 /etc/sudoers 文件,考虑在其中追加如下的行:

$hz    ALL=(ALL) NOPASSWD: ALL

sudo visudo 在存盘时也会验证你添加的行会不会语法错误。

错误的 /etc/sudoers 文件将会导致你无法sudo,这往往也会导致你无法再操控的系统。

控制台自动登录

为了安全的原因,服务器不应该启用某个账户自动登录控制台的功能。这可能意味着非法用户可以在机房借用键盘显示器直接操作该服务器而无需任何验证。

但对于你或你的组织所私有控制的云服务器组(私有云、私有高安全级的VPC网络等)来说,也许控制台自动登录功能有时候是你最后的机会,以便直接进入某失控服务器中查证来源等等。

总而言之,是否该使用控制台自动登录功能是由目的和环境决定的。

为了启用控制台自动登录功能,可以这样6

sudo systemctl edit getty@tty1.service

这条命令将会建立一个临时文件 (drop-in file)并在编辑器(例如 nano,vim)中打开它。你需要在编辑器中书写下面的内容,同时记得将 myusername 替换为你想要令其自动登录的 Linux 账户名:

[Service]
ExecStart=
ExecStart=-/sbin/agetty --noissue --autologin myusername %I $TERM
Type=idle

存盘退出,背后将会发生这些事情:

  • 一个文件夹 /etc/systemd/system/getty@tty1.service.d 将会被建立
  • 一个文件 /etc/systemd/system/getty@tty1.service.d/override.conf 将会被建立

这样一来,一个专属于 getty 账户的系统服务将会在开机时起到作用并完成自动登录控制台的功能。重启您的服务器即可验证到这一点。

重启、关闭服务器

sudo reboot
sudo poweroff -pf
sudo shutdown -hf now

如果你在使用终端远程SSH,poweroff命令就应该慎用,这倒不是因为远程服务器开机很麻烦,而是因为poweroff之后,你的终端tty将被挂起,不能做任何操作,除了关闭终端窗口之外。所以一般我们还是用shutdown来干这活,那将会让终端tty中的远程会话被打断,回到你的本地会话中来,你开可以继续连接别的服务器等等。

当然,关闭服务器要慎用——除非你有远程开机的控制面板。

增加小工具

在你的 .bashrc.profile 中增加小工具,有利于日常工作。

你也可以考虑采用其他的工具集合,或者基于 bash.sh7 自己开发。

ports

function ports () {
        if [ $# -eq 0 ]; then
                sudo lsof -Pni | grep -P "LISTEN|UDP"
        else
                local p='' i
                for i in "$@"; do
                        if [[ "$i" -gt 0 ]]; then
                                p="$p -i :$i"
                        else
                                p="$p -i $i"
                        fi
                done
                # DEBUG echo "lsof -Pn $p"
                sudo lsof -Pn $p
        fi
}

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
    PATH="$HOME/.local/bin:$PATH"
fi

也可以考虑建立 bin 目录:

mkdir -p $HOME/bin $HOME/.local/bin

并且授信 /usr/local/bin 目录:

sudo chown -R $USER /usr/local/bin
# 或者授予 power 分组
sudo chown -R $USER:power /usr/local/bin & sudo chmod -R g+w /usr/local/bin

服务器初装之后,哪些软件是例行要安装的?

这个问题很个性化,不过,由于多数工作不可避免,所以下面的案例给出了一种选择,你可以按需取用:

sudo apt install -y mc curl wget git build-essential cmake glances byobu jq htop lsof ccze net-tools dnsutils # for dig

sudo apt install ssh samba # 基础服务,确保系统初装后能够被远程配置

要不要配上 zsh?

服务器上要不要安装 zsh,oh-my-ssh?

我个人的看法是不要。

对于服务器运维来说,自己称手的工具集比较重要。干了很多事却没什么实质性帮助,只是界面更美观了,通常来说不是首选。此外,很多时候zsh的反应太慢了,这一点经历过网络卡顿的朋友才会有同感,紧急任务时每一毫秒都在念叨着快、快、快,恐怕还是只有原配的 bash 来的更贴心。

当然,像 z/autojump 等工具是很好用的,幸好 bash 中一样有它们,即使没有我们立即造一个也费不了多少事,z 这样的工具实际上念头改变一切,实现上却极为简单,只是一个带计数器的目录路径的列表,awk和grep就足以还原其特点了。

SSH 应该修改端口号吗?

很多所谓的运维材料会告诉你,如果你的服务器在公网,建议修改默认的SSH端口,可以降低自动攻击的风险,从而增加系统安全性。

这话,其实纯粹是想当然耳。

在公网上,扫描一台设备的典型端口,是常规性操作,对于手里持有大量肉鸡的人来说,使用多代理方式扫描一个网段所有地址的所有端口,看似昂贵,实际上如果精通 TCP/IP 协议的话,非常便宜。对于有充分渗透能力的人来说,OpenSSH 软件上的漏洞被利用的可能性实在是太低了,低到扫出了 22 端口他们也不会以此为突破点——除非,你还在用 Ubuntu 8、9 之类,那么有几个著名的 OpenSSH 问题可以被利用。

被扫到 SSH 口后爆破?你更应该做的是配置 Linux 账户安全策略,拒绝用户采用弱口令的可能性,而且强制他们定期修改登录密码(比如每个月)。

所以说,修改了 SSH 端口号,彷佛降低了风险,其实毫无帮助,反而对运维人员自身的各种工作带来不便,因为你需要额外的语法才能重用大量的运维脚本。其所带来的好处仅仅是低端人员扫描不到你的服务器的 SSH 端口,老实说,在今天很少有人会为了渗透而选择 SSH 入口的,大量的 XSS、CSP、XXE 或者 CTF 它们不香吗,犯得着和 OpenSSH 去较劲吗?

所以,为了你的各种脚本、登录、转发、Ansible 等等的便利,还是从了默认 22 口的好。

小结

暂时写这么多8


  1. https://ubuntu.com/download/a...
  2. 2
  3. 3
  4. 4
  5. networking - Why is my network interface named enp0s25 instead of eth0? - Ask Ubuntu
  6. 30
  7. https://github.com/hedzr/bash...
  8. GH Page: https://hedzr.github.io/ubunt...
查看原文

赞 0 收藏 0 评论 0

hedzr 发布了文章 · 2019-10-15

各种各样的镜像加速

各种各样的镜像加速

mirrors-for-coder

这里做一个集中,尽管以前都是遇到时立即搜索,但是集中一下之后,看起来也很壮观的。

当然,欢迎完善它。

China Mirrors

GitHub Clone

通过HTTPS协议Clone仓库的话,可能会遇到速度很慢的情况。

根据经验,在慢的时候中断Clone捎带片刻重复命令的话,你可能会得到正常速度,这种偷鸡的策略适合于小小仓库。

对于大型仓库,改走SSH协议进行clone的话,走到正常速度的几率较大,但此时的速度相较于HTTPS而言通常会有所损耗。

但下面还有一种较为费事的方法,通过修改 hosts 文件来完成提速,无需科学也无需代理加速也无需镜像加速(GitHub是不太可能有镜像的)。具体来说请接下去阅读:

首先在 https://www.ipaddress.com/ 查询这三个域名的地址:

  1. github.com
  2. assets-cdn.github.com
  3. github.global.ssl.fastly.net

然后按照查询的结果填写到 /etc/hosts 中,windows用户请查找 %WINDIR%/system32/drivers/etc/hosts 文件。请注意修改 hosts 文件通常需要 sudo 权限 或者管理员权限。修改内容如同下面:

140.82.118.3    github.com
185.199.109.153 assets-cdn.github.com
185.199.111.153 assets-cdn.github.com
185.199.108.153 assets-cdn.github.com
185.199.110.153 assets-cdn.github.com
151.101.113.194 github.global.ssl.fastly.net

如果你有国外的服务器,也可以通过dig指令来查找:

$ dig github.com +short
140.82.118.3

Docker CE

Docker CE 的具体加速办法有很多种,然而各种版本的本质都是一样的,一般来说你需要找到 docker daemon 的配置文件 /etc/docker/daemon.json,然后修改它像这样:

{
  "insecure-registries" : [
    "registry.mirrors.aliyuncs.com"
  ],
  "debug" : true,
  "experimental" : false,
  "registry-mirrors" : [
    "https://docker.mirrors.ustc.edu.cn",
    "https://dockerhub.azk8s.cn",
    "https://reg-mirror.qiniu.com",
    "https://registry.docker-cn.com"
  ]
}

如果你在这个文件中自定义了其他项目,或者这个文件中已经存在其他定义,请注意保持。

参考:https://docs.docker.com/engin...

Ubuntu Apt Source

如果你使用桌面版本,则 Ubuntu 的软件源设置中,你可以选取最近的地区,例如中国大陆,从而加速软件包下载速度。

如果使用 Server 版本,则可以明确地使用清华镜像(或者自行使用其他镜像)

# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse

# 预发布软件源,不建议启用
# deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse

以上例子为 18.04 版本的替换内容。你可以直接访问清华开源站查找其他版本:

https://mirror.tuna.tsinghua....

其他方法

https://askubuntu.com/questio...

使用 apt-select

可以用pip安装它:

pip install apt-select

然后运行它并跟随提示走:

apt-select --country US -t 5 --choose
使用mirrors CDN

apt-get now supports a 'mirror' method that will automatically select a good mirror based on your location. Putting:

deb mirror://mirrors.ubuntu.com/mirrors.txt precise main restricted universe multiverse
deb mirror://mirrors.ubuntu.com/mirrors.txt precise-updates main restricted universe multiverse
deb mirror://mirrors.ubuntu.com/mirrors.txt precise-backports main restricted universe multiverse
deb mirror://mirrors.ubuntu.com/mirrors.txt precise-security main restricted universe multiverse

on the top in your /etc/apt/sources.list file should be all that is needed to make it automatically pick a mirror for you based on your geographical location.

你可以无脑地使用 sed 来搞定:

sudo sed -i 's%us.archive.ubuntu.com/ubuntu/%mirrors.ubuntu.com/mirrors.txt%' /etc/apt/sources.list

Alpine Apk

清华提供一种Apk源加速方式:https://mirror.tuna.tsinghua....

在终端输入以下命令以替换TUNA镜像源: sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

制作 Docker 镜像时,这是很有用的,节约生命真的是美德。

Arch Linux Pacman

清华提供一种 Arch Linux 软件仓库加速方式:https://mirror.tuna.tsinghua....

编辑 /etc/pacman.d/mirrorlist, 在文件的最顶端添加: Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch

更新软件包缓存: sudo pacman -Syy

Go Modules

使用 Golang v1.11 以上,为你的项目启用 Go Modules 功能,然后就可以使用 GOPROXY 环境变量来透明地使用镜像代理。

比较著名的大陆加速器为:

export GOPROXY=https://goproxy.cn
# Windows 应该使用 set GOPROXY=xxxx 语法

然后 go mod download 以及 go mod tidy 就足够快了。

如果你想搭建私服,可以遵循 Go Modules 的 API 规范自己实现一个代理服务器,也可以使用开源的 athens 项目自建一个服务器。

如果使用 Golang 1.13 以上版本的话,一下语法可用:

export GOPROXY=direct,https://goproxy.cn,https://goproxy.io,https://gocenter.i
o

Android SDK

国内有多家组织提供 Android SDK的镜像缓存,甚至个人也可以很容易地建立这样的缓存,如果你有国内访问速度很好的国外服务器的话。

但是,随着时间推移,现在这些镜像基本上都已失效了。

取而代之的是,目前,Android的官方源是可以直连的,且能达到正常速度,所以还是赶紧滴做点负责任的app出来吧,不要只是会矽肺或者偷偷上传神马的。

Gradle

Gradle的配置文件为~/.gradle/init.gradle

allprojects {
    repositories {
        maven {
            url 'https://maven.aliyun.com/repository/public/'
        }
    }
    buildscript {
        repositories {
            maven {
                url 'https://maven.aliyun.com/repository/public/'
            }
        }
    }
}

以下的镜像可以选用

Gem 和 CocoaPods

替换 Ruby 源

首先是 gem 和 ruby 的源应该被替换

移除现有的Ruby镜像
$ gem sources --remove https://rubygems.org
添加国内最新镜像
$ gem sources -a https://gems.ruby-china.com
查看当前镜像
$ gem sources -l

加速 Cocoapods

几种加速方法,可能需要自己实际测试那种效果最好。

gitee镜像
pod repo remove master   
pod repo add master https://gitee.com/mirrors/CocoaPods-Specs   
pod repo update   
清华镜像
pod repo remove master   
pod repo add master https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git   
pod repo update   

对于 Cocoapods 新的版本,需要使用如下的方法:

pod repo remove master
cd ~/.cocoapods/repos
git clone https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git master
你的 xcode 工程中如果有Podfile的话,请修改加入下面的行:
source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'

Homebrew

macOS 中都会安装 Homebrew,但 brew update 可能会很慢。加速的办法是替换现有的上游:

git -C "$(brew --repo)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git

brew update

复原

(感谢Snowonion Lee提供说明)

git -C "$(brew --repo)" remote set-url origin https://github.com/Homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://github.com/Homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://github.com/Homebrew/homebrew-cask.git

brew update

以上内容从清华开源上复制:Homebrew 镜像使用帮助

如果想阻止 brew 指令运行时总是尝试去自动更新,可以设置环境变量:

# forbit autoupdate on homebrew installing
export HOMEBREW_NO_AUTO_UPDATE=1

Flutter & Dart Pub

flutter 官网有专门的页面讲述加速问题:

https://flutter.dev/community...

Flutter 镜像安装帮助

Flutter 是一款跨平台的移动应用开发框架,由 Google 开源。用 Flutter 开发的应用可以直接编译成 ARM 代码运行在 Android 和 iOS 系统上。

可以使用清华镜像:https://mirror.tuna.tsinghua....

Flutter 安装时需要从 Google Storage 下载文件,如您的网络访问 Google 受阻,建议使用本镜像。使用方法为设置环境变量 FLUTTER_STORAGE_BASE_URL,并指向 TUNA 镜像站。

$ export FLUTTER_STORAGE_BASE_URL="https://mirrors.tuna.tsinghua.edu.cn/flutter"

若希望长期使用 TUNA 镜像:

$ echo 'export FLUTTER_STORAGE_BASE_URL="https://mirrors.tuna.tsinghua.edu.cn/flutter"' >> ~/.bashrc

此外 Flutter 开发中还需要用到 Dart 语言的包管理器 Pub,其镜像使用方法参见Pub 镜像安装帮助

Pub 镜像安装帮助

Pub 是 Dart 官方的包管理器。跨平台的前端应开发 框架 Flutter 也基于 Dart 并且可以使用大部分 Pub 中的 库。

如果希望通过 TUNA 的 pub 镜像安装软件,只需要设置 PUB_HOSTED_URL 这个环境变量指向 https://mirrors.tuna.tsinghua... 即可。

以 bash 为例,临时使用 TUNA 的镜像来安装依赖:

$ PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" pub get # pub
$ PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" flutter packages get # flutter

若希望长期使用 TUNA 镜像:

$ echo 'export PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"' >> ~/.bashrc

Rust Cargo 和 Rustup

Rust 使用 creates.io,国内也有相应的提速手段:

https://lug.ustc.edu.cn/wiki/...

首先你需要在 $HOME/.cargo/config 中添加如下内容

[registry]
index = "git://mirrors.ustc.edu.cn/crates.io-index"
# Or
# index = "http://mirrors.ustc.edu.cn/crates.io-index"

如果 cargo 版本为 0.13.0 或以上, 需要更改 $HOME/.cargo/config 为以下内容:

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

有兴趣自建的朋友,可以看看:

https://github.com/rust-lang/...

清华TUNA 也有 rustup 相应的镜像

# export CARGO_HOME=$HOME/.cargo
# export RUSTUP_HOME=$HOME/.rustup
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup

详见:https://mirror.tuna.tsinghua....

Maven

采用aliyun镜像

编辑 $HOME/.m2/settings.xml,找到 <mirrors> 小节,添加如下内容:

<mirror>
    <id>aliyun-public</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun public</name>
    <url>https://maven.aliyun.com/repository/public</url>
</mirror>

<mirror>
    <id>aliyun-central</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun central</name>
    <url>https://maven.aliyun.com/repository/central</url>
</mirror>

<mirror>
    <id>aliyun-spring</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun spring</name>
    <url>https://maven.aliyun.com/repository/spring</url>
</mirror>

<mirror>
    <id>aliyun-spring-plugin</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun spring-plugin</name>
    <url>https://maven.aliyun.com/repository/spring-plugin</url>
</mirror>

<mirror>
    <id>aliyun-apache-snapshots</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun apache-snapshots</name>
    <url>https://maven.aliyun.com/repository/apache-snapshots</url>
</mirror>

<mirror>
    <id>aliyun-google</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun google</name>
    <url>https://maven.aliyun.com/repository/google</url>
</mirror>

<mirror>
    <id>aliyun-gradle-plugin</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun gradle-plugin</name>
    <url>https://maven.aliyun.com/repository/gradle-plugin</url>
</mirror>

<mirror>
    <id>aliyun-jcenter</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun jcenter</name>
    <url>https://maven.aliyun.com/repository/jcenter</url>
</mirror>

<mirror>
    <id>aliyun-releases</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun releases</name>
    <url>https://maven.aliyun.com/repository/releases</url>
</mirror>

<mirror>
    <id>aliyun-snapshots</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun snapshots</name>
    <url>https://maven.aliyun.com/repository/snapshots</url>
</mirror>  

<mirror>
    <id>aliyun-grails-core</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun grails-core</name>
    <url>https://maven.aliyun.com/repository/grails-core</url>
</mirror>

<mirror>
    <id>aliyun-mapr-public</id>
    <mirrorOf>*</mirrorOf>
    <name>aliyun mapr-public</name>
    <url>https://maven.aliyun.com/repository/mapr-public</url>
</mirror>

也可以采用 profile 方式,这里就不再赘述了。

Python pip 和 composer

Pip

  • 清华:pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
  • 阿里:pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
  • 华为:pip config set global.index-url https://mirrors.huaweicloud.com/repository/pypi/simple
  • 豆瓣:pip config set global.index-url https://pypi.douban.com/simple
# 以下可以选用其一

# 清华:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 阿里:
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
# 华为:
pip config set global.index-url https://mirrors.huaweicloud.com/repository/pypi/simple
# 豆瓣:
pip config set global.index-url https://pypi.douban.com/simple

Composer

# 以下可以选用其一

# 阿里:
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
# 华为:
composer config -g repo.packagist composer https://mirrors.huaweicloud.com/repository/php/

# Laravel中文网
composer config -g repo.packagist composer https://packagist.laravel-china.org

Node 和 npm/Yarn

可以更换镜像:

  • 阿里:yarn config set registry https://registry.npm.taobao.org
  • 华为:yarn config set registry https://mirrors.huaweicloud.com/repository/npm/
  • Node-Sass:npm config set sass_binary_site https://mirrors.huaweicloud.com/node-sass/

Vagrant

没有简单的办法。一些周知的镜像,可以通过这些地方加速:

  • 对于 Ubuntu 之类,可以取清华镜像

    vagrant box add ubuntu/trusty64 https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box
    
    vagrant box add ubuntu/bionic64 https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/bionic/20191002/bionic-server-cloudimg-amd64-vagrant.box
  • 清华找不到几个周知镜像,所以基本上还是要在 http://www.vagrantbox.es/ 寻找和添加

    vagrant box add debian/8.1 https://github.com/kraksoft/vagrant-box-debian/releases/download/8.1.0/debian-8.1.0-amd64.box 
  • 总的来说,没有什么有效的镜像,只能想各种办法去手工下载box,然后再导入。

Conclusion

CC4

https://github.com/hedzr/mirr...

查看原文

赞 3 收藏 3 评论 0

hedzr 发布了文章 · 2019-09-10

docker-compose 编排指南 (v3.7)

缘起

关于 docker-compose 的安装,关于 docker 的基本介绍,不在本文的指导范围内。

这篇文章基本上是 docker-compose YAML 文件格式的严格的英译中。这么做,缘起于昨天想起扫描一下 docker-compose 编排中怎么使用 ${PWD} 的问题,结果中文没有一点帮助,还是官网最终解决了我的模糊之处。因此我觉得还是应该做一篇比较严谨的译文以及说明,来阐释 docker-compose 编排的各项细节。

以下,我们主要是介绍 docker-compose 编排文件格式版本3 的各项细节。

阅读本文,你应该有 docker-compose 的基本认识,至少有基本的早期(版本2)编排格式的了解。

关于授权

译文从属于原文 https://docs.docker.com/compo...

译文 https://github.com/hedzr/docker-compose-file-format 本身以 MIT 方式分发。

编排格式版本3

历史

版本3是自 docker-engine 1.13 推出以来 docker-compose 所支持的格式。这之前 docker 在 1.12 中推出了 swarm mode 来构建一个虚拟网络中的虚拟计算资源,同时也大幅度改进了 docker 的网络以及存储的支持。

对于 docker-compose 编排格式与 docker-engine 之间的关系,下面这张表(摘自官网)有清晰的对照。

Compose file formatDocker Engine release
3.718.06.0+
3.618.02.0+
3.517.12.0+
3.417.09.0+
3.317.06.0+
3.217.04.0+
3.11.13.1+
3.01.13.0+
2.417.12.0+
2.317.06.0+
2.21.13.0+
2.11.12.0+
2.01.10.0+
1.01.9.1.+

编排文件结构与示例

这是一个版本3+的典型文件结构样本:

version: "3.7"
services:

  redis:
    image: redis:alpine
    ports:
      - "6379"
    networks:
      - frontend
    deploy:
      replicas: 2
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

  db:
    image: postgres:9.4
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend
    deploy:
      placement:
        constraints: [node.role == manager]

  vote:
    image: dockersamples/examplevotingapp_vote:before
    ports:
      - "5000:80"
    networks:
      - frontend
    depends_on:
      - redis
    deploy:
      replicas: 2
      update_config:
        parallelism: 2
      restart_policy:
        condition: on-failure

  result:
    image: dockersamples/examplevotingapp_result:before
    ports:
      - "5001:80"
    networks:
      - backend
    depends_on:
      - db
    deploy:
      replicas: 1
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

  worker:
    image: dockersamples/examplevotingapp_worker
    networks:
      - frontend
      - backend
    deploy:
      mode: replicated
      replicas: 1
      labels: [APP=VOTING]
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
      placement:
        constraints: [node.role == manager]

  visualizer:
    image: dockersamples/visualizer:stable
    ports:
      - "8080:8080"
    stop_grace_period: 1m30s
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    deploy:
      placement:
        constraints: [node.role == manager]

networks:
  frontend:
  backend:

volumes:
  db-data:

在这个样本中,顶级结构由 versionservicesnetworksvolumes 等等标签构成。这与版本2并没有什么翻天覆地的区别。

services 章节中,你可以定义若干个服务,每个服务通常运转着一个容器,这些服务构成了一个整体的设施栈,又或是服务群。

一般来说我们会把一堆拉拉杂杂的东西,例如一堆微服务什么的,编排成一个服务栈,让他们整体对外服务,从而避免细节外露,也可以加强架构设计弹性和对整个服务栈进行伸缩(而不是面对大批微服务去逐个处理)。

编排格式手册 - service

接下来会是一个参考手册应有的章节结构,我们按照字母顺序列列举出了服务编排的指令,例如 portsvolumescmdentry 等等。

build

该选项被用于构建。

build 可以是一个指向构建上下文的路径字符串,例如:

version: "3.7"
services:
  webapp:
    build: ./dir

也可以是一个更详细的定义。这包括了 context 项所指定的路径,以及可选的 dockerfile 文件和构建参数 args

version: "3.7"
services:
  webapp:
    build:
      context: ./dir
      dockerfile: Dockerfile-alternate
      args:
        buildno: 1

如果你在指定了 build 的同时还指定了 image,那么构建的结果会被标记为相应的名字,这就好像 docker build -t container-name:tag dir 做的那样:

    build: "./dir"
    image: "company/webapp:v1.1.9"
对于 YAML 而言,避免歧义的安全做法是对字符串加上引号包围。

上面这个例子,会找到 ./dir 文件夹中的构建上下文(默认是寻找到 Dockerfile)并完成构建,最后将其标记为 company/webapp 的名字,以及 v1.1.9 的 Tag。

context

可以是一个包含 Dockerfile 的文件夹,也可以是一个指向 git repository 的 URL。

如果指定了一个相对路径,那么这个路径是相对于 docker-compose.yml 文件的。这个路径也会被传送给 Docker daemon 用于进行构建。

docker-compose 发动构建动作和标记构建结果(按照image名字),在那之后按照对应的名字使用它。

dockerfile

可以指定不同于默认名称 Dockerfile 的其它文件名用于构建。注意同时必须指定路径到 context

    build:
      context: .
      dockerfile: Dockerfile-alternate

args

指定构建参数。通常是指用于构建时的参数(参见 Dockerfile 中的 ARG)。

以下是一个简要的概述:

首先,在 Dockerfile 指定参数:

ARG buildno
ARG gitcommithash

RUN echo "Build number: $buildno"
RUN echo "Based on commit: $gitcommithash"

然后指定构建参数的实际值(传入Map或者数组都是可以的):

  build:
    context: .
    args:
      buildno: 1
      gitcommithash: cdc3b19

或:

build:
  context: .
  args:
    - buildno=1
    - gitcommithash=cdc3b19
NOTE: 在 Dockerfile中,如果在 FROM 之前指定 ARG,那么这个 ARG 对于其后的 FROM 闭包是无效的。

多个 FROM 分别切割出了多个构建的闭包。

想要 ARG 在每个 FROM 闭包中都有效,你需要在每个闭包中都指定它。

Understand how ARGS and FROM interact 中有更详细的相关讨论。

你可以略过指定构建参数。此时,该参数的实际值取决于构建时运行环境。

  args:
    - buildno
    - gitcommithash
NOTE: YAML的布尔量(true, false, yes, no, on, off)必须用引号包围,以便 docker-compose正确处理。

cache_from

since v3.2

指定一个映像列表,用于缓存的解决。

build:
  context: .
  cache_from:
    - alpine:latest
    - corp/web_app:3.14

labels

since v3.3

向构建的映像中添加元数据标签,可以是一个数组或一个字典。

我们推荐使用反向DNS标注性前缀以防止你的标签和使用者的标签相冲突:

build:
  context: .
  labels:
    com.example.description: "Accounting webapp"
    com.example.department: "Finance"
    com.example.label-with-empty-value: ""

# anothor example
build:
  context: .
  labels:
    - "com.example.description=Accounting webapp"
    - "com.example.department=Finance"
    - "com.example.label-with-empty-value"

shm_size

since v3.5

设置构建容器时的 /dev/shm 分区大小。整数格式按字节表示,但也可以使用字符串格式(byte value):

build:
  context: .
  shm_size: '2gb'

build:
  context: .
  shm_size: 10000000

target

since v3.4

构建定义与 Dockerfile 中的特定的步骤(Stage),参阅 multi-stage build docs

build:
  context: .
  target: prod

多遍构建被典型地用于CI/CD。

例如步骤0可以被命名为 builder,负责从源码编译到目标文件,而步骤1则从步骤0中抽出目标文件用于部署打包,并生成最终的容器镜像,随后步骤0的中间层则被抛弃,这些中间层不会出现在最终容器镜像中,从而有效地缩减了最终容器镜像的尺寸,而这个结果也是从语义上、从逻辑上被自洽的。

FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

cap_add, cap_drop

添加或者移除容器的 Linux 能力。完整的清单可以参阅 man 7 capabilities

cap_add:
  - ALL

cap_drop:
  - NET_ADMIN
  - SYS_ADMIN
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

Linux 能力机制很大程度上是一种安全机制。具体含义、用途和引申属于 Linux 操作系统范畴,不再赘述。

cgroup_parent

可选地为容器指定一个上级 cgroupcgroup 也是 Linux 容器化实现的最重要的基本概念之一。

cgroup_parent: m-executor-abcd
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

command

覆盖容器内默认的 command.

command: bundle exec thin -p 3000

command 也可以被指定为一个列表。实际上这也是更被推荐的方式,无歧义而且安全,而且和 [dockerfile 中的格式具有统一性:

command: ["bundle", "exec", "thin", "-p", "3000"]

configs

为每个服务提供具体化的访问 config 的权限。

一个 config 包含一系列的配置信息,这些信息可能被通过多种途径所创建。容器的部署引用这些配置时,可以更好地区格诸如生产环境参数这样的问题。另一方面,敏感信息也因此可以被单独隔离到一个安全的区域中,在一定程度上减少了泄露的可能性。

NOTE: 指定的配置必须已经存在,或者已经被定义在顶级 configs 中了(defined in the top-level configs configuration)。否则整个容器栈的部署将会失败。

支持两个不同的语法变体格式。更详尽的信息应参考 configs

短格式

只指定配置名。容器因此可以访问配置 /<config_name 和挂载它(挂载的源和目标均为该配置名)。

version: "3.7"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    configs:
      - my_config
      - my_other_config
configs:
  my_config:
    file: ./my_config.txt
  my_other_config:
    external: true

上面的例子使用短格式在 redis 容器的服务中定义了 my_configmy_other_config 的引用。这里的 my_config 指定为一个宿主机文件 ./my_config.txt,而 my_other_config 被指定为外部的(资源),这意味着相应的资源已经在 Docker 中被定义了,或许是通过 docker config create 建立的,又或者是被别的容器栈部署所建立的。

如果外部资源找不到,那么容器栈的部署将会失败,并且抛出一个 config not found 的错误。

Note: config 定义仅在 v3.3 及更高版本的 docker-compose 格式中被支持。

长格式

长格式提供更多的信息来表述一个 config 在哪儿,如何被找到,怎么被使用:

  • source: 配置名
  • target: 该配置将被挂载到容器中的路径。默认为 /<source>
  • uid & gid: 数字值的 Linux/Unix UIDGID,如果没有指定则为0。Windows中不支持。
  • mode: 8进制的文件权限。默认值为 0444

    配置是不可写的,因为它们被挂载于临时的文件系统中。因此如果你设置了写许可,这将被忽略。

    可执行位是可以被设置的。

下面的例子类似于短格式的例子:

version: "3.7"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    configs:
      - source: my_config
        target: /redis_config
        uid: '103'
        gid: '103'
        mode: 0440
configs:
  my_config:
    file: ./my_config.txt
  my_other_config:
    external: true

在这里,redis 容器的服务并未访问 my_other_config

你可以授权一个服务访问多个配置,你也可以混用长短格式。

(在顶级)定义一个配置(config)并未隐含着某个服务就能访问到它。

container_name

指定一个自定义的容器名,而不是由 docker-compose 自己生成一个默认的。

container_name: my-web-container

由于 Docker 容器名必须是唯一的,所以你无法伸缩一个自定义了容器名的服务。

NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

credential_spec

since v3.3

从 v3.8 开始支持被用于组管理服务账户 gMSA(group Managed Service Account)方式。

为受控服务账户配置凭据。这个选项只被用于 Windows 容器服务。credential_spce 只能使用格式 file://<filename> or registry://<value-name>

当使用 file: 时,被参考的文件必须被置于 Docker 数据文件夹(通常是 C:\ProgramData\Docker\)的 CredentialSpec 子目录之下。下面的例子将会从 C:\ProgramData\Docker\CredentialSpecs\my-credential-sp 载入凭据信息:

credential_spec:
  file: my-credential-spec.json

当使用 registry: 时,凭据信息将会被从 Docker daemon 主机的 Windows Registry 中读入。一个注册表表项必须位于:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs

之中。

下面的例子读入 my-credential-spec 注册表项值:

credential_spec:
  registry: my-credential-spec

gMSA 配置例子

当为一个服务配置 gMSA 凭据时,参考如下的例子:

version: "3.8"
services:
  myservice:
    image: myimage:latest
    credential_spec:
      config: my_credential_spec

configs:
  my_credentials_spec:
    file: ./my-credential-spec.json|

depends_on

表示服务之间的依赖关系。服务依赖引发如下的行为:

  • docker-compose up 按照依赖顺序依次启动服务。在下面的例子中,dbredis 先于 web 被启动。
  • docker-compose up SERVICE 自动包括了 SERVICE 的依赖项。在下面的例子中,docker-compose up web 将会自动启动 dbredis
  • docker-compose stop 按照依赖顺序依次停止服务。在下面的例子中,web 将会被先于 dbredis 被停止。

简单的示例如下:

version: "3.7"
services:
  web:
    build: .
    depends_on:
      - db
      - redis
  redis:
    image: redis
  db:
    image: postgres

使用 depends_on 时应该注意的几件事:

  • depends_on 并不意味着等待 dbredis 就绪后才启动 web,而是在它们被启动后就会接着启动 web。如果要想等到服务就绪可用,应该参阅 Controlling startup order
  • 版本 3 不再支持 condition 表述。
  • depends_on 选项在部署到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

deploy

Version 3 only.

指定和部署以及运行相关的配置。

只会对部署到一个使用 docker stack deployswarm 有影响。

docker-compose updocker-compose run 时被忽略。

version: "3.7"
services:
  redis:
    image: redis:alpine
    deploy:
      replicas: 6
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

几个子选项是可用的:

endpoint_mode

swarm.

Version 3.3 only.
  • endpoint_mode: vip - Docker 为服务请求一个虚拟IP(VIP)用于访问。

    Docker在客户端和服务的有效的工作节点之间对请求进行自动负载均衡路由。客户端无需知道服务有多少可用节点,也不必知道服务节点的IP地址和端口号。

    这是默认方式。

  • endpoint_mode: dnsrr - 使用 DNS round-robin (DNSRR) 算法进行服务发现。Docker会为服务设置一个DNS条目,因而在进行对应的 DNS 解析时通过服务名称会返回一个IP地址清单。客户端因此直接选择一个具体的端点进行访问。
version: "3.7"

services:
  wordpress:
    image: wordpress
    ports:
      - "8080:80"
    networks:
      - overlay
    deploy:
      mode: replicated
      replicas: 2
      endpoint_mode: vip

  mysql:
    image: mysql
    volumes:
       - db-data:/var/lib/mysql/data
    networks:
       - overlay
    deploy:
      mode: replicated
      replicas: 2
      endpoint_mode: dnsrr

volumes:
  db-data:

networks:
  overlay:

endpoint_mode 的选项也被同样地用作 swarm mode 命令行选项(参阅 docker service create)。至于 docker swarm命令的一个快捷清单,可以查阅 Swarm mode CLI commands

要学习更多关于 swarm mode 的网络模型已经服务发现机制 的知识,请查看 Configure service discovery

labels

为服务指定标签。这些标签只被作用于对应的服务,而不是被应用到服务的容器或者容器实例上。

version: "3.7"
services:
  web:
    image: web
    deploy:
      labels:
        com.example.description: "This label will appear on the web service"

要为容器设置标签的话,在 deploy 之外为服务指定 labels

version: "3.7"
services:
  web:
    image: web
    labels:
      com.example.description: "This label will appear on all containers for the web service"

mode

可以是 globalreplicatedglobal 表示严格地一个 swarm node 跑一个服务,replicated 表示可以跑多个容器实例。默认是 replicated

参阅 swarm 主题下的 Replicated and global services

version: "3.7"
services:
  worker:
    image: dockersamples/examplevotingapp_worker
    deploy:
      mode: global

placement

指定 constaints 和 preferences。

参阅docker服务建立的相关文档以了解更多的关于 constraints 和 [preferences 的相关信息,包括相应的语法,可用的类型等等的完整描述。

version: "3.7"
services:
  db:
    image: postgres
    deploy:
      placement:
        constraints:
          - node.role == manager
          - engine.labels.operatingsystem == ubuntu 14.04
        preferences:
          - spread: node.labels.zone

replicas

如果服务是 replicated 的,replicas 则为其指定一个数值,此数值指示了一个 swarm 节点上最多可以跑多少个容器实例。

version: "3.7"
services:
  worker:
    image: dockersamples/examplevotingapp_worker
    networks:
      - frontend
      - backend
    deploy:
      mode: replicated
      replicas: 6

resources

配置资源约束。

NOTE: 对于非 swarm mode 而言,这个表项替换 older resource constraint options(诸如 cpu_shares, cpu_quota, cpuset, mem_limit, memswap_limit, mem_swappiness 等在版本3之前版本中的表项)。

Upgrading version 2.x to 3.x 中有相应的描述。

这些资源约束表项都具有一个单一值,相当于 docker service create 中的等价物。

在如下的例子中,redis 服务被约束为不可使用超出50M的内存,单核50%的CPU使用率,同时也保留 20M 内存以及 25%的CPU使用率作为基准值。

version: "3.7"
services:
  redis:
    image: redis:alpine
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
        reservations:
          cpus: '0.25'
          memory: 20M

以下的主题描述 swarm 场景下的服务或容器资源约束的可用选项。

Out Of Memory Exceptions (OOME)

企图在你的服务和容器实例中使用超过系统拥有的内存,那么将得到 Out Of Memory Exception (OOME) 。此时,容器实例,或者 Docker daemon,可能会被内核的 OOM 管理器所清除。

要防止这样的情况发生,请确定你的应用程序合法有效地使用内存。对于这样的风险,查阅 Understand the risks of running out of memory 以获知进一步的评估须知。

restart_policy

指示当容器实例退出时,如何重启。替换 restart

  • condition: 可以为 none, on-failureany (默认为 any)
  • delay: 在尝试重启之前的等候时长(默认为0)。应该为其指定一个 duration
  • max_attempts: 试图尝试重启多少次后放弃重启的尝试。默认为不放弃。
  • window: 要确定一次重启是否成功,需要等候的时长。默认为无等待立即认定为已成功。应该为其指定一个 duration
version: "3.7"
services:
  redis:
    image: redis:alpine
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

rollback_config

Version 3.7 file format and up

在滚动更新失败的场景下服务应该如何回滚:

  • parallelism:同时回滚的容器的数量值。如果设置为0,所有容器将被同时回滚。
  • delay: 每个容器组被回滚前的等待时长(默认为0)
  • failure_action: 一个回滚失败时应当执行的动作。可以是 continuepause(默认为pause
  • monitor: 失败的回滚状态被更新到监视器的周期(ns|us|ms|s|m|h)默认为 0s
  • max_failure_ratio: 回滚时失败的可容忍的比例(默认为0)
  • order: 回滚的操作顺序。可以为 stop-firststart-first(默认为 stop-first

update_config

指示服务应该如何被更新。这对于配置为滚动更新时有用:

  • parallelism:同时更新的容器的数量值。如果设置为0,所有容器将被同时回滚。
  • delay: 每个容器组被更新前的等待时长(默认为0)
  • failure_action: 一个更新失败时应当执行的动作。可以是 continuepause(默认为pause
  • monitor: 失败的更新状态被更新到监视器的周期(ns|us|ms|s|m|h)默认为 0s
  • max_failure_ratio: 更新时失败的可容忍的比例(默认为0)
  • order: 更新的操作顺序。可以为 stop-firststart-first(默认为 stop-first
NOTEorder 只在 v3.4 及之后有效。
version: "3.7"
services:
  vote:
    image: dockersamples/examplevotingapp_vote:before
    depends_on:
      - redis
    deploy:
      replicas: 2
      update_config:
        parallelism: 2
        delay: 10s
        order: stop-first

NOT SUPPORTED FOR DOCKER STACK DEPLOY

下列的子选项(为 docker-compose updocker-compose run 所支持)是在 docker stack deploy 中不被支持的:

Tip: See the section on how to configure volumes for services, swarms, and docker-stack.yml files. Volumes are supported but to work with swarms and services, they must be configured as named volumes or associated with services that are constrained to nodes with access to the requisite volumes.

devices

要被映射的设备清单。其用法和 docker 命令的 --device 相同。

devices:
  - "/dev/ttyUSB0:/dev/ttyUSB0"
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

dns

自定义 DNS 服务器列表。可以指定单一值或一个清单。

dns: 8.8.8.8
dns:
  - 8.8.8.8
  - 9.9.9.9

dns_search

自定义DNS搜索域名。可以指定单一值或一个清单。

dns_search: example.com
dns_search:
  - dc1.example.com
  - dc2.example.com

entrypoint

覆盖 dockerfile 中定义的默认的 entrypoint 值。

entrypoint: /code/entrypoint.sh

入口点也可以是一个清单:

entrypoint:
    - php
    - -d
    - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so
    - -d
    - memory_limit=-1
    - vendor/bin/phpunit
NOTE: 设置一个 entrypoint 不但覆盖 Dockerfile 中的任何 ENTRYPOINT 默认值,还会清理 Dockerfile 中的任何 CMD 默认值。所以 Dockerfile 中的 CMD 将会被忽略。

env_file

从给定的文件中引入环境变量值。可以是单一值或一个清单。

env_file: .env
env_file:
  - ./common.env
  - ./apps/web.env
  - /opt/secrets.env

对于 docker-compose -f FILE 来说,env_file 的路径是相对于 FILE 所在文件夹的。

environment 中声明的环境变量将会覆盖掉这里所引入的值。

对应的文件中,每一行应该使用 VAR=VAL 格式定义一个环境变量。行首为 # 表示为注释行,和空白行一样被忽略。

# Set Rails/Rack environment
RACK_ENV=development
NOTE: 如果服务定义了 build 项,在构建过程中,由 env_file 所定义的环境变量并不可见。只能使用 build 的子选项 args 去定义构建时环境变量值。

VAL 的值被原样照用,且不能被修改。例如如果值由引号所包围,那么值的表示量中也包含引号。

环境变量文件的顺序也需要被注意。位置靠后的环境变量文件中所定义的变量值会覆盖掉早前定义的旧值。

environment

添加环境变量。可以使用一个数组或者一个字典。任何布尔量:true, false, yes, no 等等都必须用引号包围为字符串字面量。

只有key值的环境变量的value值依赖于 docker-compose 运行时的主机环境,这对于防止敏感信息泄露是有用的。

environment:
  RACK_ENV: development
  SHOW: 'true'
  SESSION_SECRET:
environment:
  - RACK_ENV=development
  - SHOW=true
  - SESSION_SECRET
NOTE: 如果服务定义了 build 项,在构建过程中,由 env_file 所定义的环境变量并不可见。只能使用 build 的子选项 args 去定义构建时环境变量值。

expose

暴露端口到链接的服务。这些端口并不会被发布到宿主机。只能指定内部端口被用于暴露。

expose:
 - "3000"
 - "8000"

external_links

将在 docker-compose.yml 之外启动的容器链接到给定服务上。

和遗留选项 links 有相似的语义。

external_links:
 - redis_1
 - project_db_1:mysql
 - project_db_1:postgresql
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

更推荐的做法是通过 networks 构造一个子网以进行容器之间的链接。

extra_hosts

添加主机名映射。这些映射关系会被添加到 /etc/hosts 中。此功能等价于命令行参数 --add-host

extra_hosts:
 - "somehost:162.242.195.82"
 - "otherhost:50.31.209.229"

healthcheck

since v2.1

用于确认一个服务是否是“健康”的。参阅 HEALTHCHECK Dockerfile instruction

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]
  interval: 1m30s
  timeout: 10s
  retries: 3
  start_period: 40s

interval, timeoutstart_period 应该被指定为 durations.

Note: start_period 只在 v3.4 及以后可用。

test 必须是一个单一字符串值或一个列表。如果是一个列表,那么第一项必须是 NONE, CMD, CMD-SHELL 之一。如果是一个字符串,隐含地表示一个 CMD-SHELL 前缀。

# Hit the local web app
test: ["CMD", "curl", "-f", "http://localhost"]

如上述示例,但隐含调用 /bin/sh,和以下的形式是等效的。

test: ["CMD-SHELL", "curl -f http://localhost || exit 1"]
test: curl -f https://localhost || exit 1

要禁用任何在映像内指定的缺省的健康检查向,可以使用 disable: true。这和指定 test: ["NONE"] 是等效的。

healthcheck:
  disable: true

image

指定映像的名称。

image: redis
image: ubuntu:14.04
image: tutum/influxdb
image: example-registry.com:4000/postgresql
image: a4bc65fd

如果映像在宿主机不存在,Compose 会尝试下拉它,除非你也指定了 build 项。

init

since v3.7

在容器中运行一个 init 进程并转发信号。设置为 true 为服务使能这个特性。

version: "3.7"
services:
  web:
    image: alpine:latest
    init: true
缺省的 init 进程使用二进制执行文件 Tini,在需要时它将被安装于 daemon主机的位置 /usr/libexec/docker-init 。你也可以配置 daemon 使用一个不同的二进制文件,通过 init-path,参阅 configuration option

isolation

指定一个容器的隔离层级/技术。在 Linux 中,仅支持 default 值。在 Windows 中,可以接受的值有:default, processhyperv

labels

为容器添加元数据标签,参考 Docker labels。可以为其指定一个数组或一个字典。

我们建议你采用反向DNS标注方式来定义你的标签,这可以有效地避免标签名称的冲突。

labels:
  com.example.description: "Accounting webapp"
  com.example.department: "Finance"
  com.example.label-with-empty-value: ""
labels:
  - "com.example.description=Accounting webapp"
  - "com.example.department=Finance"
  - "com.example.label-with-empty-value"

links

已经是一个遗留特征了。在不久的未来将被移除。

链接另一个服务到本容器。可以同时制定服务名称和链接别名(SERVICE:ALIAS),也可以略过链接别名。

web:
  links:
   - db
   - db:database
   - redis

已经被链入的服务将会是主机名(即链接别名 ALIAS)可访问的。

对于服务间通讯来说,链接并不是必须的。默认情况下,任何服务都可以通过服务名访问到其他服务。参阅 Links topic in Networking in Compose

链接也表示一个依赖关系,但这已经是 depends_on 的任务了,所以链接也并不必要了。

logging

为服务指定日志转发配置。

logging:
  driver: syslog
  options:
    syslog-address: "tcp://192.168.0.42:123"

driver 指定了驱动名称,这和 --log-driver 是等效的。缺省值为 json-file

driver: "json-file"
driver: "syslog"
driver: "none"

可用的转发驱动器可以参考 https://docs.docker.com/confi...

使用 option 指定驱动器的选项,如同 --log-opt 那样。例如为 syslog 这样指定:

driver: "syslog"
options:
  syslog-address: "tcp://192.168.0.42:123"

缺省的日志转发驱动为 json-file。对此可以指定日志切割尺寸以及最多保持的日志历史文件个数:

version: "3.7"
services:
  some-service:
    image: some-service
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"

network_mode

网络模型。

--network 的取值相同。但额外支持 service:[service name] 模式。

network_mode: "bridge"
network_mode: "host"
network_mode: "none"
network_mode: "service:[service name]"
network_mode: "container:[container name/id]"
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。

也参阅 deploying a stack in swarm mode

NOTE: network_mode: "host" 不能和 links 混用。

networks

要加入的网络。目标网络是在 docker-compose.yml 顶级的 networks 项中定义的。

services:
  some-service:
    networks:
     - some-network
     - other-network

ALIASES

指定网络中该服务的别名(也即主机名)。相同网络中别的容器可以使用服务名或者服务别名来连接到该服务的容器实例。

既然 aliases 是网络范围内的,同一个服务在不同网络中可以有不同的别名。

services:
  some-service:
    networks:
      some-network:
        aliases:
         - alias1
         - alias3
      other-network:
        aliases:
         - alias2

一个更复杂而完整的例子:

version: "3.7"

services:
  web:
    image: "nginx:alpine"
    networks:
      - new

  worker:
    image: "my-worker-image:latest"
    networks:
      - legacy

  db:
    image: mysql
    networks:
      new:
        aliases:
          - database
      legacy:
        aliases:
          - mysql

networks:
  new:
  legacy:

IPV4_ADDRESS, IPV6_ADDRESS

指定一个静态IP地址。

注意相应的顶级网络配置中,必须有 ipam 块对子网进行了配置且静态IP地址符合该子网定义。

If IPv6 addressing is desired, the enable_ipv6 option must be set, and you must use a version 2.x Compose file. IPv6 options do not currently work in swarm mode.

一个例子是:

version: "3.7"

services:
  app:
    image: nginx:alpine
    networks:
      app_net:
        ipv4_address: 172.16.238.10
        ipv6_address: 2001:3984:3989::10

networks:
  app_net:
    ipam:
      driver: default
      config:
        - subnet: "172.16.238.0/24"
        - subnet: "2001:3984:3989::/64"

pid

pid: "host"

设置服务使用主机的 PID 模式。这使得容器内的服务进程和宿主机操作系统级能够共享 PID 地址空间。这是一个典型的 Linux/Unix 操作系统概念,因此这里不再展开叙述了。这样的共享的作用,可以使能安全的、借助PID地址空间的 IPC 通讯。

ports

暴露端口到宿主机。

Note: 端口暴露功能和 network_mode: host 不能兼容。

短格式

可以同时指定宿主机和容器端口 (HOST:CONTAINER) 以完成映射,也可以仅指定容器端口以自动映射为相同的主机端口一个临时端口(从32768开始)。

ports:
 - "3000"
 - "3000-3005"
 - "8000:8000"
 - "9090-9091:8080-8081"
 - "49100:22"
 - "127.0.0.1:8001:8001"
 - "127.0.0.1:5000-5010:5000-5010"
 - "6060:6060/udp"

长格式

允许进行冗长的定义:

ports:
  - target: 80
    published: 8080
    protocol: tcp
    mode: host
意义明显,所以略过解说。

NOTE:长格式仅在 v3.2 及之后有效。

restart

no 是默认的重启策略。此时无论容器怎么退出、怎么失败也不会被自动重启。

指定 always 时任何情况下容器都会被重启。

on-failure 策略可在容器失败退出时才重启。

restart: "no"
restart: always
restart: on-failure
restart: unless-stopped
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。(此时可以使用 restart_policy 达到目的)

也参阅 deploying a stack in swarm mode

secrets

从每个服务配置中,授权访问顶级 secrets 定义的表项。支持长短两个格式。

短格式

短格式仅指定敏感内容的名字。这使得容器能够挂载对应内容到 /run/secrets/<secret_name> 位置并访问它。

下面的例子使用短格式,让 redis 能够访问 my_secretmy_other_secretmy_secret 的具体内容被定义在 ./my_secret.txtmy_other_secret 被定义为外部资源,例如通过 docker secret create 方式预先定义。如果找不到对应的外部资源,stack部署将会失败并抛出一个 secret not found 错误。

version: "3.7"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    secrets:
      - my_secret
      - my_other_secret
secrets:
  my_secret:
    file: ./my_secret.txt
  my_other_secret:
    external: true

长格式

长格式可以更精细地定义敏感内容如何被用于 stack 内容器。

  • source: 敏感内容在 Docker 中所被定义的名字。
  • target: 被挂载到容器内 /run/secrets/ 中的文件名。如果没指定则使用 source 名。
  • uid & gid: 容器内挂载的文件的 UID 和 GID。如未指定则为0。在 Windows 中无效。
  • mode: 容器内挂载的文件的八进制许可权限。在 Docker 1.13.1 中默认值为 0000,但在更新的版本中为 0444。挂载的文件是不可写的。执行位可以被设置,但一般情况下没有意义。

下面是一个例子:

version: "3.7"
services:
  redis:
    image: redis:latest
    deploy:
      replicas: 1
    secrets:
      - source: my_secret
        target: redis_secret
        uid: '103'
        gid: '103'
        mode: 0440
secrets:
  my_secret:
    file: ./my_secret.txt
  my_other_secret:
    external: true

长短格式时可以被混用的,如果你在定义多个敏感内容。

security_opt

为每个容器覆盖掉默认的标签语义。

security_opt:
  - label:user:USER
  - label:role:ROLE

通常这和 seccomp 有关,这会是与安全配置有关的一个冗长的话题,故而此处不做展开。

NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。(此时可以使用 restart_policy 达到目的)

也参阅 deploying a stack in swarm mode

stop_grace_period

指定一个等待时长,如果容器未能拦住 SIGTERM 信号(或者通过 stop_signal 定义的别的信号)正常地关闭自己,则在此时长之后强制清除容器实例的相应进程(通过 SIGKILL 信号)。

stop_grace_period: 1s
stop_grace_period: 1m30s

默认时,将会等候 10s 。

stop_signal

设置一个替代信号以正常关闭容器实例。默认时使用 SIGTERM 信号.

stop_signal: SIGUSR1

sysctls

为容器设定内核参数。可以使用一个数组或字典。

sysctls:
  net.core.somaxconn: 1024
  net.ipv4.tcp_syncookies: 0
sysctls:
  - net.core.somaxconn=1024
  - net.ipv4.tcp_syncookies=0
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。(此时可以使用 restart_policy 达到目的)

也参阅 deploying a stack in swarm mode

tmpfs

since v2

挂载一个临时文件系统到容器中。可以是一个单一值或一个列表。

tmpfs: /run
tmpfs:
  - /run
  - /tmp
NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。(此时可以使用 restart_policy 达到目的)

也参阅 deploying a stack in swarm mode

since v3.6

挂载一个临时文件系统到容器中。Size参数可以指定文件系统尺寸的字节大小。默认值为无限。

 - type: tmpfs
     target: /app
     tmpfs:
       size: 1000

ulimits

覆盖容器内指定的默认的 ulimits 值。可以指定一个整数作为单一的 limit 限制,或者指定一个 mapping 以分别表示 soft/hard limit 限制。

ulimits:
  nproc: 65535
  nofile:
    soft: 20000
    hard: 40000

userns_mode

userns_mode: "host"

禁用用户名字空间。如果 Docker daemon 被配置运行在一个 user namespace 之中的话。

NOTE: 这些选项在部署一个栈到 swarm mode 时被忽略。(此时可以使用 restart_policy 达到目的)

也参阅 deploying a stack in swarm mode

volumes

挂载宿主机路径或者命名卷。

可以挂载一个主机路径到一个服务中,此时无需在顶级 volumes 中对其进行定义。

如果想要重用一个卷到多个服务,那么应该在顶级 volumes 中定义它并命名它。

可以在 services, swarms, and stack files 中使用命名卷。

NOTE: 在顶级 volumes 中定义一个命名卷,并在一个服务的 volumes 列表中引用它。

早期的 volumes_from 不再使用了。

可以参阅 Use volumes and Volume Plugins

下面的例子示意了一个命名卷 my_data ,且被用于 web 服务。在 web 中也使用一个主机文件夹 ./static 到容器内的挂载;在 db 中挂载了一个主机文件到容器内的对应文件,并使用了另一个命名卷 dbdata

version: "3.7"
services:
  web:
    image: nginx:alpine
    volumes:
      - type: volume
        source: mydata
        target: /data
        volume:
          nocopy: true
      - type: bind
        source: ./static
        target: /opt/app/static

  db:
    image: postgres:latest
    volumes:
      - "/var/run/postgres/postgres.sock:/var/run/postgres/postgres.sock"
      - "dbdata:/var/lib/postgresql/data"

volumes:
  mydata:
  dbdata:

短格式

可以使用 HOST:CONTAINER 格式,或者附带一个访问模式 HOST:CONTAINER:ro

可以挂载一个主机中的相对路径。

volumes:
  # Just specify a path and let the Engine create a volume
  - /var/lib/mysql

  # Specify an absolute path mapping
  - /opt/data:/var/lib/mysql

  # Path on the host, relative to the Compose file
  - ./cache:/tmp/cache

  # User-relative path
  - ~/configs:/etc/configs/:ro

  # Named volume
  - datavolume:/var/lib/mysql

长格式

长格式可以进行更精细的控制。

  • type: 挂载类型 为 volume, bind, tmpfsnpipe
  • source:挂载的源位置。可以是一个主机路径,一个定义于顶级 volumes 中的卷名称,等等。如果是挂载 tmpfs 则此参数无意义。
  • target: 容器内的挂载点路径。
  • read_only: 布尔值以设定卷的可写性。
  • bind: 配置附加的 bind 选项。

    • propagation: 用于 bind 的传播模式。
  • volume: 配置附加的 卷 选项

    • nocopy:布尔量以禁用数据复制(默认时当卷被首次创建时,容器内的内容将被复制到卷内)
  • tmpfs: 配置附加的 tmpfs 选项

    • size: tmpfs的容量,按字节数。
  • consistency:挂载的一致性要求:consistent 主机和容器有同样的视图,cached 读操作被缓冲,主机视图为主体,delegated 读写操作被缓冲,容器视图为主体。
version: "3.7"
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - type: volume
        source: mydata
        target: /data
        volume:
          nocopy: true
      - type: bind
        source: ./static
        target: /opt/app/static

networks:
  webnet:

volumes:
  mydata:
长格式在 v3.2 之后可用

VOLUMES FOR SERVICES, SWARMS, AND STACK FILES

当工作在 services, swarms, 或 docker-stack.yml 场景下,要注意一个服务在 swarm 中可能被部署到任意节点,而每当服务被更新之后再次启动时,可能已经不再在原来的节点上了。

当指定名称的卷并不存在时,Docker会自动创建一个匿名卷用于引用的服务。匿名卷是不可持久化的,因此当关联的容器实例退出并被移除时,匿名卷也会被销毁。

如果想要持久化你的数据,采用命名卷以及选择恰当的卷驱动程序,这个驱动应该是跨主机的,这样数据才能在不同的主机之间漫游。否则的话,你应该设置服务的约束条件以便该服务只会被部署到特定的节点上,这些节点上有相应的卷服务在正确工作。

作为一个例子,votingapp sample in Docker Labsdocker-stack.yml 文件定义了 db 服务,运行着 postgresql。它使用了一个命名卷 db-data 来持久化数据库数据,这个卷被通过swarm约束在只能运行在 manager 这个节点上,因此一切疑难都不存在了。下面是源码:

version: "3.7"
services:
  db:
    image: postgres:9.4
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend
    deploy:
      placement:
        constraints: [node.role == manager]

CACHING OPTIONS FOR VOLUME MOUNTS (DOCKER DESKTOP FOR MAC)

在 Docker 17.04 CE Edge 以及其后版本中(乃至于 17.06CE Edge 和 Stable 版本),你可以配置容器和主机之间卷如何被同步的一致性约束参数。这些标志包括:

  • consistent 完全一致。主机和容器有同样的视图,这是默认的策略。
  • cached 宿主机为准。对卷的读操作被缓冲,主机视图为主体,
  • delegated 容器为准。对卷的读写操作被缓冲,容器视图为主体。

这是专为 Docker Desktop for Mac 而适配的。由于已经知道的 osxfx 的关于文件共享特性原因,合理的设置一致性标志能够改善容器内外访问挂载卷时的性能问题。

下面是一个cached卷的例子:

version: "3.7"
services:
  php:
    image: php:7.1-fpm
    ports:
      - "9000"
    volumes:
      - .:/var/www/project:cached

对于读写操作均被缓冲的情况,即使容器中发生了什么修改(对于向PHP Website这样的典型架构来说,./config.php 经常是可能被写入的),也不会立即体现到宿主机中来,容器中的写入将会被堆积。

卷在容器内外的一致性问题,应该参考 Performance tuning for volume mounts (shared filesystems)

在这里我未能原样翻译,因为那样会带来较长的篇幅,我尚未能就此问题组织好语言。

domainname, hostname, ipc, mac_address, privileged, read_only, shm_size, stdin_open, tty, user, working_dir

这些配置具有单一值。和 docker run 的相应命令行参数相对应。mac_address 已经是被遗弃的设定。

user: postgresql
working_dir: /code

domainname: foo.com
hostname: foo
ipc: host
mac_address: 02:42:ac:11:65:43

privileged: true


read_only: true
shm_size: 64M
stdin_open: true
tty: true

指定时间段 duration

有的配置选项,例如 interval 或者 timeout(都是 check 的子选项),接受一个字符串风格的时间周期或时间段的参数值。它们应该具有这样的格式:

2.5s
10s
1m30s
2h32m
5h34m56s

可以为数值附加的后缀单位有 us, ms, s, m, 以及 h

含义自明。

指定字节值

有的配置选项,例如 build 的子选项 shm_size,接受一个字符串分隔的容量尺寸参数值。它们应该具有这样的格式:

2b
1024kb
2048k
300m
1gb

有效的后缀单位包括 b, k, mg。此外,kb, mbgb 也是合法的。纯粹的十进制数值并不合法。

卷编排格式手册 - volumes

顶级的 volumes 章节可以声明和创建命名卷(无需使用 volume_from),这些卷能够被用于在 service 章节下的 volumes 小节中被引用。所以我们可以重用它们,甚至能够跨越多个 services。docker命令的 docker volume 子命令有更多的参考信息。

关于卷的使用,也可以参考 Use volumesVolume Plugins

这里有一个示例,包含了两个服务,数据库的数据存储文件夹在两个服务之间被共享,因而数据库可以使用这个存储文件夹,而备份服务同样可以操作它以完成备份任务:

version: "3.7"

services:
  db:
    image: db
    volumes:
      - data-volume:/var/lib/db
  backup:
    image: backup-service
    volumes:
      - data-volume:/var/lib/backup/data

volumes:
  data-volume:

顶级 volumes 章节下的条目可以是空,无需指定细节,这样的话,默认的卷驱动程序将被应用(通常都会是 local 卷驱动)。

但你也可以通过下面的参数对其进行定制:

driver

指定哪一个卷驱动程序会被采用。一般来说,默认值会是 local。如果卷驱动程序无效、或不能工作,在 docker-compose up 时 Docker Engine将会返回一个错误。

driver: foobar

driver_opts

可选地指定一组键值对参数集,这些参数将被传递给卷驱动程序。所以这些参数集是和卷驱动程序相关的,请参考卷驱动程序的有关文档。

volumes:
  example:
    driver_opts:
      type: "nfs"
      o: "addr=10.40.0.199,nolock,soft,rw"
      device: ":/docker/example"

external

如果设置为 true,表示相应的卷是在 compose 编排文件之外被创建就绪的。此时 docker-compse up 将不会尝试创建这个卷,而如果该卷尚未存在则会返回一个错误。

对于 v3.3 以及更低的 compose 编排格式版本而言,external 不可以被用于与其他卷配置参数组合使用,例如 driver, driver_opts, labels 等等。但对于 v3.4 以及更高版本来说不再有此限制。

下面的示例中,Compose 查找一个名为 data 的外部卷并挂载它到 db 服务中,而不是尝试创建一个名为 [projectname]_data 的新卷。

version: "3.7"

services:
  db:
    image: postgres
    volumes:
      - data:/var/lib/postgresql/data

volumes:
  data:
    external: true
external.name 在 v3.4+ 已被废弃,这之后直接使用 name

你也可以单独指定卷名字(这时,data 被认为是该卷在当前编排文件中被引用时的 卷别名):

volumes:
  data:
    external:
      name: actual-name-of-volume
External volumes are always created with docker stack deploy

在使用 docker stack deploy 部署到 swarm 中时,外部卷如果不存在,则总是自动被创建。进一步的有关信息请参阅 moby/moby#29976

labels

使用 Docker labels 为容器添加元数据。可以是数组格式或者字典格式。

我们建议你使用反向DNS标注方式,为你的元数据表键添加反向域名前缀,从而避免潜在的和其它应用的相同名字的表键发生冲突:

labels:
  com.example.description: "Database volume"
  com.example.department: "IT/Ops"
  com.example.label-with-empty-value: ""
labels:
  - "com.example.description=Database volume"
  - "com.example.department=IT/Ops"
  - "com.example.label-with-empty-value"

name

since v3.4+

为卷指定一个自定义的名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。

version: "3.7"
volumes:
  data:
    name: my-app-data

name 可以被与 external 相组合:

version: "3.7"
volumes:
  data:
    external: true
    name: my-app-data

网络编排格式手册 - networks

顶级章节 networks 使得你可以配置想要创建和使用的网络(Compose内网)。

driver

指定该网络的驱动程序。

缺省的驱动程序由 Docker Engine 的启动参数所指定。通常情况下,启动参数内置为在单节点宿主机上使用 bridge 驱动,而在 swarm mode 中使用 overlay 驱动。

如果驱动程序不可用,Docker Engine 将会返回一个错误。

driver: overlay

bridge

缺省时 Docker 在每个宿主机节点上使用 bridge 驱动。有关桥接网络是如何工作的,可以参考 Docker Labs 的和网络相关的辅导用例:Bridge networking

overlay

overlay 驱动在多个 swarm mode 节点之间建立一个命名子网,这是一个跨主机的虚拟网络。

host or none

使用主机网络栈,或者不使用网络。

和命令行参数 --net=host 以及 --net=none 是等价的。

这两种驱动及网络模型只能被用于 docker stack 之中。如果你正在使用 docker compose 相关指令,请使用 network_mode 来指定它们。

If you want to use a particular network on a common build, use [network] as mentioned in the second yaml file example.

使用内建的网络模型,例如 hostnone,语法上有一点点需要注意的地方:如果用 hostnone 这样的名字定义一个外部网络(注意你并不需要真的创建他们,这两者都属于Docker内置的网络模型),那么在 Compose 编排文件中引用它们时你需要使用 hostnetnonet,如同这样:

version: "3.7"
services:
  web:
    networks:
      hostnet: {}

networks:
  hostnet:
    external: true
    name: host

---
services:
  web:
    ...
    build:
      ...
      network: host
      context: .
      ...
services:
  web:
    ...
    networks:
      nonet: {}

networks:
  nonet:
    external: true
    name: none

driver_opts

指定一组键值对表示的选项集,以传递给网络驱动程序。它们是和驱动程序密切相关的,所以具体的可用参数应该参考对应的驱动程序文档。

driver_opts:
  foo: "bar"
  baz: 1

attachable

since v3.2+

只能用于 driver: overlay 的场景。

如果被设置为 true,独立运行的容器也能被附着在该网络之中。如果独立运行的容器实例被附着到了一个 overlay 网络中,那么容器中的服务与单独的容器实例之间能够互相通讯。请注意你甚至可以附着其他 Docker daemon 中的容器实例到本 overlay 网络中。

networks:
  mynet1:
    driver: overlay
    attachable: true

enable_ipv6

在该网络/子网中启用 IPv6。

在 v3+ 中不被支持。

enable_ipv6 需要你使用 v2 的编排格式,而且也不能被用于 swarm mode 中。

ipam

自定义 IPAM 配置。每一项子配置都是可选参数。

  • driver: 自定义 IPAM 驱动程序,而不使用默认值
  • config: 一个列表,包含一到多个配置块。每个配置块具有下列子参数:

    • subnet: CIDR格式的子网定义,以划定一个网段。

一个完整的例子:

ipam:
  driver: default
  config:
    - subnet: 172.28.0.0/16
NOTE:附加IPAM如 gateway 只在 v2 中可用。

internal

默认时,Docker也会连接到一个桥接网络以提供外部可连接性。如果你想建立一个外部的隔离的 overlay 网络,请设置本选项为 true

labels

使用 Docker labels 为容器添加元数据。可以是数组格式或者字典格式。

我们建议你使用反向DNS标注方式,为你的元数据表键添加反向域名前缀,从而避免潜在的和其它应用的相同名字的表键发生冲突:

labels:
  com.example.description: "Financial transaction network"
  com.example.department: "Finance"
  com.example.label-with-empty-value: ""
labels:
  - "com.example.description=Financial transaction network"
  - "com.example.department=Finance"
  - "com.example.label-with-empty-value"

external

如果设置为 true,那么本网络是在 Compose 编排文件之外被创建和管理的。此时 dockercompose up 不会试图创建它,如果网络并不存在则返回一个错误。

对于 v3.3 以及更低版本的格式,external 不可与 driver, driver_opts, ipam, internal 等连用。此限制在 v3.4+ 之后被取消。

下面的例子里,proxy 是一个外部世界中的网关,Compose将会寻找通过 docker network create outside 所建立的 outside 外部网络,而不是试图去自动建立一个名为 [projectname]_outside 的新网络:

version: "3.7"

services:
  proxy:
    build: ./proxy
    networks:
      - outside
      - default
  app:
    build: ./app
    networks:
      - default

networks:
  outside:
    external: true
external.name 在 v3.5 及之后已经被废弃,请改用 name

你也可以单独指定一个网络名用于在Compose编排文件内被引用。

name

since v3.5

为网络设置一个自定义名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。

version: "3.7"
networks:
  network1:
    name: my-app-net

name 可以与 external 一起连用:

version: "3.7"
networks:
  network1:
    external: true
    name: my-app-net

配置项编排格式手册 - configs

顶级的 configs 章节声明,定义了一个配置项或者其参考,该配置项可以被授权给栈内服务使用。配置项的来源可以是 fileexternal

  • file: 配置项的内容在一个宿主机文件中。
  • external: 如果设置为 true,表示该配置项已经创建就绪了。Docker将不会试图建立它,而是在起不存在时生成一个 config not found 错误。
  • name: 该配置项在 Docker 中的名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。

下面的例子中,当作为栈的一部分被部署时,my_first_config 会被自动创建并命名为 <stack_name>_my_first_config,至于 my_second_config 是已经存在的。

configs:
  my_first_config:
    file: ./config_data
  my_second_config:
    external: true

另一种变化是外部配置项带有 name 定义的情况,此时该配置项在 Compose 中可以被以 redis_config 为名进行参考和使用:

configs:
  my_first_config:
    file: ./config_data
  my_second_config:
    external:
      name: redis_config

你仍需在栈内为每个服务声明 configs 章节以获得访问配置项的权利,参考 grant access to the config

敏感信息项编排格式手册 - secrets

顶级的 secrets 章节声明,定义了一个敏感信息项或者其参考,该敏感信息项可以被授权给栈内服务使用。敏感信息项的来源可以是 fileexternal

  • file: 敏感信息项的内容在一个宿主机文件中。
  • external: 如果设置为 true,表示该敏感信息项已经创建就绪了。Docker将不会试图建立它,而是在起不存在时生成一个 secret not found 错误。
  • name: 该敏感信息项在 Docker 中的名字。名字的值可被用于解决具有特殊字符名字的卷。注意该值被原样使用,引号不会被忽略,也不会被添加上栈名字前缀。

下面的例子中,当作为栈的一部分被部署时,my_first_secret 会被自动创建并命名为 <stack_name>_my_first_secret,至于 my_second_secret 是已经存在的。

secrets:
  my_first_secret:
    file: ./secret_data
  my_second_secret:
    external: true

另一种变化是外部配置项带有 name 定义的情况,此时该配置项在 Compose 中可以被以 redis_secret 为名进行参考和使用。

Compose File v3.5 及更高版本

secrets:
  my_first_secret:
    file: ./secret_data
  my_second_secret:
    external: true
    name: redis_secret

Compose File v3.4 和更低版本

my_second_secret:
    external:
      name: redis_secret

你仍需在栈内为每个服务声明 secret 章节以获得访问敏感信息项的权利,参考 grant access to the secret

变量替换

Compose编排文件中可以使用环境变量。当 docker-compose 运行时,Compose 从 Shell 环境变量中抽取变量值。例如,假设操作系统的环境变量中包含 POSTGRES_VERSION=9.3 的定义,那么以下定义

db:
  image: "postgres:${POSTGRES_VERSION}"

等价于

db:
  image: "postgres:9.3"

如果环境变量并不存在或者为空串,那么它就被当做是空字符串。

你可以通过 .env 文件来为环境变量设定缺省值。Compose 将会自动查找当前文件夹中的 .env 文件以获得环境变量的值。

IMPORTANT: 注意 .env 文件只在 docker-compose up 场景中有效,而在 docker stack deploy 时它并不会被使用。

两种语法 $VARIABLE${VARIABLE} 都是可用的。此外在 v2.1 格式中,类似于 Shell 语法的如下形式也可以被使用:

  • ${VARIABLE:-default} 将会返回 default,如果环境变量 VARIABLE 为空字符串或未被设置的话。
  • ${VARIABLE-default} 将会返回 default,如果环境变量 VARIABLE 未被设置的话。

类似地,下面的语法有助于指定一个明确的值:

  • ${VARIABLE:?err} 将会产生错误信息 err,如果环境变量 VARIABLE 为空或未被设置的话。
  • ${VARIABLE?err} 将会产生错误信息 err,如果环境变量 VARIABLE 未被设置的话。

其他的 Shell 语法特性并未被支持,例如 ${VARIABLE/foo/bar}

如果需要一个美元符号的话,请使用 $$`。此时 `$$ 不再参与环境变量替换的解释。如下例:

web:
  build: .
  command: "$$VAR_NOT_INTERPOLATED_BY_COMPOSE"

如果你忘记了这个规则而使用了一个 $ 单个字符的话,Compose 会警告你:

The VAR_NOT_INTERPOLATED_BY_COMPOSE is not set. Substituting an empty string.

扩展字段

since v3.4

通过扩展字段,能够重用编排配置片段。它们可以是自由的格式,前提是你将它们定义在 yaml 文档的顶级,并且其章节名以 x- 开头:

version: '3.4'
x-custom:
  items:
    - a
    - b
  options:
    max-size: '12m'
  name: "custom"

NOTE

从 v3.7 开始(对于 3.x 系列),或者从 v2.4 开始(对于 2.x 系列),扩展字段也可以被放在 服务,卷,网络,配置项以及敏感信息项顶级章节之下的第一级。

如同这样:

version: '3.7'
services:
redis:
 # ...
x-custom:
 items:
      - a
      - b
 options:
   max-size: '12m'
 name: "custom"

所谓的自由格式,是指这些定义并不被 Compose 所解释。然而当你在某个地方插入它们的引用时,它们会被展开到插入点,然后再结合上下文被 Compose 解释具体的语义。这使用了 YAML anchors 语法。

例如,如果你的多个服务都会使用相同的日志记录选项:

logging:
  options:
    max-size: '12m'
    max-file: '5'
  driver: json-file

你可以这样定义:

x-logging:
  &default-logging
  options:
    max-size: '12m'
    max-file: '5'
  driver: json-file

services:
  web:
    image: myapp/web:latest
    logging: *default-logging
  db:
    image: mysql:latest
    logging: *default-logging

通过 YAML merge type 语法,你也可以在插入扩展字段定义是覆盖某些子选项。例如:

version: '3.4'
x-volumes:
  &default-volume
  driver: foobar-storage

services:
  web:
    image: myapp/web:latest
    volumes: ["vol1", "vol2", "vol3"]
volumes:
  vol1: *default-volume
  vol2:
    << : *default-volume
    name: volume02
  vol3:
    << : *default-volume
    driver: default
    name: volume-local

Compose 文档参考

结束

查看原文

赞 1 收藏 1 评论 0

hedzr 发布了文章 · 2019-07-23

从 golang flag 迁移到 cmdr

基于 cmdr v1.0.3

从 golang flag 迁移到 cmdr

采用一个新的命令行解释器框架,最痛苦地莫过于编写数据结构或者流式定义了。我们首先回顾一下 cmdr 和其它大多数三方增强命令行解释器都支持的最典型的两种命令行界面定义方式,然后再来研究一下 cmdr 新增的最平滑的迁移方案。

典型的方式

通过结构数据体定义

有的增强工具(例如 cobra,viper)采用结构体数据定义方式来完成界面指定,如同 cmdr 的这样:

rootCmd = &cmdr.RootCommand{
    Command: cmdr.Command{
        BaseOpt: cmdr.BaseOpt{
            Name:            appName,
            Description:     desc,
            LongDescription: longDesc,
            Examples:        examples,
        },
        Flags: []*cmdr.Flag{},
        SubCommands: []*cmdr.Command{
            // generatorCommands,
            // serverCommands,
            msCommands,
            testCommands,
            {
                BaseOpt: cmdr.BaseOpt{
                    Short:       "xy",
                    Full:        "xy-print",
                    Description: `test terminal control sequences`,
                    Action: func(cmd *cmdr.Command, args []string) (err error) {
                        fmt.Println("\x1b[2J") // clear screen

                        for i, s := range args {
                            fmt.Printf("\x1b[s\x1b[%d;%dH%s\x1b[u", 15+i, 30, s)
                        }

                        return
                    },
                },
            },
            {
                BaseOpt: cmdr.BaseOpt{
                    Short:       "mx",
                    Full:        "mx-test",
                    Description: `test new features`,
                    Action: func(cmd *cmdr.Command, args []string) (err error) {
                        fmt.Printf("*** Got pp: %s\n", cmdr.GetString("app.mx-test.password"))
                        fmt.Printf("*** Got msg: %s\n", cmdr.GetString("app.mx-test.message"))
                        return
                    },
                },
                Flags: []*cmdr.Flag{
                    {
                        BaseOpt: cmdr.BaseOpt{
                            Short:       "pp",
                            Full:        "password",
                            Description: "the password requesting.",
                        },
                        DefaultValue: "",
                        ExternalTool: cmdr.ExternalToolPasswordInput,
                    },
                    {
                        BaseOpt: cmdr.BaseOpt{
                            Short:       "m",
                            Full:        "message",
                            Description: "the message requesting.",
                        },
                        DefaultValue: "",
                        ExternalTool: cmdr.ExternalToolEditor,
                    },
                },
            },
        },
    },

    AppName:    appName,
    Version:    cmdr.Version,
    VersionInt: cmdr.VersionInt,
    Copyright:  copyright,
    Author:     "xxx <xxx@gmail.com>",
}
//... More

它的问题在于,如果你有 docker 那样的较多的子命令以及选项需要安排的话,这个方案会相当难定位,写起来也很痛苦,改起来更痛苦。

通过流式调用链方式定义

比结构体数据定义方案更好一点的是采用流式调用链方式。它可能长得像这样:

    // root

    root := cmdr.Root(appName, "1.0.1").
        Header("fluent - test for cmdr - no version - hedzr").
        Description(desc, longDesc).
        Examples(examples)
    rootCmd = root.RootCommand()

    // soundex

    root.NewSubCommand().
        Titles("snd", "soundex", "sndx", "sound").
        Description("", "soundex test").
        Group("Test").
        Action(func(cmd *cmdr.Command, args []string) (err error) {
            for ix, s := range args {
                fmt.Printf("%5d. %s => %s\n", ix, s, cmdr.Soundex(s))
            }
            return
        })

    // xy-print

    root.NewSubCommand().
        Titles("xy", "xy-print").
        Description("test terminal control sequences", "test terminal control sequences,\nverbose long descriptions here.").
        Group("Test").
        Action(func(cmd *cmdr.Command, args []string) (err error) {
            fmt.Println("\x1b[2J") // clear screen

            for i, s := range args {
                fmt.Printf("\x1b[s\x1b[%d;%dH%s\x1b[u", 15+i, 30, s)
            }

            return
        })

    // mx-test

    mx := root.NewSubCommand().
        Titles("mx", "mx-test").
        Description("test new features", "test new features,\nverbose long descriptions here.").
        Group("Test").
        Action(func(cmd *cmdr.Command, args []string) (err error) {
            fmt.Printf("*** Got pp: %s\n", cmdr.GetString("app.mx-test.password"))
            fmt.Printf("*** Got msg: %s\n", cmdr.GetString("app.mx-test.message"))
            fmt.Printf("*** Got fruit: %v\n", cmdr.GetString("app.mx-test.fruit"))
            fmt.Printf("*** Got head: %v\n", cmdr.GetInt("app.mx-test.head"))
            return
        })
    mx.NewFlag(cmdr.OptFlagTypeString).
        Titles("pp", "password").
        Description("the password requesting.", "").
        Group("").
        DefaultValue("", "PASSWORD").
        ExternalTool(cmdr.ExternalToolPasswordInput)
    mx.NewFlag(cmdr.OptFlagTypeString).
        Titles("m", "message", "msg").
        Description("the message requesting.", "").
        Group("").
        DefaultValue("", "MESG").
        ExternalTool(cmdr.ExternalToolEditor)
    mx.NewFlag(cmdr.OptFlagTypeString).
        Titles("fr", "fruit").
        Description("the message.", "").
        Group("").
        DefaultValue("", "FRUIT").
        ValidArgs("apple", "banana", "orange")
    mx.NewFlag(cmdr.OptFlagTypeInt).
        Titles("hd", "head").
        Description("the head lines.", "").
        Group("").
        DefaultValue(1, "LINES").
        HeadLike(true, 1, 3000)

    // kv

    kvCmd := root.NewSubCommand().
        Titles("kv", "kvstore").
        Description("consul kv store operations...", ``)
//...More

这种方式很有效地改进的痛苦之源。要说起来,也没有什么缺点了。所以这也是 cmdr 主要推荐你采用的方案。

通过结构 Tag 方式定义

这种方式被有一些第三方解释器所采用,可以算是比较有价值的定义方式。其特点在于直观、易于管理。

它的典型案例可能是这样子的:

type argT struct {
    cli.Helper
    Port int  `cli:"p,port" usage:"short and long format flags both are supported"`
    X    bool `cli:"x" usage:"boolean type"`
    Y    bool `cli:"y" usage:"boolean type, too"`
}

func main() {
    os.Exit(cli.Run(new(argT), func(ctx *cli.Context) error {
        argv := ctx.Argv().(*argT)
        ctx.String("port=%d, x=%v, y=%v\n", argv.Port, argv.X, argv.Y)
        return nil
    }))
}

不过,由于 cmdr 没有打算支持这种方案,所以这里仅介绍到这个程度。

说明一下,cmdr 之所以不打算支持这种方案,是因为这样做好处固然明显,坏处也同样令人烦恼:复杂的定义可能会因为被嵌套在 Tag 内而导致难以编写,例如多行字符串在这里就很难过。

cmdr 新增的兼容 flag 的定义方式

那么,我们回顾了两种或者三种典型的命令行界面定义方式之后,可以发现他们和 flag 之前的区别是比较大的,当你一开始设计你的 app 时,如果为了便宜和最快开始而采用了 flag 方案的话(毕竟,这是golang自带的包嘛),再要想切换到一个增强版本的话,无论哪一个都会令你痛一下。

flag 方式

我们看看当你采用 flag 方式时,你的 main 入口可能是这样的:

// old codes

package main

import "flag"

var (
      serv           = flag.String("service", "hello_service", "service name")
      host           = flag.String("host", "localhost", "listening host")
      port           = flag.Int("port", 50001, "listening port")
      reg            = flag.String("reg", "localhost:32379", "register etcd address")
      count          = flag.Int("count", 3, "instance's count")
      connectTimeout = flag.Duration("connect-timeout", 5*time.Second, "connect timeout")
)

func main(){
      flag.Parse()
      // ...
}

迁移到 cmdr

为了迁移为使用 cmdr,你可以简单地替换 import "flag" 语句为这样:

import (
  // “flag”
  "github.com/hedzr/cmdr/flag"
)

其它内容一律不变,也就是说完整的入口现在像这样:

// new codes

package main

import (
  // “flag”
  "github.com/hedzr/cmdr/flag"
)

var (
      serv           = flag.String("service", "hello_service", "service name")
      host           = flag.String("host", "localhost", "listening host")
      port           = flag.Int("port", 50001, "listening port")
      reg            = flag.String("reg", "localhost:32379", "register etcd address")
      count          = flag.Int("count", 3, "instance's count")
      connectTimeout = flag.Duration("connect-timeout", 5*time.Second, "connect timeout")
)
  
func main(){
    flag.Parse()
    // ...
}

怎么样,足够简单吧?

引入增强特性

那么我们现在期望引入更多 cmdr 专有特性怎么办呢?

例如想要全名(完整单词)作为长选项,补充短选项定义,这可以通过如下的序列来达成:

import (
    // “flag”
      "github.com/hedzr/cmdr"
      "github.com/hedzr/cmdr/flag"
)

var(
    // uncomment this line if you like long opt (such as --service)
    treatAsLongOpt = flag.TreatAsLongOpt(true)
  
    serv = flag.String("service", "hello_service", "service name",
                       flag.WithShort("s"),
                       flag.WithDescription("single line desc", `long desc`))
)

类似的可以完成其他增强特性的定义。

可用的增强特性

所有 cmdr 特性被浓缩在几个少量的接口中了。此外,某些特性是当你使用 cmdr 时就立即获得了,无需其它表述或设定(例如短选项的组合,自动的帮助屏,多级命令等等)。

所有的这些需要指定适当参数的特性,包含在如下的这些定义中:

flag.WithTitles(short, long string, aliases ...string) (opt Option)

定义短选项,长选项,别名。

综合来说,你必须在某个地方定义了一个选项的长名字,因为这是内容索引的依据,如果长名字缺失,那么可能会有意料之外的错误。

别名是随意的。

如果可以,尽可能提供短选项。

短选项一般来说是一个字母,然而使用两个甚至更多字母是被允许的,这是为了提供多种风格的命令行界面的兼容性。例如 wget, rar 都采用了双字母的短选项。而 golang flag 自身支持的是任意长度的短选项,没有长选项支持。cmdr 在短选项上的宽松和兼容程度,是其它几乎所有第三方命令行参数解释器所不能达到的。

flag.WithShort(short string) (opt Option)

提供短选项定义。

flag.WithLong(long string) (opt Option)

提供长选项定义。

flag.WithAliases(aliases ...string) (opt Option)

提供别名定义。别名是任意多、可选的。

flag.WithDescription(oneLine, long string) (opt Option)

提供描述行文本。

oneLine 提供单行描述文本,这通常是在参数被列表时。long 提供的多行描述文本是可选的,你可以提供空字串给它,这个文本在参数被单独显示帮助详情时会给予用户更多的描述信息。

flag.WithExamples(examples string) (opt Option)

可以提供参数用法的命令行实例样本。

这个字串可以是多行的,它遵照一定的格式要求,我们以前的文章中对该格式有过描述。这样的格式要求是为了在 man/info page 中能够获得更视觉敏锐的条目,所以你可以自行判定要不要遵守规则。

flag.WithGroup(group string) (opt Option)

命令或者参数都是可以被分组的。

分组是可以被排序的。给 group 字串一个带句点的前缀,则这个前缀会被切割出来用于排序,排序规则是 A-Z0-9a-z 按照 ASCII 顺序。所以:

  • 1001.c++, 1100.golang, 1200.java, …;
  • abcd.c++, b999.golang, zzzz.java, …;

是有顺序的。

由于第一个句点之前的排序用子串被切掉了,因此你的 group 名字可以不受这个序号的影响。

给分组一个空字串,意味着使用内置的 分组,这个分组被排列在其他所有分组之前。

给分组一个 cmdr.UnsortedGroup 常量,则它会被归纳到最后一个分组中。值得注意的是,最后一个分组,依赖的是 cmdr.UnsortedGroup 常量的具体值zzzz.unsorted,所以,你仍然有机会定义一个别的序号来绕过这个“最后”。

flag.WithHidden(hidden bool) (opt Option)

hidden为true是,该选项不会被列举到帮助屏中。

flag.WithDeprecated(deprecation string) (opt Option)

一般来说,你需要给 deprecation 提供一个版本号。这意味着,你提醒最终用户该选项从某个版本号开始就已经被废弃了。

按照 Deprecated 的礼貌规则,我们废弃一个选项时,首先标记它,并给出替代提示,然后在若干次版本迭代之后正式取消它。

flag.WithAction(action func(cmd *Command, args []string) (err error)) (opt Option)

按照 cmdr 的逻辑,一个选项在被显式命中时,你可以提供一个即时的响应动作,这可能允许你完成一些特别的操作,例如为相关联的其它一组选项调整默认值什么的。

flag.WithToggleGroup(group string) (opt Option)

如果你打算定义一组选项,带有互斥效果,如同 radio button group 那样,那么你可以为它们提供相同的 WithToggleGroup group name这个名字和 WithGroup group name 没有任何关联关系

flag.WithDefaultValue(val interface{}, placeholder string) (opt Option)

提供选项的默认值以及占位符。

默认值的数据类型相当重要,因为这个数据类型时后续抽取该选项真实值的参考依据。

例如,int数据一定要提供一个 int 数值,Duration数据一定要提供一个 3*time.Second 这样的确切数值。

flag.WithExternalTool(envKeyName string) (opt Option)

提供一个环境变量名,例如 EDITOR。那么,如果该选项的 value 部分没有在命令行中被提供时,cmdr 会搜索环境变量的值,将其作为控制台终端应用程序运行,并收集该运行的结果(一个临时文件的文件内容)用于为该选项复制。

如同 git commit -m 那样。

flag.WithValidArgs(list ...string) (opt Option)

提供一个枚举表,用于约束用户所提供的值。

flag.WithHeadLike(enable bool, min, max int64) (opt Option)

当该选项被设定为 enable=true 时,识别用户输入的诸如 -1973, -211 之类的整数短选项,将其整数数值作为本选项的数值。

如同 head -9 等效于 head -n 9 那样。

结束语

好了。很多内容。不过还是堆出来了,自己欣慰一下。

真正的结束语

嗯,cmdrv1.0.3 是一个 pre-release 版本,我们已经提供一个 flag 的最平滑迁移的基本实现。

最近的日子里,我们会考虑完成子命令部分,并最终释出 v1.1.0,请期待。

如果认为这样做有价值的话,考虑去鼓励一下。

查看原文

赞 0 收藏 0 评论 0

hedzr 发布了文章 · 2019-06-16

CMDR-05: Tricks / Walks / Hooks

暂时来讲,这是最后一篇关于 cmdr 的系列介绍文章了。

所有这个系列包括:

这一次的内容算是杂烩乱炖。

Tricks

~~debug

已经在前文讲述过了。这里不再凑字数了。

--tree

cmdr 提供了一个内置的选项:--tree

虽然这是一个选项,但它和 --version 一样是有着命令一样的效果:如果 cmdr 在命令行参数中检测到了 --tree,那么它会忽略已经处理的和将要处理的子命令、选项,直接执行 --treeAction

要想达到类似的效果并不困难:

定义一个选项,重载其 Action 字段到一个响应函数,并且在该响应函数的结尾返回 cmdr.ErrShouldBeStopException,这样就会在该选项被识别时并执行Action后直接退出应用程序了。

--tree 的功能是打印出全部命令和子命令,以树结构方式呈现出来。

一个样例如下图:

clipboard.png

这是我在开发阶段执行 examples/demo 小程序所得到的结果。

Walk for all commands

--tree 实际上是利用了 cmdr 内建的 WalkAllCommands() 所提供的遍历方式。

对所有命令及其选项进行遍历,实际上有两种方式:一是利用 Painter 以及相应的内部机制,二是通过 WalkAllCommands 明确地遍历。

Painter

Painter 是一个接口。它被用在输出帮助屏这个方面。尽管输出帮助屏只是一个小小的功能,但你还是可以自定义它的行为。你可以自行实现 Painter 接口并通过 SetCurrentHelpPainter(painter) 来更改帮助屏的显示内容。

如果你真的想这么做,可以查阅 Painter 的定义,也可以 issue 到我,或许说不定我能够有所建议。

Walker

WalAllCommands(cmd, index, walker) 是一个更为强大的遍历器,实际上 manpage,markdown 的输出就是通过这个机制来实现的。利用这个遍历器,你可以便利整个命令集的树状结构。一般来说,你应该给它传递 cmd=nil, index=0 的参数值来开始你的遍历,这表示将会从顶级命令开始遍历,而且将其视作第 0 层。index 这个参数将会在遍历器递归时自动修正到符合层级计数,然后会被传递给 walker。我只是懒得将它改成 level 名字了,它就是那个用途。

例如 --tree 的实现源代码如下:

func dumpTreeForAllCommands(cmd *Command, args []string) (err error) {
    command := &rootCommand.Command
    _ = walkFromCommand(command, 0, func(cmd *Command, index int) (e error) {
        if cmd.Hidden {
            return
        }

        deep := findDepth(cmd) - 1
        if deep == 0 {
            fmt.Println("ROOT")
        } else {
            sp := strings.Repeat("  ", deep)
            // fmt.Printf("%s%v - \x1b[%dm\x1b[%dm%s\x1b[0m\n",
            //     sp, cmd.GetTitleNames(),
            //     BgNormal, CurrentDescColor, cmd.Description)

            if len(cmd.Deprecated) > 0 {
                fmt.Printf("%s\x1b[%dm\x1b[%dm%s - %s\x1b[0m [deprecated since %v]\n",
                    sp, BgNormal, CurrentDescColor, cmd.GetTitleNames(), cmd.Description,
                    cmd.Deprecated)
            } else {
                fmt.Printf("%s%s - \x1b[%dm\x1b[%dm%s\x1b[0m\n",
                    sp, cmd.GetTitleNames(), BgNormal, CurrentDescColor, cmd.Description)
            }
        }
        return
    })
    return ErrShouldBeStopException
}

比较

可以想象到你能够借助这个遍历器实现某些更强大的特性,在具备遍历能力的基础上,我们其实可以设计更强大的命令行界面结构,而不必担心过分复杂带来的负面效果。

关于如何设计命令行界面的体系结构,保持其清晰性,这个不是我们再这个系列文章中要讨论的话题。

至于 Painter 和 Walker,其区别也很明显。Painter 是被限定在帮助屏构造层面的,且不会递归下去,除非你想自行实现。Walker 是全局层面的递归遍历器,面向的是所有的命令。

Actions

Action for Command

CommandAction 字段可以定义你的命令的业务实现逻辑。

func MsTagsList(cmd *cmdr.Command, args []string) (err error) {
    return
}

一般来说,你在 impl package 中定义业务实现逻辑的入口,如同上面的代码示例,并在某个 Command 的数据定义中引用它。

msTagsListCommand = &cmdr.Command {
    BaseOpt: cmdr.BaseOpt {
        Short: "ls",
        Full: "list",
        Description: "list all tags of a micro-service",
        Action: impl.MsTagsList,
    }
}

所以,对于 Command 来说,Action 可能是最重要的 Hook。

PreAction & PostAction for Command

CommandAction 被执行之前,其 PreAction 会被首先调用,你可以定义自己的逻辑,例如检查特定条件是否满足,如果不满足则返回一个error以通知cmdr错误性结束。如果你认为并没有错误发生,但仍应该结束处理,你可以返回一个 cmdr.ErrShouldBeStopException,这样的话 cmdr 也会结束处理,但整个 program 会被正常终止:反应到 Shell 层面上时,此时程序是无错的,Shell返回值为0。

CommandAction 被执行之后,无论处理结果如何,PostAction 将会被调用。它可以用来进行某些退出时逻辑处理。

PreAction & PostAction for RootCommand

在被命中的命令的 PreActionPostAction 被执行之际,RootCommandPreActionPostAction 也会分别被执行。这是一个关键性的特性。它使得你可以妥善地实现你的全局预处理和后处理逻辑,例如注册微服务到注册中心以及撤销注册,连接到数据库和关闭数据库连接,等等。

Action for Flag

对于 Flag 来说,没有 Pre/Post 机制。

Flag 具有 Action 的 Hook,所以你可以在每个 Flag 被命中时做点什么事。例如,反转或复位 Owner 的所有其他 bool flags,或者为应用程序的其他配置设定一整组预设方案,等等。

值得一提的是,在 v0.2.15 之后,我们已经实现了 ToggleGroup,因此对于想要建立RadioButtonGroup 效果的场景来说,你倒是不必再手写逻辑了。

Listeners

AddOnAfterXrefBuilt(cb HookXrefFunc), AddOnBeforeXrefBuilding(cb HookXrefFunc)

Xref 术语是一个特定节点的表述。

当 cmdr.Exec(rootCmd) 进入状态时,它依次做这些事:

  1. 初始化相关内务
  2. 调用所有 beforeXrefBuilding Hooks
  3. buildXref
  4. 调用所有 afterXrefBuilt Hooks
  5. 开始处理命令行的所有参数
  6. ...

在 buildXref 阶段,cmdr 实际上建立了 rootCmd 及其所有子命令和选项集合 的内部map、索引等等交叉引用。这个过程中,cmdr 处理 rootCmd,也查找配置文件以及子目录 conf.d,也对环境变量进行搜寻。

当 buildXref 完成之后,cmdr.GetXXX() 就是完全可用的状态了。

如此,beforeXrefBuilding 和 afterXrefBuilt 的 hooks 实际上是你想要进行定制化操作的关键节点。

欢迎使用。

AddOnConfigLoadedListener,RemoveOnConfigLoadedListener,SetOnConfigLoadedListener

应该不必解释太多。

当外部文件被修改时,cmdr自动载入变化,并合并到已有的选项集合中,然后发出回调通知观察者应该做点什么了。

你可以在任何地方任意地发出观察约定。整体上说这是极为轻量级的。这么做的原因是不同业务模块有必要自行管理自己的相关选项集而不是在全局的某一个集中点处理全部模块的全部选项集合,那太耦合了。

需要特别指出的是,配置文件总是一个主文件,加上可选多个子配置文件(都被放在配置文件所在目录的 conf.d 子目录之下)。而我们内部的文件系统 Watcher 只会监听 conf.d 之中的文件变化,而忽略主文件的变化。

所以你不应该在主配置文件中放置太多具体选项。好的实践是将一切配置都切分到 conf.d 之中。

SetPredefinedLocations(locations string) 允许你指定配置文件的搜索路径。

SetOnConfigLoadedListener 的用途是临时禁用和启用一个观察者。

SetCustomShowBuildInfo(fn func())SetCustomShowVersion(fn func())

说它们是 Hook 也不算错。

cmdr 自动提供 ShowBuildInfo 和 ShowVersion 实现,用于打印 编译信息屏 和 版本信息屏。

SetCustonShowBuildInfo 和 SetCustomShowVersion 则允许你自行提供你的实现。

小结

暂时结束。继续改进。

查看原文

赞 0 收藏 0 评论 0

hedzr 关注了用户 · 2019-06-03

苦茶清浅丶 @kuchaqingqian_5ceb3ef28c8dc

关注 3

hedzr 发布了文章 · 2019-06-03

cmdr 04 - 简单微服务 (daemon)

cmdr 04 - simple micro-service
based on cmdr v0.2.21

My ado is too much.

所以这次直入主题,谢绝吐槽。不知道 cmdr 干嘛用的,无妨看看前文

那么,golang适合做后端开发,无论是 gRPC 还是 RESTful 都是它的强项。

一旦我们想要开发一个微服务时,抛开核心逻辑不谈,也不论 DevOps 方面究竟是 K8s,还是 Docker,还是裸机,总要面对一个启动、调试、测试的日常问题。

cmdr 除了提供命令行参数的解释能力之外,也额外提供了一个daemon插件,它可以帮助你简化日常开发工作,也令你不必关心 pid 文件、日志、退出信号等等问题,也无需重复编排 daemon 相关的命令行指令。

下面介绍怎么使用 daemon 插件,怎么编写实际的业务逻辑。我们以 demo 为例编写一个简单的示例性微服务,并解释具体的做法。

使用 Daemon 插件

启用 Daemon 插件

启用 Daemon 插件只需一行代码:

// Entry is app main entry
func Entry() {

    logrus.SetLevel(logrus.DebugLevel)
    logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})

    daemon.Enable(svr.NewDaemon(), nil, nil, nil)

    if err := cmdr.Exec(rootCmd); err != nil {
        logrus.Errorf("Error: %v", err)
    }

}

实现 daemon.Daemon 接口

启用 daemon 插件,需要你实现 daemon.Daemon 接口,并编写一定的包装代码来连接 cmdr, daemon 以及你的业务逻辑。

daemon.Daemon 接口

daemon.Daemon 接口是这样的:

// Daemon interface should be implemented when you are using `daemon.Enable()`.
type Daemon interface {
    OnRun(cmd *cmdr.Command, args []string, stopCh, doneCh chan struct{}) (err error)
    OnStop(cmd *cmdr.Command, args []string) (err error)
    OnReload()
    OnStatus(cxt *Context, cmd *cmdr.Command, p *os.Process) (err error)
    OnInstall(cxt *Context, cmd *cmdr.Command, args []string) (err error)
    OnUninstall(cxt *Context, cmd *cmdr.Command, args []string) (err error)
}

对于一个微服务来说,你一定要编写的是 OnRunOnStop 两个方法。其他的几个方法,通常是可选的,daemon插件针对 RESTful http2 完成了默认的逻辑来支持 reload,status。此外,daemon插件还针对 systemd 实现了默认的 install 和 uninstall 逻辑。

所以下面我们首先完成必须的部分:

OnRun

type daemonImpl struct {}

func (*daemonImpl) OnRun(cmd *cmdr.Command, args []string, stopCh, doneCh chan struct{}) (err error) {
    logrus.Debugf("demo daemon OnRun, pid = %v, ppid = %v", os.Getpid(), os.Getppid())
    go worker(stopCh, doneCh)
    return
}

func worker(stopCh, doneCh chan struct{}) {
LOOP:
    for {
        time.Sleep(time.Second) // this is work to be done by worker.
        select {
        case <-stopCh:
            break LOOP
        default:
        }
    }
    doneCh <- struct{}{}
}

daemon 提供两个 channels,stopCh 应该促使你的代码结束无限循环,当你的代码退出循环之后应该触发 doneCh 事件。这样的逻辑保证了整个微服务的 graceful shutdown。

至于核心的逻辑,我们的 worker,现在仅仅是个无限循环而已,你可以根据实际业务需要对其完成替换。

下一次我们也许提供一个 RESTful micro-service 的样本。

OnStop 以及其他

func (*daemonImpl) OnStop(cmd *cmdr.Command, args []string) (err error) {
    logrus.Debugf("demo daemon OnStop")
    return
}

func (*daemonImpl) OnReload() {
    logrus.Debugf("demo daemon OnReload")
}

func (*daemonImpl) OnStatus(cxt *daemon.Context, cmd *cmdr.Command, p *os.Process) (err error) {
    fmt.Printf("%v v%v\n", cmd.GetRoot().AppName, cmd.GetRoot().Version)
    fmt.Printf("PID=%v\nLOG=%v\n", cxt.PidFileName, cxt.LogFileName)
    return
}

func (*daemonImpl) OnInstall(cxt *daemon.Context, cmd *cmdr.Command, args []string) (err error) {
    logrus.Debugf("demo daemon OnInstall")
    return
}

func (*daemonImpl) OnUninstall(cxt *daemon.Context, cmd *cmdr.Command, args []string) (err error) {
    logrus.Debugf("demo daemon OnUninstall")
    return
}

其它的接口方法基本上什么也不做,因为对于我们的worker来说,不需要在 OnStop 时清理数据库连接、释放其它资源,也不需要在 OnReload 时加载新的配置文件。

测试 demo

现在我们可以将 demo 跑起来看看了。首先研究下有什么命令行指令可供使用,我们采用 --tree 来看看:

可以注意到 s, server, serve, svr, daemon 命令是新增的,它包含一组子命令以提供 daemon 相关的操作。

其中 server start 子命令的解说是这样的:

也就是说,start子命令的两个变形允许你在前台运行微服务,这是为了便于 docker 集成,以及在 IDE 中调试微服务的目的:

# 在前台运行微服务,而不是进入 daemon 模式
demo run
demo start --foreground

对于 daemon 模式,没有标准的规范定义来要求一定具备哪些要素,不过大体上还是有约定俗成的东西。daemon 在中文中常常被称作 守护进程

daemon 模式一般来说包含这些要素:

  • 进程启动后,fork自己的一份副本在操作系统中运行,这样副本和 tty 的关联就被detach了,此外子进程也具有独立的环境和进程空间,甚至是身份,不会收到其它服务、其它 ttys 的干扰。
  • 子进程在 /var/run 中保持一个 pid 文件,这指示了子进程的基本状态
  • 子进程通过 syscall signals 来与前台交互,一般地说,SIGHUP信号使得子进程 reload 配置信息完成重启动、却不被关闭进程和重新启动进程;SIGTERM等信号通知子进程结束服务。等等。
  • 子进程将日志输出为 /var/log/ 下的日志文件

前台运行

所以,我们运行下demo在前台:

然后按下 CTRL-C 终止它,可以看到这个”微服务“能够正常地跑起来,也能正常地自行销毁。

守护进程运行

而如果我们要运行 demo 为守护进程的话,首先你需要将它编译成可执行文件,然后才能启动为守护进程模式。

通过 vagrant 环境,我们可以看到守护进程启动了,然后被我们的 stop 指令正确地关闭了。

systemd 服务运行

在支持 systemd 的 Linux 发行版中,我们可以测试将微服务安装为 systemd 服务。

其中,sudo /vagrant/demo server install 完成安装动作,成功之后demo服务就安装就绪了,并且已经被预设为随系统启动而自动启动的模式。

然后我们依照 systemd 的规范启动它:sudo systemctl start demo@$USER.service

值得注意的是,我们将 demo 安装为了通配模式,因此 demo 是可以在不同用户身份下被启动的。如果你想用专用的微服务账户启动它,你可以使用:sudo systemctl start demo@msuser.service

然后我们通过 sudo systemctl status demo@vagrant.service 查看到 demo 已经启动成功了,其中有三个错误,然而他们是可以被忽略的,它们都是为了尝试建立几个相关文件夹的目的,所以只是预防性的指令。而 demo 的正主,也就是 ExecStart 行表示启动时成功的,而且 Active 的状态也是 running 状态。

此时,log/logrus 等日志输出也被转发到了日志文件 /var/log/demo/demo.log 中。

那么我们也可以通过 sudo systemctl stop demo@$USER.service 来停止服务。

小结

由于 systemd 在 macOS 中并不被直接支持,所以对于这个部分的测试是放在 vagrant 中完成的。

对于 Windows 来说,你只能使用 server run 前台运行的方式,我们也暂无支持 NT Service 的计划。但你可以通过前台运行的方式完成日常开发调试工作。

实际的生产环境中,你可以选择 centos,ubuntu 等发行版,部署需要的只是 sudo demo server install 一条指令。

对于容器的环境,你应该使用 demo server run 这种前台运行模式来启动。

参考

查看原文

赞 0 收藏 0 评论 0

hedzr 发布了文章 · 2019-06-01

cmdr 03 - 用流式接口定义命令行参数处理选项

cmdr 03 - 用流式接口定义命令行参数处理选项

基于 v0.2.17

转眼已经来到了 cmdr v0.2.17 了,为了解决此前版本中关于子命令和选项定义语句的太多嵌套的问题,我们实现了流式调用接口(Fluent APIs)。

cmdr 是我发布的一个开源的 golang 命令行参数处理器。它是 golang flags 的替代品。之所以发布它,是因为已有的 command line UI 三方包无法满足我的日常要求,迫不得己自己造一个。如果尚未有了解 cmdr 怎么使用的,不妨抽空浏览我的早前文章,以求获得一些基本概念:

稍后我会继续针对 cmdr 的用法做介绍文章。

至于本文呢 ,只是简单讲述一下如何使用 cmdr 的流式接口(Fluent API)来完成定义。

定义 RootCommand

root := cmdr.Root("aa", "1.0.1").Header("aa - test for cmdr - no version")
rootCmd = root.RootCommand()

第二句是拿到一个 *cmdr.Command 指针,稍后可以做一下相关的其它操作。

此外,rootCmd 作为函数返回值,也便于被用到向 cmdr.Exec() 做传递参数。

func buildCmds() *cmdr.Command {
    root := ...
    rootCmdr = root.RootCommand()
    ...
    return rootCmdr
}

func main() {
    if err := cmdr.Exec(buildCmds()); err != nil {
        logrus.Fatal(err)
    }
}

定义命令 Command

顶级的命令其实就是 RootCommand 的 子命令,所以:

    co := root.NewSubCommand().
        Titles("ms", "micro-service").
        Description("", "").
        Group("")

在这里,你可以做的定义基本上和 cmdr.Command 结构定义是相匹配的,所以你可以使用 OptCmd接口所支持的方法来完成一条命令的定义:

// OptCmd to support fluent api of cmdr.
// see also: cmdr.Root().NewSubCommand()/.NewFlag()
OptCmd interface {
    Titles(short, long string, aliases ...string) (opt OptCmd)
    Short(short string) (opt OptCmd)
    Long(long string) (opt OptCmd)
    Aliases(ss ...string) (opt OptCmd)
    Description(oneLine, long string) (opt OptCmd)
    Examples(examples string) (opt OptCmd)
    Group(group string) (opt OptCmd)
    Hidden(hidden bool) (opt OptCmd)
    Deprecated(deprecation string) (opt OptCmd)
    Action(action func(cmd *Command, args []string) (err error)) (opt OptCmd)

    // FlagAdd(flg *Flag) (opt OptCmd)
    // SubCommand(cmd *Command) (opt OptCmd)
    PreAction(pre func(cmd *Command, args []string) (err error)) (opt OptCmd)
    PostAction(post func(cmd *Command, args []string)) (opt OptCmd)
    TailPlaceholder(placeholder string) (opt OptCmd)

    NewFlag(typ OptFlagType) (opt OptFlag)
    NewSubCommand() (opt OptCmd)

    OwnerCommand() (opt OptCmd)
    SetOwner(opt OptCmd)

    RootCommand() *RootCommand
}

定义选项 Flag

对于每条命令,你都可以为其附着一系列的选项,这是通过 NewFlag 来完成的:

    co.NewFlag(cmdr.OptFlagTypeUint).
        Titles("t", "retry").
        Description("", "").
        Group("").
        DefaultValue(3, "RETRY")

类似的,所有 OptFlag 接口支持的方法都可以用在这里:

// OptFlag to support fluent api of cmdr.
// see also: cmdr.Root().NewSubCommand()/.NewFlag()
OptFlag interface {
    Titles(short, long string, aliases ...string) (opt OptFlag)
    Short(short string) (opt OptFlag)
    Long(long string) (opt OptFlag)
    Aliases(ss ...string) (opt OptFlag)
    Description(oneLine, long string) (opt OptFlag)
    Examples(examples string) (opt OptFlag)
    Group(group string) (opt OptFlag)
    Hidden(hidden bool) (opt OptFlag)
    Deprecated(deprecation string) (opt OptFlag)
    Action(action func(cmd *Command, args []string) (err error)) (opt OptFlag)

    ToggleGroup(group string) (opt OptFlag)
    DefaultValue(val interface{}, placeholder string) (opt OptFlag)
    ExternalTool(envKeyName string) (opt OptFlag)

    OwnerCommand() (opt OptCmd)
    SetOwner(opt OptCmd)

    RootCommand() *RootCommand
}

重复以上步骤

按照递归的定义方案,反复重复,你就可以得到一套完整的命令行界面定义了。

我得承认,这个方式避免了传统方式的结构嵌套问题,可读性上是要好很多的了。但它的问题也很明显,你需要在程序启动时额外消耗一点点 CPU 来完成上述定义指令的执行,相比而言,这比传统方式略微费事了 a little bit。但我还要承认,这个消耗,人是感受不出来的。

小结

流式接口并未带来任何新鲜东西。它只是改善了定义 Command Line UI 的友善性。

cmdr 同时支持两种方式以支持你的命令行参数定义。

版本计划和规划

v0.2.17:在这个版本中,我们计划做一系列 gocov 自测工作,以便扫荡此前功能性推进过程中的潜在隐患。在某些临界条件满足的场景下,cmdr 也许会工作的不令人满意,因此是时候自检一下下了。

新的版本很快就会发布以覆盖 v0.2.17 的一系列子版本。

总的来说,如无意外,我们遵循古老的传统,奇数版本号代码着 stable 发布。如果有,偶数版本属于临时性的、又或是试验性的发布。

更多情况下,我们会在奇数版本号上加以后缀以完成线上测试,例如 v0.2.17-rel01。它们往往是为了配合开源 CI/CD 而产生的。

如果我们有实验性的想法,那么通常会在 devel 的基础上展开特殊分支来进行测试。

参考

查看原文

赞 0 收藏 0 评论 0

hedzr 发布了文章 · 2019-05-30

cmdr 02 - 复刻一个 wget

cmdr 02 - Covered for wget

基于 cmdr v0.2.11

Getting Start 之后,我们来介绍如何用 cmdr 复刻一个 wget 的命令行界面,并具体介绍 CommandFlag 的各个细节以及 cmdr 能够做到哪些别人做不到的事。

此外,我们也声明一下,Getting Start ('另一个go命令行参数处理器 - cmdr') 的内容有了一些轻微的变化,因为这两周来,我们已经不停地增加了很多特性来完善 cmdr 的能力,期间有一些不恰当的策略、衍生的命名、采用的算法都有所调整,虽然尽力避免变化,但它是不可免的。我们是期望给你的编程界面越来越完美,让整个编写的流程流畅化,自然化。

wget 的参数

wget 本身是一个 GNU 应用程序。它的命令行参数有长有短,短参数可能有两个字符,此外参数被分为若干个分组。请看一部分截取:

这将是我们复刻的基准。

cmdr 都能做到些什么 - First

我们曾经做过多个应用,不同的开发语言,不同的目标,有的是练练手,有的是眼前有个事情有点烦、不好处理、一怒之下就干,有的是有特定的目的例如一个RESTful服务,等等。

所以,要想满足那么多的情况下命令行参数的组织和设定都能被很好地表示,不夸张地说,迄今数十年来,我们没有找到一个命令参数解释器能够完成这个任务。把时间限定在最近几年,把开发语言限定在 Golang,C++,Python 等几种之内,依然没有谁真的能这么称呼自己。现有的命令行参数解释器都有这样那样的不如意:

  • 短参数不能重复,哪怕是在多级命令结构下也必须全局唯一;
  • 不能分组;
  • 分组后顺序随机或者字母序,开发者无法干预,无法按照自己的意愿提供最好的顺序;
  • 短参数需要两个字母、或者三个字母的缩略语,更能表达参数原意时,基本上大多数现有的命令行参数解释器都废了;
  • 想要长参数显示为“--progress=TYPE”的式样,其中的 TYPE 还可以被复用;
  • 想要 git -m 的效果,结果费尽了力,终于实现了一个,然而受制于既有命令行解释器的结构,实现的坑坑洼洼的,自己都难以满意;
  • 想要和配置文件挂钩,没错挂钩了,然而需要写很多代码来安排;
  • 想要 /etc/program 加载配置文件,结果累了;想要 /etc/nginx/sites.avaliable 那样的效果,自己 watch 了,却合并不了新的配置到已经加载和构建好的配置中,也无法有效地通知应用的业务层按需取用新的配置条目;
  • 还有很多

遇到这些情况时,多数时候只能忍了,毕竟没有太多精力专门去搞参数问题,还有大把的业务需要去完成的对吧。

cmdr 选择和实现 wget-demo 也是为了展示自己大体上能够解决命令行参数处理的多数问题。不过和其它命令行参数的策略不同地在于:别人通常会对参数值的类型做很多文章,例如支持 string/int/slice/map 的多种式样,或者提供 validator,或者采用 Golang 结构 Tag 方式来挂钩参数类型处理器等等。但是 cmdr 在参数类型方面只能说有且够,整体的重心并不在这些方面。

cmdr 具有一个精悍短小的关键处理器 InternalExecFor(),它负责处理组合短参数的各种情况。

例如:对于 -1acg -t3 来说,cmdr 能够正确地识别到 -1 -c -c -g -t=3 的参数集合。

进一步地,对于 -4nva 来说,cmdr 能够正确识别到 -4 - nv -a 的参数集合。

此外,-mmsg -m msg -m=msg -m'what msg' -m"msg" '-mmsg' "-mWhat msg" 都是对的。在这里,cmdr处理了多数变形形态,有的形态则不必处理,因为 Shell 会负责处理其中一部分引号问题。

cmdr 也关注短参数的字母重复问题,在不同层级的子命令之间,你可以同时使用 -a 这样的短参数,当然,-a 仍然不能在子命令内重复,也不能和子命令的上层命令的参数相冲突。长参数以及别名都有同样的处理逻辑。

wget-demo 的实现细节

按照上一小节 cmdr 都能做到些什么 - First 提到的 cmdr 的专注点的说法,wget-demo 已经可以被很好地实现出来了。实际上,wget-demo 的代码非常简单(并不短),这也是 cmdr 想要给予开发者的方便。

这里 查阅 wget-demo 的目录。

这里 查阅 wget-demo 的单一代码文件。

main()

首先看 main:

func main() {
    logrus.SetLevel(logrus.DebugLevel)
    logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})

    // To disable internal commands and flags, uncomment the following codes
    cmdr.EnableVersionCommands = false
    cmdr.EnableVerboseCommands = false
    cmdr.EnableHelpCommands = false
    cmdr.EnableGenerateCommands = false
    cmdr.EnableCmdrCommands = false

    if err := cmdr.Exec(rootCmd); err != nil {
        logrus.Errorf("Error: %v", err)
    }
}

line 2,3可以被忽略,那是便于 cmdr 开发阶段的内容。发布后的 cmdr 也依赖于 logrus,但实际上这是因为 cmdr 的 examples 的原因,而 cmdr 自身是不做此依赖的,所以你还是可以自己选择 logger。logger 问题以后或许会被 cmdr 慎重考虑,彻底去除对任何 logger 的依赖。

line 5-10 是为了 wget-demo 专用的。因为 wget 没有命令和子命令,只有参数,因此 cmdr 内置的几个命令(组)被禁用了。

真正的代码,只有 line 12-14。无需解释。

rootCmd

所以你需要做的只是编排 rootCmd 结构。

var (
    rootCmd = &cmdr.RootCommand{
        Command: cmdr.Command{
            BaseOpt: cmdr.BaseOpt{
                Name: "wget",
                Flags: append(
                    startupFlags,
                    append(loggerFlags,
                        downloadFlags...)...,
                ),
            },
            SubCommands: []*cmdr.Command{},
        },

        AppName:    "wget-demo",
        Version:    wgetVersion,
        VersionInt: 0x011400,
        Header: `GNU Wget 1.20, a non-interactive network retriever.

Usage: wget [OPTION]... [URL]...

Mandatory arguments to long options are mandatory for short options too.`,
    }
)

rootCmd 包含一个 Command 嵌入结构。然后 rootCmd 包含 AppName, Version, Header 等等顶级宣告。看看 RootCommand 的定义:

type(
    // RootCommand holds some application information
    RootCommand struct {
        Command

        AppName    string
        Version    string
        VersionInt uint32

        Copyright string
        Author    string
        Header    string // using `Header` for header and ignore built with `Copyright` and `Author`, and no usage lines too.

        ow   *bufio.Writer
        oerr *bufio.Writer
    }
)

你可以编写自己的 CopyrightAuthor 字段,由 cmdr 为你构造 app 的 header 部分。你也可以单纯指定 Header 字段让 cmdr 原样输出。

为了复刻的更像一点,wget-demo 定制了 Header 字段。

此外,wget 的分组的参数选项,我们选择实现了前三组,因此你能看到 line 6-9 使用了一个 append 嵌套组合这三组参数集定义。

Command

rootCmd 包含一个 Command 嵌入结构,其定义为:

type(
    // BaseOpt is base of `Command`, `Flag`
    BaseOpt struct {
        Name string
        // single char. example for flag: "a" -> "-a"
        // Short rune.
        Short string
        // word string. example for flag: "addr" -> "--addr"
        Full string
        // more synonyms
        Aliases []string
        // group name
        Group string
        // to-do: Toggle Group
        ToggleGroup string

        owner  *Command
        strHit string

        Flags []*Flag

        Description             string
        LongDescription         string
        Examples                string
        Hidden                  bool
        DefaultValuePlaceholder string

        // Deprecated is a version string just like '0.5.9', that means this command/flag was/will be deprecated since `v0.5.9`.
        Deprecated string

        // Action is callback for the last recognized command/sub-command.
        // return: ErrShouldBeStopException will break the following flow and exit right now
        // cmd 是 flag 被识别时已经得到的子命令
        Action func(cmd *Command, args []string) (err error)
    }
    
    // Command holds the structure of commands and subcommands
    Command struct {
        BaseOpt
        SubCommands []*Command
        // return: ErrShouldBeStopException will break the following flow and exit right now
        PreAction func(cmd *Command, args []string) (err error)
        // PostAction will be run after Action() invoked.
        PostAction func(cmd *Command, args []string)
        // be shown at tail of command usages line. Such as for TailPlaceHolder="<host-fqdn> <ipv4/6>":
        // austr dns add <host-fqdn> <ipv4/6> [Options] [Parent/Global Options]
        TailPlaceHolder string

        root            *RootCommand
        allCmds         map[string]map[string]*Command // key1: Commnad.Group, key2: Command.Full
        allFlags        map[string]map[string]*Flag    // key1: Command.Flags[#].Group, key2: Command.Flags[#].Full
        plainCmds       map[string]*Command
        plainShortFlags map[string]*Flag
        plainLongFlags  map[string]*Flag
    }
)
Name

Name 暂时没有什么用处,目前你总是可以忽略它。将来,它可能被更好地用在文档输出方面。

Short, Full, Aliases

Short, Full, Aliases 无需再特别说明了,只是再强调一次,在上级命令的所有子命令中,它们不能重复。在多级子命令结构的不同层级中,没有这个限制,你可以比较宽泛地定义自己的命令和子命令集合。

PreAction, Action, PostAction

当命令被识别出来时,PreAction 被立即执行,此时,cmd.GetHitStr() 可以获得被命中的命令行参数中的命令字符串。你可以在这里建立 PreAction 逻辑,当特定条件不满足时,你的逻辑可以返回 cmdr.ErrShouldBeStopException 来通知立即退出。

ActionPostAction 的用法应该很明确,这里就不展开了。你对命令的实现逻辑通常应该总是利用 Action 字段来完成。

Command 的函数

Command 也包含一些类似于 GetHitStr() 的函数:

  • PrintHelp(justFlags bool):输出帮助屏。
  • PrintVersion():输出版本信息屏。
  • GetRoot() 直接访问到 rootCmd;如果想逐级回溯,通过 Owner 字段就可以了。
  • IsRoot() 帮助你测试是否到达了顶级命令。
  • HasParent() 帮助你测试是否还有 Owner/Parent。
  • ...
Group

Group 字段被用于命令分组。相同的字符串会被组织为一个命令组,显示的效果像这样:

如果你不指定Group,那么它们会被自动归属于一个名为 cmdr.UnsortedGroup 的特殊组中,图示中的 ms, s, t 都是这样的未指定分组,它们不会有组标题输出,而且总是被作为第一个被输出的分组。

如果你想要归属到 “Misc” 分组,那么你可以指定 Group 字段为 cmdr.SysMgmtGroup,其特殊之处在于总是被最后输出(v0.2.11及前可能存在不同的表现,下一版本会予以确认,但想要最后输出也很容易,稍后描述)。

对于分组谁先谁后,实际上有一个方案:指定你的Group字符串时使用两段结构“a.b”。a被用于排序,你可以使用字母和数字,例如:“001”,“011”,“091”等等。又或者:“A01”,“B01"等等。b被用作分组名并被用于显示。

ToggleGroup

ToggleGroup暂未实现,因为其功能可以暂时使用 PreAction 来代替。

since 0.2.13,ToggleGroup 已被移出 BaseOpt 结构,移入 Flag 中。

since 0.2.15 (待发布),ToggleGroup 已被实现。

Description,LongDescription

DescriptionLongDescription,是命令的描述性文字。你必须提供 Description 字段,在上面的图示中,它被显示在命令的后半段。如果你提供了 LongDescription ,它将会在命令的 --help 屏中被显示,另外,在 man page 或者文档输出中,LongDescription 也会被输出以便更细致地进行描述。

Examples

Examples 是命令的用例。实际上我们限定了用例的格式:

                    Examples:`
$ {{.AppName}} start
                    make program running as a daemon background.
$ {{.AppName}} start --foreground
                    make program running in current tty foreground.
$ {{.AppName}} run
                    make program running in current tty foreground.
$ {{.AppName}} stop
                    stop daemonized program.
$ {{.AppName}} reload
                    send signal to trigger program reload its configurations.
$ {{.AppName}} status
                    display the daemonized program running status.
$ {{.AppName}} install [--systemd]
                    install program as a systemd service.
$ {{.AppName}} uninstall
                    remove the installed systemd service.
`,

你必须按上述格式来提供 Examples 的具体内容。第一行以 $ {{.AppName}} 开头,然后是你的命令,如果是多级下的子命令,请注意补全,例如 $ {{.AppName}} ms tags list。然后第二行为上一行命令的功能性描述,不建议描述太冗长,也不建议描述被切分到多行。如是重复。

这样做的原因是为了在 man page 和文档输出时 cmdr 能够重组 examples 部分的格式令其更视觉化。

这是一个 man page 的部分截图,我们可以令其更视觉化,帮助最终使用者。

Hidden

如果你不想命令被显示在帮助屏、man page、文档中,使用 Hidden 字段来隐藏它。

Deprecated

如果你计划在下一某个版本废弃某个命令,可以使用 Deprecated 字段来标识它,你应该提供一个语义化的版本号到 Deprecated 中,至少在 Markdown 的文档输出中,它会被显示为删除线样式。

在 Terminal 中,deprecated 的命令显示为暗色。

DefaultValuePlaceholder, DefaultValue
适用于 Flag,不适用于 Command

DefaultValuePlaceholder 字段提供一个字符串 X,X 被连接在长参数之后用于显示目的,例如:--config=FILE。这是为了让参数的用法更具有表义性,也是为了强调参数为带值的。

注意为了提醒 cmdr 你需要一个带值参数,你必须明确设定 DefaultValue 字段为一个特定数据类型的值。你可以使用 string, int, string slice, int slice, duration 作为默认值。

如果是不带值的参数,它们总是具有 bool 类型的隐含值。如果你不指定 DefaultValue,那么 cmdr 认为你需要的是一个 bool 类型的不带值参数。

如果你在提供命令行参数是使用逗号分隔的字符串,而且为 DefaultValue 设定了 string slice, int slice 的话,那么 cmdr 会识别到并切分字符串转义为 Slice。稍后你在 Action 中可以使用 cmdr.GetStringSlice() 等方式直接抽取到数组。

DefaultValue 字段决定了 该参数的值的存储方式。但你可以自由地抽取该参数值到不同的数据类型,你可以通过 Get() 抽出该参数值的内部存储,然后自行转义为想要的类型。

since 0.2.13,DefaultValuePlaceholder 已被移出 BaseOpt 结构,移入 Flag 中。
Flags
since 0.2.13,Flags 已被移出 BaseOpt 结构,移入 Command 中。

命令的参数集被定义于此。

SubCommands

对于命令来说,多级命令能够构成一个结构化的层次,不仅便于用户索引和记忆,也有利于业务逻辑的构建和编写。

嵌套多级的子命令可能会很冗长,因此实际编码过程中,你可以考虑拆分并独立定义子命令,并在父命令中组合它们。

TailPlaceHolder

对于命令来说,在 Usage 行的显示也需要被 meaningful。如果你有这样的需要,那么 TailPlaceHolder 字段可以在 Usage 行的正常输出之外额外嵌入一段文字。

对于 TailPlaceHolder="<host-fqdn> <ipv4/6>" 来说,显示的效果是这样的:

应该不需要更多解释了,这个用文字表达我需要首先给出一堆术语释义才行,就不骗字数了。

Flag

参数,选项,都是 Flag 的同义语。cmdr 在代码实现时选用了 Flag 这个单词而已。

除了在 Command 中已经描述过的 术语两者都有的字段之外,这一小节描述其它部分,尤其是 Flag 特有的部分:

ToggleGroup

参考 Command 中有关小节的描述。虽未实现,但这个字段可以干点什么,将来吧

DefaultValuePlaceholder

参考 Command 中有关小节的描述。自 cmdr v0.2.13 起,经过代码 review,这个字段正式移入 Flag 中,因为这才是正确的逻辑归属点。

DefaultValue

参考 Command 中有关小节的描述。嗯,它本来就设计在 Flag 中,难怪以前写 demo 时感觉怪怪的,DefaultValuePlaceholder 写在一处,DefaultValue 又写在另一处。今后就是一家人了。

ValidArgs

尚未实现。暂时也没考虑。原来的意图是提供枚举文字量。可是大家都是写代码的,不如就 1,2,3 将就了吧先。

Required

未用。实际上 cmdr 没有校验的概念,也没有必须存在这种概念。

因为我们觉得,你不应该要求用户一定要提供一个什么。

比如 consul 集群在哪里呀?consul 集群当然是在 consul.ops.local 那儿啊,要不然你们家云设施架构师设计的不一样,那么它就在 registrar.prod.ashiley.org.local 啊。换句话说,你总是应该给参数一个默认值,甚至给它 nil 或者 ”“ 也可以,你的业务逻辑应该处理一下这些临界场景。

尽管我们设计了 cmdr 以帮助你建立完善的 Command Line UI,但让用户随时随地能省缺就省缺才是正确的。

ExternalTool

这个字段的用途,首先是实现 git commit -m 效果。

为了达到效果,你必须在 ExternalTool 中填写 ”EDITOR“ 字符串,又或者使用 cmdr.ExternalToolEditor 常量。

本质上,cmdrExternalTool 视为环境变量名,试图探查环境变量是不是存在,并取得该值作为执行文件X,然后采用一个临时文件T作为执行文件X的输入参数并就地执行它们,待用户操作完毕并关闭执行文件X之后,临时文件T的内容被当做文本并被作为选项值填入。

所以,git commit -m 就是这么干的,cmdr 复制了这个流程。如果你需要类似的逻辑,那么就可以借助于 ExternalTool 字段。

组织

依据上面各小节的对 RootCommand,Command,Flag的阐述,接下来就是具体的数据集的定义了。

我们已经提到过嵌套结构的烦恼并做出了建议,至于更好的数据集定义方案,继续改善吧,欢迎给我建议。

小结

那么现在,你已经可以构建出你的 Command Line UI 了。wget-demo 已经实现了三组参数集,不但能够被正确识别,显示的效果也还不错:

如果希望对命令行参数的解释和操作有更多便利,欢迎 Issue 到:

https://github.com/hedzr/cmdr

REF

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 14 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-02-14
个人主页被 320 人浏览