总结下平时积累的 docker 使用经验与技巧

国内安装使用 docker

相比在线安装,这里推荐使用国内源下载离线安装包进行安装,但也可以通过国内镜像源加速在线安装。

在线安装

官方文档的 docker 安装 如果较慢的话,可以使用国内的镜像进行加速:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg2 \
    software-properties-common

# 这里把源替换为国内的源
$ curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/debian/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

使用离线安装包

如 Ubuntu 16 代号是 xenial,就可在 https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/dists/xenial/pool/stable/amd64/ 下载 containerd docker-ce-cli docker-ce 三个 deb 离线包,对于不同的版本替换上面 url 中的关键字即可。

之后使用 sudo dpkg -i 安装三个包(最后安装 docker-ce),之后使用 sudo systemctl start docker 启动即可。

如果不确定版本之间的关系。可以先安装 docker-ce,会提示失败然后显示其依赖的版本要求。

使用预编译文件安装

这里并不推荐使用 https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/static/stable/x86_64/docker-19.03.5.tgz 链接下载可执行文件然后自己编写 docker 的 systemd 启动项,该方法过于繁琐且容易出错,这里就不叙述了。

国内 docker 镜像源

下载 docker 镜像时,使用默认的 Docker Hub 可能有点慢,可以使用国内的镜像源,修改 /etc/docker/daemon.json 文件(如果没有该文件可先 touch 一个),在 registry-mirrors 里添加内容:

1
2
3
4
5
6
7
{
  "registry-mirrors": [
    "https://dockerhub.azk8s.cn",
    "https://reg-mirror.qiniu.com",
    "http://hub-mirror.c.163.com"
  ]
}

上面的是国内的几个有用的镜像源,除此之外还可以使用 Redhat 的 Quay.io,不过这个是单独的 registry 而不是 docker hub 的镜像,所以资源可能少点。修改完后注意 sudo systemctl restart docker 重启服务,之后使用 docker info 检查是否添加上了。

开放 docker daemon 端口

原有的 docker daemon 使用的是 unix socket 即 unix:///var/run/docker.sock 进行 RESTful 接口的交互。为了方便二次开发和平时测试,需要将其开放为 TCP 端口的方式。这里介绍修改启动参数和使用 nginx 转发两种方式。

对于修改启动参数,不同的 linux 的 init 系统有着不同的方式,对于 Ubuntu 16 来说其 init 为 systemd,开放端口分下面几个步骤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 1. 新建文件
$ sudo mkdir /etc/systemd/system/docker.service.d
$ sudo vim /etc/systemd/system/docker.service.d/startup_options.conf

# 以下为需要粘进去的内容
[Service]
# 必要步骤需要先清空
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2376

# 2. 重启 systemd 和 docker.service
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker.service

# 测试一下端口是否开放成功
$ curl http://0.0.0.0:2376/info

nginx 转发 socket

也可以使用 nginx 作为转发,暴露 docker daemon 的接口,该 nginx 最方便的是作为容器运行,其 Dockerfile 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
FROM nginx:stable

RUN echo 'user  root;\n\
        worker_processes  1;\n\
        error_log  /var/log/nginx/error.log warn;\n\
        pid        /var/run/nginx.pid;\n\
        events {\n\
            worker_connections  1024;\n\
        }\n\
        stream {\n\
            server {\n\
                listen 80;\n\
                proxy_pass unix:/var/run/docker.sock;\n\
            }\n\
        }\n' \
    > /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

之后使用下面的命令构建和启动和测试该容器:

1
2
3
$ docker build . -t docker-socket-proxy
$ docker run -d -p 8376:80  -v /var/run/docker.sock:/var/run/docker.sock docker-socket-proxy:latest
$ curl http://0.0.0.0:8376/info

相比直接修改启动文件,可避免 docker daemon 重启,同时该容器作为 service 运行时,可以通过域名直接访问,这样依赖于 docker api 的服务在部署时候就不必部署在 manager 节点之上且强耦合与管理节点的 ip,而是直接通过域名的方式访问部署在 manager 节点上的 docker-socket-proxy 服务,实现任意节点的部署。

