Docker

  • https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html
  • https://cheatsheetseries.owasp.org/cheatsheets/NodeJS_Docker_Cheat_Sheet.html

可以对容器开启内存限制, 内存达到限制的程序会因为OOM被kill.
启用内存限制首先要求Linux内核开启相关功能, 启用内存限制后无论Docker是否运行, 系统的性能都会下降约10%.

容器可以使用的最大内存量, 最小值为4m(MB)

允许此容器交换到磁盘的内存量, 该选项仅在设置 --memory 时才有意义.

容器可以使用与memory设置同等大小的swap.
--memory=300m 时, 容器可以使用 300m 的swap.

意为物理内存和swap的总和.
--memory=300m--memory-swap=1g 时, 容器可以使用300MB的内存, 700MB的swap.

意为容器无权访问swap.

意为忽略该项设置.

允许使用无限的swap.

对于内存的软限制, 其值必须低于 --memory (硬限制),
该设置无法保证内存不会超过此值, 在一些情况下可以超出此负载.

该值本身是cgroup的memory.soft_limit_in_bytes选项的包装,
其作用是在内存不足时, 将一些进程的内存用量退回至此值(因为Linux在默认情况下会尽可能多的使用内存).

默认情况下, OOM会终止容器进程, 此设置可以关闭此默认行为.

cgroup v1的内核内存限制已经在Linux kernal 5.4中被弃用, cgroup v2不提供此功能, 因此可以认为此功能已不存在.

docker-compose up -d 根据当前目录下的docker-compose.yml文件启动容器
docker-compose down 根据当前目录下的docker-compose.yml文件停止容器

默认情况下, docker-compose会以当前目录的名字作为项目名.
通过 --project-name 可以手动指定项目名, 这在进行蓝绿部署等操作时很有用.

docker-compose.yml文件有两个主要版本, 2.x和3.x.
其中 3.x版本是专为Docker Swarm设计的, 其中一些功能只有在集群模式下可以使用(例如内存限制),
为了在单机情况下使用这些功能, 用户可能会想要回退到2.x版本.

在Docker Compose 1.27.0以上版本引入的新格式, 取代了过去的版本2和版本3.

在过去的实现中, 支持特定版本号的文件并不相当于支持该版本的所有功能(不支持的功能会出现警告提醒用户),
因此Compose Specification决定舍弃使用版本号的做法.

env_file只会在镜像启动时加载, 不会在镜像构建过程中加载.

env_file与docker-compose命令时读取的同目录下的 .env 文件无关(.env 文件的路径不能自定义).

参考:

  • https://stackoverflow.com/a/39548957/5462167
  • https://github.com/docker/compose/issues/6581#issuecomment-540638939

在docker-compose里使用volumes时, docker-compose.yml所在目录的名称如果改变将会生成新的volume.
由于volume不能重命名, 改变目录名会带来很多麻烦.
改善此问题的最明显的替代方案: 使用bind mounts, 很可能会引入文件权限相关的问题.

因此, 务必在使用docker-compose时将volume以external的形式绑定, 以保证名称不会改变.

docker-compose支持使用多个compose文件,
默认会使用当前目录下的 docker-compose.ymldocker-compose.override.yml 文件.

在有多个compose文件的情况下, 排在后面的文件会根据规则覆盖掉之前文件的配置项.

此特性有时也被用于将大docker-compose分解为数个小docker-compose, 以便维护,
但不推荐这种做法, 因为之后可能出现添加同名容器导致意外覆盖的情况.

https://docs.docker.com/compose/extends/#adding-and-overriding-configuration

单值项将替换掉已有项目, 多值项将合并进已有项目.

# docker-compose.yml
# 用于生产
web:
image: example/my_web_app:latest
depends_on:
- db
- cache
db:
image: postgres:latest
cache:
image: redis:latest
# docker-compose.override.yml
# 用于开发
web:
build: .
volumes:
- '.:/code'
ports:
- 8883:80
environment:
DEBUG: 'true'
db:
command: '-d'
ports:
- 5432:5432
cache:
ports:
- 6379:6379

用Go重写的docker-compose.

Docker Registry使用HTTP基本身份验证, 因此只需要在反向代理里加上HTTP验证就可以作为私有registry服务器使用.

docker login docker.registry.example.com 登录私服
docker pull docker.registry.example.com/ubuntu 从私服上拉取镜像

