Docker学习笔记——进阶篇

cuixiaogang

《Docker学习笔记——基础篇》已经记录了容器与镜像的核心概念、安装、基本命令、docker rundocker inspect的详细用法。这篇接着往下,把日常开发和部署中绑定更紧的几块记下来:网络、数据卷、Dockerfile构建镜像、镜像分发,以及用Compose编排多容器服务。


网络

容器默认各自有独立的网络命名空间,彼此隔离。但实际用起来,容器之间要互相通信(比如应用连数据库)、容器要对外提供服务(比如把端口映射到宿主机),这些都靠Docker网络打通。Docker启动时会自动建一个名为docker0的虚拟网桥,默认情况下容器都接在它上面。

网络模式

Docker通过网络驱动(driver)提供几种网络模式,常用的几种如下:

模式 驱动 说明 适用场景
bridge bridge 默认模式,容器接到虚拟网桥docker0,通过NAT与外界通信 单机多容器,最常用
host host 容器直接共享宿主机网络栈,不做网络隔离,容器端口即宿主机端口 追求网络性能、不在意隔离
none null 只保留lo回环,没有任何对外网络 完全不需要网络的容器
container - 复用另一个容器的网络命名空间,两者共享同一IP和端口 边车(sidecar)、调试
overlay overlay 跨主机的容器网络,让多台机器上的容器互通 Swarm、集群
macvlan macvlan 给容器分配独立MAC,使它像物理机一样直接出现在局域网 容器需独立IP接入物理网络

--network指定模式,不写默认就是bridge:

1
2
3
docker run -d --network host nginx          # host模式,直接用宿主机网络
docker run -d --network none busybox # 无外部网络
docker run -d --network container:web app # 复用web容器的网络

入门阶段最常打交道的是bridge,host和none偶尔用;overlay、macvlan等到集群或特殊组网时再深入即可。

默认 bridge 与自定义 bridge

同样是bridge,Docker自带的docker0和自己docker network create出来的自定义网桥,有一个关键差别——容器之间能不能用名字互相访问

对比项 默认bridge(docker0) 自定义bridge
容器名DNS解析 不支持,只能用IP互访 支持,直接用容器名访问
创建方式 Docker自带,无需创建 docker network create
网络隔离 所有默认容器都在同一网段 不同自定义网络之间互相隔离
互访方式 老式--link(已不推荐) 同网络内开箱即用

说白了,自定义bridge内置了DNS,容器A可以直接用容器B的名字连它,不用关心对方IP重启后会不会变。这也是官方推荐用自定义网络、而非默认bridge加--link的主要原因。

docker network 常用命令

命令 作用
docker network ls 列出所有网络
docker network create mynet 创建网络(默认bridge驱动,可加-d overlay等指定驱动)
docker network inspect mynet 查看网络详情(网段、网关、已连接的容器等)
docker network connect mynet web 把已运行的容器web接入mynet
docker network disconnect mynet web 把web从mynet断开
docker network rm mynet 删除网络(需先断开其上所有容器)
docker network prune 清理所有未被使用的网络

docker network ls默认能看到三个内置网络:bridgehostnone,分别对应前面三种模式,它们删不掉。

查看网络列表
查看网络列表

一个容器还可以同时接入多个网络——用docker network connect把运行中的容器再挂到别的网络上,常用于让某个容器既能在内网通信、又能访问另一组服务。

数据卷

容器的文件系统由镜像的只读层加上层一个可写层组成,数据默认写在这个可写层里。可写层和容器同生共死——容器一删,里面攒下的数据就全没了。所以但凡要持久化数据(数据库文件、上传的内容)、或者要在宿主机与容器之间、容器与容器之间共享文件,就得把数据放到可写层之外,这就是挂载(mount)要解决的事。

数据卷
数据卷

挂载点之下的数据不进容器可写层,而是落到独立的卷或宿主机目录里,所以删容器不影响它,新容器挂上去就能接着用。

三种挂载方式

方式 数据位置 由谁管理 典型场景
volume(卷) /var/lib/docker/volumes/<卷名>/_data Docker 持久化生产数据(如数据库),首选
bind mount(绑定挂载) 宿主机任意指定路径 用户自己 开发时挂源码、配置文件进容器
tmpfs mount 宿主机内存,不落盘 Docker 临时、敏感数据,容器停止即消失(仅 Linux)

三者里 volume 是官方最推荐的——位置由 Docker 统一管理、不依赖宿主机目录结构、可移植,也能用 docker volume 命令成体系地管理。bind mount 胜在直接,想改宿主机上哪个文件就挂哪个,开发期最顺手。

volume 与 bind mount 的区别

这两个最常用,也最容易混,单独拉出来对比:

维度 volume(命名卷) bind mount(绑定挂载)
宿主机路径 由 Docker 决定,固定在 volumes 目录 自己指定任意绝对路径
可移植性 强,换台机器照样建同名卷 弱,依赖宿主机有对应目录
初次挂载时 若卷为空且容器内目标路径已有数据,会把镜像里那份数据复制进卷 直接用宿主机目录覆盖,容器内原有内容被遮盖
路径不存在时 卷不存在会自动创建 -v 自动把宿主机路径建成目录;--mount 直接报错
适用 生产数据持久化 开发挂代码 / 配置

那条「volume 为空时复制镜像数据、bind mount 直接遮盖」是最容易踩的坑:用命名卷挂 MySQL 数据目录,首次能拿到镜像里初始化好的库;换成 bind mount 挂一个空目录,容器里那份初始数据就被空目录盖掉了。

挂载语法