docker-socket-proxy 的使命是将 unix socket 暴露成 tcp ,docker api 虽然是 http 的协议,但是 nginx 没有必要去使用该层次信息,作为 tcp 透传即可(nginx 1.9 以上支持)。所以在 nginx.conf 配置里将 http 配置块省去,并直接添加上 tcp,修改原来的 www-data user 为 root 以便访问 unix socket 文件。

如果要用到 http 层的信息的话,自己改写 nginx 配置文件使用 http 转发即可,不过需要注意的是:对于 docker api 中的 WebSocket 接口和有 Transfer-Encoding: chunked 头的 HTTP 请求(如日志查看)来说需要特殊处理下。

nginx 代理 socket

首先给出 nginx http 代理 docker daemon socket 到 /api/docker/ 下的配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
upstream docker {
    server unix:/var/run/docker.sock fail_timeout=0;
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
    listen 80 default_server;

    location /api/docker/ {
        # 或者直接 proxy_pass http://unix:/var/run/docker.sock;
        proxy_pass http://docker/;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        proxy_read_timeout 1h;
    }
}

WebSocket proxying 需要显式的说明,nginx 才会处理。但是对于不是 ws 的接口怎么办?

在 nginx 的文档当中可以找到使用 map 的方式定义变量,根据客户端请求中 $http_upgrade 的值,来构造改变 $connection_upgrade ,参考 Nginx 支持 WebSocket 反向代理-学习小结

上面 nginx 的官方文档里提到默认的 proxy_read_timeout 是一分钟,如果没有读写动作的话会自动断开,这对对于 log 的 Transfer-Encoding: chunked 头 HTTP 请求来说也是一样的,文档里建议设置周期的 ping frames 去激活连接,但是对于 log 和 shell 来说的话不适用,所以这里就直接配置为 proxy_read_timeout 1h,超过一小时没有读写才断开。

docker-cli 连接远程的 dockerd

可以使用命令 docker -H 127.0.0.1:2576 ps 的方式使用本机的 docker-cli 访问其他开放的 dockerd(docker daemon),之后 alias 一下,可以更方便的使用。

或者是修改环境变量 DOCKER_HOST 以变更 docker-cli 的默认 dockerd。

对于 windows 来说,如果不想在本地安装 docker,而是想在本地使用 docker 命令即 docker-cli 连接到远程的 docker 可以在 win 的包管理 choco 工具下搜索到 Docker CLI 然后安装,或者是直接在 docker-cli-builder 下载他人编译好的,之后同 -H 参数或者 DOCKER_HOST 环境变量指定远程 dockerd,这样就可以在 win 下的 cmd 或 powershell 中使用 docker 命令了。

docker 命令

如果使用的 ohmyzsh,可在 ~/.zshrc 里面的 plugin 项里加入 docker 插件以提高补全率,同时将当前用户加入 docker 组,这样使用 docker 时不用加 sudo:

1
2
3
$ sudo groupadd docker
# 重新登录终端生效
$ sudo usermod -aG docker ${USER}

其他有用的命令如下:

  • docker ps

    • 一行太长可以使用 docker ps -a| less -S 滚屏查看
    • docker ps -s 可以看见容器大小
    • docker ps --no-trunc 完全展开信息
    • docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}" 定义输出信息
  • 删除所有的镜像和容器,其中的 -q 参数是只显示 id 列表,达到迭代删除的目的

    • docker image rm $(docker image ls -q)
    • docker container rm $(docker ps -q)
  • 根据 docker-compose 去删除容器

    • docker -f xxxx.yml stop
    • docker -f xxxx.yml rm
  • 查看容器返回值 docker inspect ID --format='{{.State.ExitCode}}'

  • 查看服务日志 docker service logs -f --tail 10 ServerName

使用 curl 命令创建 service