docker tag ubuntu docker.registry.example.com/ubuntu 为镜像添加私服tag
docker push docker.registry.example.com/ubuntu 将添加tag后的镜像推至私服

registry有一个garbage-collect命令, 该命令可以回收所有未引用的镜像.
在早期版本里, 将镜像标记为未引用本身需要由用户手动来完成, 后来增加了 --delete-untagged 选项,
从而省去了这一步骤.

docker exec registry \
bin/registry garbage-collect /etc/docker/registry/config.yml --delete-untagged
curl --verbose \
--request DELETE \
http://host:port/v2/${image_name}/manifests/${digest}

Docker自18.09引入的新构建工具, 通过将环境变量设置为 DOCKER_BUILDKIT=1 开启.

这两者之间的关键区别:

  • volumes的文件系统与宿主环境是分离的: 新建的volume总是会适应容器的权限, 数据迁移/备份总是以打包的形式进行.
  • bind mounts的文件系统就是宿主环境: 文件权限对宿主系统来说会比较怪异, 数据迁移/备份时可能会受权限困扰.

https://github.com/lavie/runlike/

docker save --output {filename}.tar {image_name}
docker save {image_name} > {filename}.tar
docker save {image_name} | gzip > {filename}.tar.gz

docker load < {filename}.tar.gz
docker load --input {filename}.tar

数据卷的默认路径为 /var/lib/docker/volumes/{volume_name}/_data

docker volume craete {volume_name} 创建数据卷
docker volume inspect {volume_name} 查看数据卷信息
docker volume ls 列出全部数据卷
docker volume rm {volume_name} 删除数据卷

docker run --rm -it -v volume:/data ubuntu /bin/bash 启动临时容器访问数据卷

docker run --rm -v {volume_name}:/data -v $(pwd):/backup ubuntu cp -r /data /backup
数据卷内的数据将保存为当前目录下的backup文件夹.

docker run --rm -v {volume_name}:/data -v $(pwd):/backup ubuntu /bin/bash -c 'cp -r /backup/* /data'
当前目录下的数据将被复制到数据卷内.

docker run --rm -v {volume_name}:/data ubuntu chown -R 1000:1000 /data
将数据卷的所有权转给1000:1000

在docker run时使用此标志可以将容器加载的所有卷以相同的目录结构加载至新的容器里.
这种特性恰好是这个标志没有什么意义的原因: 因为需要提前了解被加载容器使用的卷路径.

/var/lib/docker 下的内容原则上是由Docker全权管理的, 不应该由任何外部方式改变.
Docker缺乏内置的改变数据卷保存路径的能力.

可以用符号链接将 /var/lib/docker/volumes/ 连接到用户需要的位置.
这种方式破坏了与Docker的约定, 因此实际上不推荐使用.

该方案的主要缺点是无法删除卷, 因为Docker认为卷不在正常的路径下:
https://github.com/moby/moby/issues/39446

Docker有一个名为 --data-root 的启动参数, 改变此参数会改变 /var/lib/docker 的位置.

该方案的主要缺点是包括image在内, 所有Docker数据的位置都会改变, 缺乏灵活性.

docker cp {local_file} {container_id}:{remote_file} 将文件从宿主系统复制到容器内
docker cp {container_id}:{remote_file} {local_file} 将文件从容器内复制到宿主系统

docker rm $(docker ps -qa) 删除全部容器

docker exec -it {container_id} bash 打开容器的bash(如果容器的CMD是 /bin/sh, 那么也可以直接执行命令)

docker logs {container_id} 查看容器日志
docker logs -f {container_id} 同步打印容器日志
docker logs --tail {number} {container_id} 查看容器最后number条日志

自1.25版本起可用以下命令一次性清除所有停止的容器, 未使用的网络, 未使用的镜像和编译缓存:
docker system prune --all

docker volume prune 删除所有未被container使用的数据卷

docker images -f dangling=true 列出所有dangling镜像
<<dangling>>镜像指的是缺乏tag的镜像.
docker image prune 删除所有dangling镜像

docker image prune -a 删除所有没有container运行的镜像

docker stats --no-stream 查看所有容器的资源使用情况(没有no-stream会不断输出新数据)

docker system df --verbose 显示详细的磁盘空间使用情况