有两种写法,-v 简写和 --mount 显式,效果基本一致,--mount 更啰嗦但语义清楚、脚本里更不易错。

写法 示例 说明
-v 卷名:容器路径 -v mydata:/var/lib/mysql 命名卷
-v 宿主机路径:容器路径 -v /home/data:/service/data bind mount(路径以 / 开头)
-v 容器路径 -v /data 匿名卷,Docker 随机起名
结尾加 :ro -v mydata:/data:ro 容器内只读
--mount type=volume,... --mount type=volume,src=mydata,dst=/data 显式命名卷
--mount type=bind,... --mount type=bind,src=/home/data,dst=/data,readonly 显式 bind,可加 readonly
--mount type=tmpfs,... --mount type=tmpfs,dst=/tmp/cache 挂内存盘
1
2
3
4
5
6
7
8
9
# 命名卷:把MySQL数据持久化到名为 mysql_data 的卷
docker run -d --name db -v mysql_data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8

# 删掉容器,卷还在,数据不丢
docker rm -f db
docker run -d --name db2 -v mysql_data:/var/lib/mysql mysql:8 # 接着用原来的数据

# bind mount:开发时把宿主机代码目录挂进容器,改完即时生效
docker run -d -p 8080:80 -v /home/cuixiaogang/www:/usr/share/nginx/html:ro nginx

匿名卷(-v /data 不给名字)要留意:Docker 会生成一长串随机名的卷,容器删了卷常残留下来,时间长了攒一堆没人认领的卷。docker run --rm 退出时会顺手清掉容器挂的匿名卷,但不会动命名卷——命名卷得自己显式删。

docker volume 常用命令