可以直接利用 curl 调用 dockerd 进行一些操作,比如可以 inspect 一下已有的 service 的配置粘贴到文件内,然后 curl 去创建,这样便于使用脚本批量的根据文件创建 docker 资源。如下面的 nginx service 的 nginx.json 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "Name": "nginx",
  "TaskTemplate": {
    "ContainerSpec": {
      "Image": "nginx:stable",
      "Env": [
        "TZ=Asia/Shanghai"
      ]
    }
  },
  "Mode": {
    "Replicated": {
      "Replicas": 2
    }
  },
  "EndpointSpec": {
    "Mode": "dnsrr"
  },
  "Labels": {
    "test": "nginx"
  }
}

可以由下面的命令创建:

1
2
3
4
5
curl --unix-socket /var/run/docker.sock \
        http:/services/create \
        -H "Content-Type: application/json" \
        --request POST \
        -d @nginx.json

减小镜像体积

  • 对于 debian 和 pip 清除安装后的缓存

    • apt-get 需要 rm -rf /var/cache/apt/* && rm -rf /var/lib/apt/lists/*
    • pip 需要 rm -rf ~/.cache/pip
  • 如 alpine 的镜像,单独安装 build 依赖包,并在之后清除

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
RUN apk --no-cache add --virtual build-dependencies \
      build-base \
      py-mysqldb \
      gcc \
      libc-dev \
      libffi-dev \
      mariadb-dev \
      && pip install -qq -r requirements.txt \
      && rm -rf .cache/pip \
      && apk del build-dependencies

RUN apk -q --no-cache add mariadb-client-libs
  • 对于如 Spring Boot 的 fat jar,里面包含了很共有的依赖,这时候可以使用 Google 的 maven 插件 Jib 合理的将 fat jar 分散到不同的镜像层。

备份与还原

镜像导出与导入:

1
2
$ docker save openjdk:8-jre-stretch | gzip > openjdk.8-jre-stretch.tar.gz
$ zcat openjdk.8-jre-stretch.tar.gz | docker load

Volume 备份与还原 docker 没有提供命令,但是可以通过运行一个容器挂载需要备份的容器,然后将其打包,还原时候再逆操作一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 设置备份 volume 到 VOL,设置备份版本到 BKTAG
$ VOL=mysql-data
$ BKTAG=untag

# 备份文件打包到当前文件夹
$ docker run --rm -v $(VOL):/volume -v $(PWD):/backup alpine \
        tar cf /backup/$(VOL)-$(BKTAG).tar -C /volume ./
# 恢复备份
$ docker run --rm -v $(VOL):/volume -v $(PWD):/backup alpine \
        sh -c "rm -rf /volume/* /volume/..?* /volume/.[!.]* ; tar -C /volume/ -xf /backup/$(VOL)-$(BKTAG).tar"

Docker 实践技巧

Nginx remote ip 不正确

docker 通过 Nginx 负载均衡时候,容器内获取到的 HTTP 协议的 remote ip 是不正确的,参考 解决办法是可以将 /etc/default/docker 添加 DOCKER_OPTS="--userland-proxy=false"

ENTRYPOINT 中的环境变量

对于 java 类的镜像,在 dockerfile 中一般会定义 ENV JAVA_OPTS="" 这样的环境变量,然后在 ENTRYPOINT 里作为 JVM 启动参数。

但是,如 ENTRYPOINT exec java $JAVA_OPTS -jar /app.jarENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /my.jar"] 的方式设定环境变量给 JVM 都是错误的。

正确的方式是 ENTRYPOINT ["/bin/bash", "-c", "java $JAVA_OPTS -jar app.jar"]

时区设置

以 stretch (Debian) 为基础镜像的可以以 TZ=Asia/Shanghai 环境变量指定时区,这样可以在运行时候(docker run, compose)指定时区,或者在 Dockerfile 里直接 ENV TZ=Asia/Shanghai

其他基础镜像可使用挂载本地时区文件的方式 /etc/localtime:/etc/localtime:ro 来完成。

其他技巧

docker-compose 里面定义一个直接从文件系统挂载的 volume:

1
2
3
4
5
6
7
volumes:
    mysql-data:
    driver: local
    driver_opts:
    type: none
    o: bind
    device: /var/lib/mysql