分层文件系统的层如果扩容了镜像(例如安装编译工具), 则无法在之后的层里通过删除文件缩小镜像大小.

出于这个理由, 人们需要在尽可能少的RUN语句里完成镜像的构建, 因为每次RUN都会导致新的层.

使用多阶段构建可以减去构建时期所需依赖占用的空间.
使用squash选项可以减小最终生成的镜像大小.

多阶段构建被用来移除那些只在构建时期需要的依赖.
原理是将构建分成两个阶段进行:

  • 第一阶段 构建镜像完成构建
  • 第二阶段 最终镜像直接从构建镜像里复制构建成果, 从而避免扩容最终镜像.
FROM rust:1.52-slim AS builder
WORKDIR /usr/src/app
COPY . .
RUN cargo install --path .
FROM debian:buster-slim
COPY --from=builder /usr/local/cargo/bin/app /usr/local/bin/app
ENTRYPOINT ["app"]

使用squash要求docker daemon开启experimental选项.
通过 docker info 来检查daemon是否开启了experimental.
在构建时启用 --squash 会在构建结束时将分层文件系统压缩为一层.

这可以减小最终生成的容器镜像大小.

https://github.com/wagoodman/dive

一个用于查看分层结构的工具, 可以用来定位导致镜像大小增加的层.

ONBUILD指令将特定指令的执行j延迟到继承此Dockerfile的子Dockerfile构建的时候.
ONBUILD的常见场景是复制当前文件夹下的文件或安装依赖项,
这样子Dockerfile就无需自己复制文件和安装依赖项, 不必过多了解父Dockerfile的目录结构.

不应该在Dockerfile里使用VOLUME语句,
因为VOLUME会导致 docker run 一定产生持久化的副作用(即创建一个匿名volume),
而用户很可能对此毫不知情, 进而无意义地吞占用户的硬盘空间.

如果用户需要用数据卷实现持久化, 那么用户应该手动创建卷, 或指定存储的位置.

ADD早于COPY出现, 因此ADD承载了许多与复制本地文件无关的功能(例如复制远程文件).
在只需要复制本地文件时, 应优先使用COPY, 因为它的功能定义更清晰, 被官方推荐.

CMD早于ENTRYPOINT出现, ENTRYPOINT的出现是因为人们要求自定义入口点,
否则每个CMD命令都需要包含相同的内容.
ENTRYPOINT将作为容器的入口, 即执行 docker run 时运行的程序,
docker run 后添加的参数也会一并加入到容器入口之后.
Docker的默认入口点为 /bin/sh -c, 这就是为什么CMD命令可以直接输入shell命令(例如ls).

ENTRYPOINT只能有一条, 而CMD可以有多条.
当base镜像存在CMD时, ENTRYPOINT会将CMD清空.

ENTRYPOINT有两种形式, exec形式和shell形式, 两者行为不同:

  • exec形式(推荐): ENTRYPOINT ["exec_entry", "p1_entry"]
    该形式下, 会受CMD影响, CMD命令会被附加到ENTRYPOINT命令的末尾.
  • shell形式: ENTRYPOINT exec_entry p1_entry
    该形式下, 不会受CMD影响.

由于K8s的存在, Docker Swarm被广泛认为正接近生命终结, 不具有投资价值.

Docker Swarm可以和Docker一同使用, 但是Docker Swarm节点只能由Docker Swarm管理.

Docker Swarm分为Manager节点和Worker节点:

  • Manager: 相当于K8s的主节点, 负责调度整个集群, 但在非独占模式下可以作为Worker节点使用.
  • Worker: 相当于K8s的工作节点, 运行容器.

Task包含一个Docker容器和容器内运行的命令, 是Docker Swarm的原子调度单位.

Service是决定task使用的容器镜像和需要在容器内运行的命令, manager通过服务创建task.
Replicated Service是有副本的服务, 在这个服务里会启用指定数量的task副本.

Routing Mesh建立了一种网状网络, 在集群中的任意一个节点上, 都可以访问到已经发布的服务.

Docker Swarm从1.13起支持绕过Routing Mesh, 以直接在Swarm节点上公开服务
(每个节点公开的服务端口都是独立的, 如果不明确指定, 那么每个节点都会使用各自随机的端口号),
这种方法提供了更大的灵活性, 但需要用户手动实现负载均衡等基础应用, 通常不会使用.