命令 作用
docker volume create mydata 创建命名卷
docker volume ls 列出所有卷
docker volume inspect mydata 查看卷详情(含宿主机上的真实路径 Mountpoint
docker volume rm mydata 删除卷(卷被容器占用时删不掉)
docker volume prune 清理所有未被任何容器使用的卷

想知道某个卷在宿主机上到底存在哪,docker volume inspect 输出里的 Mountpoint 就是,一般是 /var/lib/docker/volumes/mydata/_data。命名卷用之前不必先create,docker run -v mydata:/path时若卷不存在会自动建。

卷与容器的生命周期

卷的生命周期独立于容器,这正是它能持久化的原因:

  • 删容器(docker rm)默认不会删卷,数据还在,新容器挂同名卷即可接着用。
  • docker rm -v 容器才会连带删掉它的匿名卷(命名卷依然保留)。
  • 想彻底清掉没人用的卷,用docker volume prune

Dockerfile

Dockerfile是一个文本文件,用一条条指令描述「从某个基础镜像出发,一步步构建出自己的镜像」的全过程。docker build读取它来生成镜像。每条指令大致对应镜像里的一个只读层(layer),层会被缓存复用,这是重复构建很快的原因。

指令一览

下面是Dockerfile的全部指令关键字:

指令 作用 示例
FROM 指定基础镜像,必须是第一条(ARG除外) FROM golang:1.24
RUN 构建时执行命令,常用于装软件、编译 RUN apt-get update && apt-get install -y git
CMD 容器启动时的默认命令,可被docker run后面的参数覆盖 CMD ["./app"]
ENTRYPOINT 容器启动入口,不会被run参数轻易覆盖,常和CMD配合 ENTRYPOINT ["nginx"]
COPY 把构建上下文里的文件复制进镜像 COPY . /app
ADD 类似COPY,但额外支持自动解压本地tar、拉远程URL ADD app.tar.gz /app
ENV 设置环境变量,构建期和运行期都有效 ENV PORT=8080
ARG 构建期变量,仅build时有效,可被--build-arg传入 ARG VERSION=1.0
WORKDIR 设置工作目录,后续指令的相对路径都基于它 WORKDIR /app
EXPOSE 声明容器监听的端口,仅声明不等于映射 EXPOSE 80
VOLUME 声明一个挂载点(匿名卷) VOLUME /data
USER 指定后续指令及容器运行时的用户 USER 1000
LABEL 给镜像加元数据标签 LABEL maintainer="me@x.com"
HEALTHCHECK 定义容器健康检查命令 HEALTHCHECK CMD curl -f http://localhost || exit 1
ONBUILD 触发器,本镜像被当作基础镜像时才执行 ONBUILD COPY . /app
STOPSIGNAL 指定停止容器时发送的系统信号 STOPSIGNAL SIGTERM
SHELL 覆盖RUN/CMD默认使用的shell SHELL ["/bin/bash", "-c"]
MAINTAINER 标注作者,已废弃,用LABEL代替 MAINTAINER me

几个易混点

  • CMDENTRYPOINTENTRYPOINT定义固定入口,CMD给默认参数。docker run后面跟的参数会覆盖CMD,但不会覆盖ENTRYPOINT。两者都建议用exec形式(JSON数组)写。
  • ADDCOPY:优先用COPY,语义单纯、可预期;只有确实需要自动解压本地tar时才用ADD,不推荐用它拉远程URL。
  • 每条RUN/COPY/ADD都会新增一层,把多条RUN&&合并能减少层数、缩小镜像。
  • shell形式(RUN apt-get update)与exec形式(RUN ["apt-get","update"]):CMD/ENTRYPOINT推荐exec形式,避免被sh -c包裹导致信号传不进主进程。

几个常见的Dockfile示例

开发环境:PHP-7.1.30

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
FROM harbor.qihoo.net/ybjcjdz-mobile-url-collection/centos:7_v1

LABEL maintainer="cuixiaogang@360.cn"

# 安装依赖环境
RUN yum install -y openssl openssl-devel libcurl libcurl-devel libmcrypt libmcrypt-devel zlib zlib-devel libxml2 libxml2-devel curl boost-devel bzip2 bzip2-devel libcurl libcurl-devel libxslt libxslt-devel pcre pcre-devel wget make gcc gcc-c++ cmake autoconf vim net-tools \
&& rm -rf /var/cache/yum \
&& yum clean all \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& mkdir -p /service/ops \
&& cd /service/ops \
&& mkdir -p /service/app/php-7.1.30 \
&& wget --no-check-certificate https://www.php.net/distributions/php-7.1.30.tar.gz \
&& cd /service/ops \
&& tar -zxvf php-7.1.30.tar.gz \
&& mkdir /service/app/php-7.1.30/run \
&& mkdir /service/app/php-7.1.30/logs \
&& mkdir /service/app/php-7.1.30/etc \
&& mkdir /service/app/php-7.1.30/etc/scan.d \
&& cd /service/ops/php-7.1.30 \
&& ./configure --prefix=/service/app/php-7.1.30 --with-config-file-path=/service/app/php-7.1.30/etc/ \
--with-config-file-scan-dir=/service/app/php-7.1.30/etc/scan.d/ --enable-fpm --with-fpm-group=nobody \
--disable-rpath --with-openssl -with-libxml-dir --with-xmlrpc --with-mhash --with-pcre-regex --with-zlib --enable-bcmath \
--with-bz2 --enable-calendar --with-curl --with-gettext --with-openssl-dir --with-zlib-dir --with-mysqli=mysqlnd \
--with-pdo-mysql=mysqlnd --enable-mbstring --with-xsl --enable-zip --with-pear --enable-shmop --enable-sockets --enable-sysvmsg \
--enable-sysvsem --enable-sysvshm --enable-wddx \
&& make && make install

ENV PATH=$PATH:/service/app/php-7.1.30/bin:/service/app/php-7.1.30/sbin

# 安装amqp
RUN cd /service/ops/ \
&& wget --no-check-certificate https://github.com/alanxz/rabbitmq-c/archive/refs/tags/v0.7.1.tar.gz \
&& mv v0.7.1.tar.gz rabbitmq-c-0.7.1.tar.gz \
&& tar -zxvf rabbitmq-c-0.7.1.tar.gz \
&& cd rabbitmq-c-0.7.1 \
&& mkdir build && cd build \
&& cmake -DCMAKE_INSTALL_PREFIX=/usr/local/rabbitmq-c-0.7.1 .. \
&& cmake --build . --target install \
&& ln -s /usr/local/rabbitmq-c-0.7.1/lib64 /usr/local/rabbitmq-c-0.7.1/lib \
&& cd /service/ops/php-7.1.30/ext/ \
&& wget --no-check-certificate http://pecl.php.net/get/amqp-1.9.4.tgz \
&& tar -zxvf amqp-1.9.4.tgz && cd amqp-1.9.4 && phpize \
&& ./configure --with-php-config=/service/app/php-7.1.30/bin/php-config --with-amqp --with-librabbitmq-dir=/usr/local/rabbitmq-c-0.7.1 \
&& make && make install \
&& echo "extension=amqp.so" &>> /service/app/php-7.1.30/etc/scan.d/amqp.ini \
## mongodb
&& cd /service/ops/php-7.1.30/ext/ \
&& wget --no-check-certificate http://pecl.php.net/get/mongodb-1.9.1.tgz \
&& tar -zxvf mongodb-1.9.1.tgz && cd mongodb-1.9.1 && phpize \
&& ./configure --with-php-config=/service/app/php-7.1.30/bin/php-config \
&& make && make install \
&& echo "extension=mongodb.so" &>> /service/app/php-7.1.30/etc/scan.d/mongodb.ini \
## rdkafka
&& yum install -y librdkafka librdkafka-devel \
&& cd /service/ops/php-7.1.30/ext/ \
&& wget --no-check-certificate http://pecl.php.net/get/rdkafka-4.1.2.tgz \
&& tar -zxvf rdkafka-4.1.2.tgz && cd rdkafka-4.1.2 && phpize \
&& ./configure --with-php-config=/service/app/php-7.1.30/bin/php-config \
&& make && make install \
&& echo "extension=rdkafka.so" &>> /service/app/php-7.1.30/etc/scan.d/rdkafka.ini \
## redis
&& cd /service/ops/php-7.1.30/ext/ \
&& wget --no-check-certificate http://pecl.php.net/get/redis-5.3.4.tgz \
&& tar -zxvf redis-5.3.4.tgz && cd redis-5.3.4 && phpize \
&& ./configure --with-php-config=/service/app/php-7.1.30/bin/php-config \
&& make && make install \
&& echo "extension=redis.so" &>> /service/app/php-7.1.30/etc/scan.d/redis.ini \
## memcache
&& yum install -y libmemcached libmemcached-devel unzip zip \
&& cd /service/ops/php-7.1.30/ext/ \
&& wget --no-check-certificate https://pecl.php.net/get/memcache-4.0.5.1.tgz \
&& tar -zxvf memcache-4.0.5.1.tgz && cd memcache-4.0.5.1 && phpize \
&& ./configure --with-php-config=/service/app/php-7.1.30/bin/php-config \
&& make && make install \
&& echo "extension=memcache.so" &>> /service/app/php-7.1.30/etc/scan.d/memcache.ini \
## pcntl
&& cd /service/ops/php-7.1.30/ext/pcntl \
&& phpize && ./configure --with-php-config=/service/app/php-7.1.30/bin/php-config \
&& make && make install \
&& echo "extension=pcntl.so" &>> /service/app/php-7.1.30/etc/scan.d/pcntl.ini \
## uuid
&& yum install -y libuuid libuuid-devel \
&& cd /service/ops/php-7.1.30/ext/ \
&& wget --no-check-certificate https://pecl.php.net/get/uuid-1.2.0.tgz \
&& tar -zxvf uuid-1.2.0.tgz && cd uuid-1.2.0 && phpize \
&& ./configure --with-php-config=/service/app/php-7.1.30/bin/php-config \
&& make && make install \
&& echo "extension=uuid.so" &>> /service/app/php-7.1.30/etc/scan.d/uuid.ini \
## qlog
&& echo "extension=qlogphp.so" &>> /service/app/php-7.1.30/etc/scan.d/qlogphp.ini

COPY ./files/qlogphp.so /service/app/php-7.1.30/lib/php/extensions/no-debug-non-zts-20160303/qlogphp.so
COPY ./files/lib64/libqlog.so.2 /lib64/libqlog.so.2
COPY ./files/lib64/liblog4cplus-1.1.so.9 /lib64/liblog4cplus-1.1.so.9

# 安装composer
RUN curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/bin/composer

#nginx
RUN mkdir -p /service/app/nginx-1.16.1 \
&& cd /service/ops \
&& wget --no-check-certificate http://nginx.org/download/nginx-1.16.1.tar.gz \
&& tar -zxvf nginx-1.16.1.tar.gz \
&& cd nginx-1.16.1 \
&& ./configure --user=root --group=root --prefix=/service/app/nginx-1.16.1/ \
--pid-path=/service/app/nginx-1.16.1/run/nginx.pid --lock-path=/service/app/nginx-1.16.1/run/nginx.lock \
--with-http_stub_status_module --with-http_ssl_module --with-stream --with-http_gzip_static_module --with-http_sub_module \
&& make && make install

ENV PATH=$PATH:/service/app/nginx-1.16.1/sbin

# ssh
RUN yum install -y openssh-server \
&& echo 'root:4mCrjpI2si7hVJLPv6zMe' | chpasswd \
&& sed -i 's/#Port 22/Port 15001/' /etc/ssh/sshd_config \
&& sed -i 's/PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config \
&& ssh-keygen -A \
&& mkdir -p /root/.ssh \
&& echo "ssh-rsa xxxxxxxxxxxxxx" >> /root/.ssh/authorized_keys

# 代码准备
RUN mkdir /service/run

COPY ./config/nginx.conf /service/app/nginx-1.16.1/conf/nginx.conf
COPY ./config/php.ini /service/app/php-7.1.30/etc/php.ini
COPY ./config/php-fpm.conf /service/app/php-7.1.30/etc/php-fpm.conf
COPY ./run.sh /service/run.sh

EXPOSE 15001-15200

VOLUME "/service/www"
VOLUME "/service/conf"
VOLUME "/service/logs"
VOLUME "/service/data"

WORKDIR "/service/data"

RUN chmod 777 -R /service/run.sh \
&& echo "export PATH=$PATH" >> /root/.bashrc

CMD ["/service/run.sh"]

# docker build -t cxg_dev_php_7_1_30:1.0 -f ./Dockerfile .
# docker run -d --name cxg_dev_php71 -p 15001-15200:15001-15200 -v /home/cuixiaogang/www:/service/www -v /home/cuixiaogang/conf:/service/conf -v /home/cuixiaogang/logs:/service/logs -v /home/cuixiaogang/data:/service/data cxg_dev_php_7_1_30:1.0

开发环境:PHP-5.5.25

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
FROM harbor.qihoo.net/ybjcjdz-mobile-url-collection/centos:7_v1

LABEL maintainer="cuixiaogang@360.cn"

RUN rm -rf /etc/localtime \
&& ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&&yum install -y wget make cmake gcc gcc-c++ autoconf libxml2 libxml2-devel openssl openssl-devel bzip2 bzip2-devel libcurl libcurl-devel libxslt libxslt-devel \
&& mkdir -p /service/ops \
&& cd /service/ops \
&& mkdir -p /service/app/php-5.5.25 \
&& wget --no-check-certificate https://www.php.net/distributions/php-5.5.25.tar.gz \
&& tar -zxvf php-5.5.25.tar.gz \
&& mkdir /service/app/php-5.5.25/run \
&& mkdir /service/app/php-5.5.25/logs \
&& mkdir /service/app/php-5.5.25/etc \
&& mkdir /service/app/php-5.5.25/etc/scan.d \
&& cd /service/ops/php-5.5.25 \
&& ./configure --prefix=/service/app/php-5.5.25 --with-config-file-path=/service/app/php-5.5.25/etc/ --with-config-file-scan-dir=/service/app/php-5.5.25/etc/scan.d/ \
--enable-fpm --with-fpm-group=nobody --disable-rpath --with-openssl -with-libxml-dir --with-xmlrpc --with-mhash --with-pcre-regex --with-zlib --enable-bcmath --with-bz2 \
--enable-calendar --with-curl --with-gettext --with-openssl-dir --with-zlib-dir --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --enable-mbstring \
--with-xsl --enable-zip --with-pear --enable-shmop --enable-sockets --enable-sysvmsg --enable-sysvsem --enable-sysvshm --enable-wddx \
&& make && make install


ENV PATH=$PATH:/service/app/php-5.5.25/bin

RUN cd /service/ops/ \
&& wget https://github.com/alanxz/rabbitmq-c/releases/download/v0.5.2/rabbitmq-c-0.5.2.tar.gz \
&& tar -zxvf rabbitmq-c-0.5.2.tar.gz \
&& cd rabbitmq-c-0.5.2 \
&& mkdir build && cd build \
&& cmake -DCMAKE_INSTALL_PREFIX=/usr/local/rabbitmq-c-0.5.2 .. \
&& cmake --build . --target install \
&& cp -r /usr/local/rabbitmq-c-0.5.2/lib64 /usr/local/rabbitmq-c-0.5.2/lib \
&& cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/amqp-1.4.0.tgz \
&& tar -zxvf amqp-1.4.0.tgz && cd amqp-1.4.0 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config --with-amqp --with-librabbitmq-dir=/usr/local/rabbitmq-c-0.5.2 \
&& make && make install \
&& echo "extension=amqp.so" &>> /service/app/php-5.5.25/etc/scan.d/amqp.ini

RUN cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/memcache-2.2.5.tgz \
&& tar -zxvf memcache-2.2.5.tgz && cd memcache-2.2.5 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config \
&& make && make install \
&& echo "extension=memcache.so" &>> /service/app/php-5.5.25/etc/scan.d/memcache.ini

RUN yum install -y libmemcached libmemcached-devel \
&& cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/memcached-2.2.0.tgz \
&& tar -zxvf memcached-2.2.0.tgz && cd memcached-2.2.0 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config \
&& make && make install \
&& echo "extension=memcached.so" &>> /service/app/php-5.5.25/etc/scan.d/memcached.ini

RUN cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/mongo-1.6.10.tgz \
&& tar -zxvf mongo-1.6.10.tgz && cd mongo-1.6.10 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config \
&& make && make install \
&& echo "extension=mongo.so" &>> /service/app/php-5.5.25/etc/scan.d/mongo.ini

RUN cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/mongodb-1.1.6.tgz \
&& tar -zxvf mongodb-1.1.6.tgz && cd mongodb-1.1.6 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config \
&& make && make install \
&& echo "extension=mongodb.so" &>> /service/app/php-5.5.25/etc/scan.d/mongodb.ini

RUN yum install -y librdkafka librdkafka-devel \
&& cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/rdkafka-3.0.5.tgz \
&& tar -zxvf rdkafka-3.0.5.tgz && cd rdkafka-3.0.5 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config \
&& make && make install \
&& echo "extension=rdkafka.so" &>> /service/app/php-5.5.25/etc/scan.d/rdkafka.ini

RUN cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/redis-2.2.8.tgz \
&& tar -zxvf redis-2.2.8.tgz && cd redis-2.2.8 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config \
&& make && make install \
&& echo "extension=redis.so" &>> /service/app/php-5.5.25/etc/scan.d/redis.ini

RUN yum install -y libuuid libuuid-devel \
&& cd /service/ops/php-5.5.25/ext \
&& wget http://pecl.php.net/get/uuid-1.0.4.tgz \
&& tar -zxvf uuid-1.0.4.tgz && cd uuid-1.0.4 \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config \
&& make && make install \
&& echo "extension=uuid.so" &>> /service/app/php-5.5.25/etc/scan.d/uuid.ini

RUN cd /service/ops/php-5.5.25/ext/mysql \
&& phpize \
&& ./configure --with-php-config=/service/app/php-5.5.25/bin/php-config --with-pdo-mysql=mysqlnd \
&& make && make install \
&& echo "extension=mysql.so" &>> /service/app/php-5.5.25/etc/scan.d/mysql.ini

COPY ./files/qbus.so /service/app/php-5.5.25/lib/php/extensions/no-debug-non-zts-20121212/qbus.so
COPY ./files/qcm.so /service/app/php-5.5.25/lib/php/extensions/no-debug-non-zts-20121212/qcm.so
COPY ./files/qconf.so /service/app/php-5.5.25/lib/php/extensions/no-debug-non-zts-20121212/qconf.so
COPY ./files/qlogphp.so /service/app/php-5.5.25/lib/php/extensions/no-debug-non-zts-20121212/qlogphp.so
COPY ./files/mobilekill_protobuf.so /service/app/php-5.5.25/lib/php/extensions/no-debug-non-zts-20121212/mobilekill_protobuf.so
COPY ./files/lib64/libqlogThread.so /usr/lib64/libqlogThread.so
COPY ./files/lib64/liblog4cplus-1.1.so.7 /usr/lib64/liblog4cplus-1.1.so.7
COPY ./files/lib64/libprotobuf.so.7 /usr/lib64/libprotobuf.so.7

RUN echo "extension=qbus.so" &>> /service/app/php-5.5.25/etc/scan.d/qbus.ini \
&& echo "extension=qcm.so" &>> /service/app/php-5.5.25/etc/scan.d/qcm.ini \
&& echo "extension=qconf.so" &>> /service/app/php-5.5.25/etc/scan.d/qconf.ini \
&& echo "extension=qlogphp.so" &>> /service/app/php-5.5.25/etc/scan.d/qlogphp.ini \
&& echo "extension=mobilekill_protobuf.so" &>> /service/app/php-5.5.25/etc/scan.d/mobilekill_protobuf.ini \
&& curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/bin/composer

# nginx
RUN mkdir -p /service/app/nginx-1.16.1 \
&& mkdir -p /service/ops && cd /service/ops \
&& wget http://nginx.org/download/nginx-1.16.1.tar.gz \
&& tar -zxvf nginx-1.16.1.tar.gz \
&& cd nginx-1.16.1 \
&& ./configure --user=root --group=root --prefix=/service/app/nginx-1.16.1/ \
--pid-path=/service/app/nginx-1.16.1/run/nginx.pid --lock-path=/service/app/nginx-1.16.1/run/nginx.lock \
--with-http_stub_status_module --with-http_ssl_module --with-stream --with-http_gzip_static_module --with-http_sub_module \
&& make && make install

ENV PATH=$PATH:/service/app/nginx-1.16.1/sbin

# ssh
RUN yum install -y openssh-server net-tools vim \
&& echo 'root:4mCrjpI2si7hVJLPv6zMe' | chpasswd \
&& sed -i 's/#Port 22/Port 15201/' /etc/ssh/sshd_config \
&& sed -i 's/PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config \
&& ssh-keygen -A \
&& mkdir -p /root/.ssh \
&& echo "ssh-rsa xxxxxxxxxxxxxxxxxxxxxxxxxxx" >> /root/.ssh/authorized_keys

# 代码准备
RUN mkdir /service/run

COPY ./config/nginx.conf /service/app/nginx-1.16.1/conf/nginx.conf
COPY ./config/php.ini /service/app/php-5.5.25/etc/php.ini
COPY ./config/php-fpm.conf /service/app/php-5.5.25/etc/php-fpm.conf
COPY ./run.sh /service/run.sh


EXPOSE 15201-15400

VOLUME "/service/www"
VOLUME "/service/conf"
VOLUME "/service/logs"
VOLUME "/service/data"

WORKDIR "/service/data"

RUN chmod 777 -R /service/run.sh \
&& echo "export PATH=$PATH" >> /root/.bashrc

CMD ["/service/run.sh"]

# docker build -t cxg_dev_php_5_5_25:1.0 -f ./Dockerfile .
# docker run -d --name cxg_dev_php55 -p 15201-15400:15201-15400 -v /home/cuixiaogang/www:/service/www -v /home/cuixiaogang/conf:/service/conf -v /home/cuixiaogang/logs:/service/logs -v /home/cuixiaogang/data:/service/data cxg_dev_php_5_5_25:1.0

开发环境:NODEJS-14.9.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
FROM harbor.qihoo.net/ybjcjdz-android-platform/centos:7_v1

RUN rm -rf /etc/localtime \
&& ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& yum install -y wget make cmake gcc gcc-c++ autoconf libxml2 libxml2-devel openssl openssl-devel bzip2 bzip2-devel libcurl libcurl-devel libxslt libxslt-devel git \
&& mkdir -p /service/www \
&& mkdir -p /service/app \
&& cd /service/app \
&& wget https://nodejs.org/dist/v14.9.0/node-v14.9.0-linux-x64.tar.gz \
&& tar -zxvf node-v14.9.0-linux-x64.tar.gz

ENV PATH=$PATH:/service/app/node-v14.9.0-linux-x64/bin/

RUN git config --global url."https://".insteadOf git:// \
&& npm config set registry https://registry.npmmirror.com/

# ssh
RUN yum install -y openssh-server net-tools vim \
&& echo 'root:4mCrjpI2si7hVJLPv6zMe' | chpasswd \
&& sed -i 's/#Port 22/Port 15801/' /etc/ssh/sshd_config \
&& sed -i 's/PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config \
&& ssh-keygen -A \
&& mkdir -p /root/.ssh \
&& echo "ssh-rsa xxxxxxxxxxxxxxxxxx" >> /root/.ssh/authorized_keys

COPY ./run.sh /service/run.sh

EXPOSE 15801-16000

VOLUME "/service/www"
VOLUME "/service/conf"
VOLUME "/service/logs"
VOLUME "/service/data"

WORKDIR "/service/data"

RUN chmod 777 -R /service/run.sh \
&& echo "export PATH=$PATH" >> /root/.bashrc

CMD ["/service/run.sh"]

# 构建镜像
# docker build -t cxg_dev_node_14_9_0:1.0 -f ./Dockerfile .
# docker run -d --name cxg_dev_node149 -p 15801-16000:15801-16000 -v /home/cuixiaogang/www:/service/www -v /home/cuixiaogang/conf:/service/conf -v /home/cuixiaogang/logs:/service/logs -v /home/cuixiaogang/data:/service/data cxg_dev_node_14_9_0:1.0

开发环境:GO-1.16.15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
FROM harbor.qihoo.net/ybjcjdz-mobile-url-collection/centos:7_v1

LABEL maintainer="cuixiaogang@360.cn"

# 安装依赖环境
RUN yum install -y openssl openssl-devel libcurl libcurl-devel libmcrypt libmcrypt-devel zlib zlib-devel libxml2 libxml2-devel curl boost-devel bzip2 bzip2-devel libcurl libcurl-devel libxslt libxslt-devel pcre pcre-devel wget make gcc gcc-c++ cmake autoconf vim net-tools git \
&& rm -rf /var/cache/yum \
&& yum clean all \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& mkdir -p /service/app \
&& cd /service/app \
&& wget https://go.dev/dl/go1.16.15.linux-amd64.tar.gz \
&& tar -zxvf go1.16.15.linux-amd64.tar.gz \
&& mv go go-1.16.15

ENV PATH=$PATH:/service/app/go-1.16.15/bin

RUN go env -w GOPATH=/service/data/library/go_path \
&& go env -w GOCACHE=/service/data/library/go_cache \
&& go env -w GONOPROXY="w.src.corp.qihoo.net" \
&& go env -w GONOSUMDB="w.src.corp.qihoo.net" \
&& go env -w GO111MODULE=on \
&& git config --global url."https://".insteadOf git:// \
&& git config --global url."ssh://git@code.geelib.qihoo.net:10022/whitelist_scancenter/go-biz-library.git".insteadOf "https://w.src.corp.qihoo.net/android-analysis/go-biz-library.git" \
&& git config --global url."ssh://git@code.geelib.qihoo.net:10022/whitelist_scancenter/go-library.git".insteadOf "https://w.src.corp.qihoo.net/android-analysis/go-library.git"

# ssh
RUN yum install -y openssh-server \
&& echo 'root:4mCrjpI2si7hVJLPv6zMe' | chpasswd \
&& sed -i 's/#Port 22/Port 15401/' /etc/ssh/sshd_config \
&& sed -i 's/PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config \
&& ssh-keygen -A \
&& mkdir -p /root/.ssh \
&& echo "ssh-rsa xxxxxxxxx" >> /root/.ssh/authorized_keys

# 代码准备
RUN mkdir /service/run

COPY ./run.sh /service/run.sh

EXPOSE 15401-15600

VOLUME "/service/www"
VOLUME "/service/conf"
VOLUME "/service/logs"
VOLUME "/service/data"

WORKDIR "/service/data"

RUN chmod 777 -R /service/run.sh \
&& echo "export PATH=$PATH:/service/data/library/go_path/bin" >> /root/.bashrc

CMD ["/service/run.sh"]

# docker build -t cxg_dev_go_1_16_15:1.0 -f ./Dockerfile .
# docker run -d --name cxg_dev_go116 -p 15401-15600:15401-15600 -v /home/cuixiaogang/www:/service/www -v /home/cuixiaogang/conf:/service/conf -v /home/cuixiaogang/logs:/service/logs -v /home/cuixiaogang/data:/service/data cxg_dev_go_1_16_15:1.0

构建命令

1
2
3
4
5
6
7
8
# 示例中的构建命令
docker build -t cxg_dev_go_1_16_15:1.0 -f ./Dockerfile .

# 指定其他Dockerfile
docker build -t myapp:1.0 -f Dockerfile.prod .

# 传入构建参数
docker build --build-arg VERSION=2.0 -t myapp .

末尾的.是「构建上下文」,Docker会把这个目录打包发给daemon,所以别在体积巨大的目录里直接build,必要时用.dockerignore排除无关文件。

镜像与 Registry

镜像是Docker分发的核心载体。前面「Dockerfile」讲了怎么把镜像构建出来,这一章把剩下的部分收口:镜像到底由什么组成(分层),以及怎么在机器之间分发(build/tag/push/pull 和私有仓库)。

镜像分层

一个镜像不是单一文件,而是由多个只读层(layer)自下而上叠起来的。每条Dockerfile指令(主要是RUN/COPY/ADD)生成一层,Docker用联合文件系统(如overlay2)把这些层叠成一个完整的文件视图。

镜像分层
镜像分层

分层带来两个直接好处:

  • 缓存复用:构建时某一层没变就直接用缓存,所以改一行代码重新build往往很快,只有改动的那层及其之后的层会重建。
  • 存储共享:多个镜像若基于同一个FROM基础镜像,这些底层在磁盘上只存一份,省空间。

容器运行时会在镜像最上面再加一个可写层,所有改动都写在这一层(写时复制),不影响下面只读的镜像层——这也是删掉容器、镜像还在的原因。

可以使用命令docker history [images]{:tag}来查看分层信息

一个常用的镜像分层
一个常用的镜像分层

镜像命名与标签

镜像的完整名字格式:

1
2
[仓库地址[:端口]/]仓库名[:标签]
例:registry.example.com:5000/team/app:1.2
  • 不写仓库地址,默认是Docker Hub(docker.io)。
  • 不写标签,默认latest。注意latest只是个默认名字,并不保证是最新版本。
  • 每个镜像还有唯一的内容摘要sha256:...,可用镜像名@sha256:...精确锁定某个版本。

镜像处理全流程

把自己的镜像推到仓库、再在别的机器拉下来,是最典型的分发流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 构建镜像
docker build -t app:1.0 .

# 2. 打上目标仓库的标签:仓库地址/项目/名字:版本
docker tag app:1.0 registry.example.com/team/app:1.0

# 3. 登录仓库
docker login registry.example.com

# 4. 推送
docker push registry.example.com/team/app:1.0

# 5. 在另一台机器拉取并运行
docker pull registry.example.com/team/app:1.0
docker run -d registry.example.com/team/app:1.0

关键是第2步tag:push推到哪里,是由镜像名里的仓库地址决定的,所以推送前必须先把镜像重命名成仓库地址/项目/名字的形式。

如果不方便联网推送,也可以用docker save把镜像打成tar包,拷过去再docker load

1
2
docker save -o app.tar app:1.0      # 导出镜像为tar
docker load -i app.tar # 在另一台机器导入

注意区分:save/load针对的是镜像,export/import针对的是容器的文件系统(会丢掉分层和元数据),日常分发镜像用前者。

私有仓库与 Harbor

Docker Hub是公共仓库,公司内部一般要自建私有仓库,常见两种:

方案 说明
registry:2 Docker官方的Distribution,轻量,只有存储和HTTP API,没有界面和权限管理
Harbor CNCF项目,在registry之上补齐了一整套企业能力

Harbor是目前企业里最常用的私有镜像仓库,相比裸registry,它主要补齐了:

  • 图形界面 + 项目管理:按项目(project)组织镜像,可分公开/私有。
  • 权限控制(RBAC):用户、角色、项目级别的访问权限。
  • 漏洞扫描:集成Trivy等扫描器,push上来的镜像自动扫CVE。
  • 镜像签名:保证镜像来源可信、未被篡改。
  • 复制(replication):多个Harbor之间、或与Docker Hub之间同步镜像,做多机房分发。

用法上和普通仓库没区别:docker login harbor.example.com之后,push/pull照常,把地址换成Harbor的域名和项目名即可。

小结

操作 命令
构建 docker build -t 名:tag .
重命名 docker tag 旧名 仓库/项目/名:tag
登录 docker login 仓库地址
推送 docker push 仓库/项目/名:tag
拉取 docker pull 仓库/项目/名:tag
离线导出/导入 docker save -o x.tar 名 / docker load -i x.tar

Docker Compose

当一个应用由多个容器组成(比如web加数据库加缓存),一条条敲docker run既繁琐又难维护。Docker Compose用一个docker-compose.yml(新版也叫compose.yml)文件,把多个服务、网络、卷一次性声明清楚,一条命令拉起整套。

命令有两代:老的docker-compose(v1,Python写的,带连字符)和新的docker compose(v2,Go写的Docker插件,无连字符)。新版已是主流,下面统一用docker compose

文件结构(顶层关键字)

顶层关键字 作用
version Compose文件版本,Compose Spec之后已可省略,旧文件里常见
services 核心,定义各个服务(每个服务对应一类容器)
networks 声明自定义网络
volumes 声明命名卷
configs 配置项,主要用于Swarm
secrets 敏感信息,主要用于Swarm

services 下常用关键字

关键字 作用 对应docker run
image 使用的镜像 镜像名
build 用Dockerfile现场构建,可指定上下文和文件 docker build
command 覆盖默认启动命令 CMD
entrypoint 覆盖入口 --entrypoint
container_name 指定容器名 --name
ports 端口映射宿主:容器 -p
expose 仅声明端口,不映射到宿主 EXPOSE
environment 设置环境变量 -e
env_file 从文件读环境变量 --env-file
volumes 挂载卷或目录 -v
networks 加入的网络 --network
depends_on 声明服务启动的先后依赖 -
restart 重启策略 --restart
healthcheck 健康检查 HEALTHCHECK
deploy 部署配置(副本数、资源限制,主要Swarm) -
labels 元数据标签 --label
working_dir 工作目录 -w
user 运行用户 -u

一个完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
version: '3.7'

services:
transfer:
image: "harbor.qihoo.net/ybjcjdz-downloader/mobile-url-downloader:20260318100535"
command: [ "downloadTransfer" ]
deploy:
mode: replicated
replicas: 5
networks:
- net
mobile_url_collection:
image: "harbor.qihoo.net/ybjcjdz-downloader/mobile-url-downloader:20260318100535"
command: [ "mobileUrlCollectionDownloader" ]
deploy:
mode: replicated
replicas: 512
resources:
reservations:
memory: 2G
update_config:
parallelism: 8 # 每次只同时启动/更新 8 个容器
delay: 3s # 每批次间隔 5 秒,给 iptables 喘息的时间
failure_action: pause
order: stop-first
networks:
- net
cooperator:
image: "harbor.qihoo.net/ybjcjdz-downloader/mobile-url-downloader:20260318100535"
command: [ "cooperatorDownloader" ]
deploy:
mode: replicated
replicas: 0
networks:
- net
spider:
image: "harbor.qihoo.net/ybjcjdz-downloader/mobile-url-downloader:20260318100535"
command: [ "spiderDownloader" ]
deploy:
mode: replicated
replicas: 0
networks:
- net
networks:
net:
driver: overlay
ipam:
driver: default
config:
- subnet: "10.1.0.0/16"

这是一个实际的下载器服务栈,有几个值得注意的点:

  • 一份镜像,多个角色:四个服务(transfer、mobile_url_collection、cooperator、spider)用的是同一个镜像,靠不同的command跑出不同的进程入口。这是worker型服务常见的组织方式,省去为每个角色单独打镜像。镜像地址harbor.qihoo.net/...正是前面「镜像与 Registry」讲的Harbor私有仓库。
  • 副本数(replicas)deploy.replicas指定每个服务起几个实例——transfer起5个、collection起512个,cooperator和spider先设0(占位,需要时再scale起来)。
  • 滚动更新控制:collection因为副本多,额外配了update_configparallelism: 8每批只启动/更新8个、delay控制批次间隔、order: stop-first先停旧的再起新的、failure_action: pause出错就暂停。一次性拉起512个容器会把网络和iptables压垮,分批正是为此。它还用resources.reservations.memory预留了内存。
  • overlay 网络:所有服务都接在名为net的overlay网络上。overlay是跨主机的网络(前面「网络」一章提过),Swarm集群里多台机器上的容器互通就靠它,这里还指定了子网10.1.0.0/16

要特别说明:这份文件用到了deploy,本质是Swarm的stack文件,得用docker stack deploy -c xxx.yml 栈名部署到Swarm集群才完整生效。普通的docker compose up只认其中一部分字段(如replicasresources),update_configmode这些会被直接忽略。

常用命令

命令 作用
docker compose up -d 后台拉起所有服务,不存在则创建
docker compose down 停止并删除容器、网络(加-v连卷一起删)
docker compose ps 查看本项目的容器状态
docker compose logs -f 跟踪查看日志
docker compose build 构建或重新构建镜像
docker compose exec web bash 进入某个服务的容器
docker compose restart 重启服务
docker compose stop / start 停止/启动服务但不删除

up时Compose会自动建一个项目专属的网络,所有服务默认都接在上面,因此服务之间天然能用服务名互访,这和前面「网络」一章的自定义bridge是一脉相承的。