Dockerfile编写规则

一、前言

承接上篇文章 docker 镜像与容器,本篇来讲讲如何创建 Dockerfile 来构建一个镜像。上篇文章有讲到构建一个自定义镜像是手动去构建的,虽然步骤清晰,但是操作比较繁琐,镜像分发起来也不是很方便,所以有必要用一种更好的办法去替换这种模式去创建自定义镜像,于是 Dockerfile 就是最优替代方案。废话少说,现在就来看看如何编写一个 Dockerfile 文件并创建容器镜像的,先说明一个本篇文章的运行环境吧,有看过上篇文章的朋友应该知道,我用的 docker 的镜像加速地址是阿里云的,我觉得这是我用 docker 最无痛的环境了。

二、Dockerfile 示例

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
# Base images 基础镜像
FROM centos

#MAINTAINER 维护者信息
MAINTAINER lorenwe

#ENV 设置环境变量
ENV PATH /usr/local/nginx/sbin:$PATH

#ADD 文件放在当前目录下,拷过去会自动解压
ADD nginx-1.13.7.tar.gz /tmp/

#RUN 执行以下命令
RUN rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 \
&& yum update -y \
&& yum install -y vim less wget curl gcc automake autoconf libtool make gcc-c++ zlib zlib-devel openssl openssl-devel perl perl-devel pcre pcre-devel libxslt libxslt-devel \
&& yum clean all \
&& rm -rf /usr/local/src/*
RUN useradd -s /sbin/nologin -M www

#WORKDIR 相当于cd
WORKDIR /tmp/nginx-1.13.7

RUN ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-pcre && make && make install

RUN cd / && rm -rf /tmp/

COPY nginx.conf /usr/local/nginx/conf/

#EXPOSE 映射端口
EXPOSE 80 443

#ENTRYPOINT 运行以下命令
ENTRYPOINT ["nginx"]

#CMD 运行以下命令
CMD ["-h"]

以上代码示例是我编写的一个认为很有代表性的 dockerfile 文件,涉及到的内容不多,但基本上把所有 dockerfile 指令都用上了,也包含一些细节方面的东西,为了达到示例的效果所以并不是最简洁的 dockerfile,建立一个文件夹将以上 dockerfile 放在该文件内,再去 nginx 官网把 nginx 源码包下来放到该文件夹内,之后再在该文件夹内打开命令行窗口,最好是以管理员权限打开命令行窗口,以免出现一些权限问题的错误,此时的目录结构应该是以下样子的

dockerfile catalog

三、指令分析

FROM 表示的是这个 dockerfile 构建镜像的基础镜像是什么,有点像代码里面类的继承那样的关系,基础镜像所拥有的功能在新构建出来的镜像中也是存在的,一般用作于基础镜像都是最干净的没有经过任何三方修改过的,比如我用的就是最基础的 centos,这里有必要说明一下,因为我用的镜像加速源是阿里云的,所以我 pull 下来的 centos 是自带阿里云的 yum 源的镜像,如果你用的不是阿里云的镜像加速源,pull 下来的镜像 yum 源也不一样,到时 yum 安装软件的时候可能会遇到很多问题(你懂得)。

MAINTAINER 就是维护者信息了,填自己名字就可了,不用说什么了

ENV 设置环境变量,简单点说就是设置这个能够帮助系统找到所需要运行的软件,比如我上面写的是 “ENV PATH /usr/local/nginx/sbin:$PATH”,这句话的意思就是告诉系统如果运行一个没有指定路径的程序时可以从 /usr/local/nginx/sbin 这个路径里面找,只有设置了这个,后面才可以直接使用 ngixn 命令来启动 nginx,不然的话系统会提示找不到应用。

ADD 顾名思义,就是添加文件的功能了,但是他比普通的添加做的事情多一点,源文件可以是一个文件,或者是一个 URL 都行,如果源文件是一个压缩包,在构建镜像的时候会自动的把压缩包解压开来,示例我写的是 ‘ADD nginx-1.13.7.tar.gz /tmp/’ 其中 nginx-1.13.7.tar.gz 这个压缩包是必须要在 dockefile 文件目录内的,不在 dockerfile 文件目录内的 比如你写完整路径 D:test/nginx-1.13.7.tar.gz 都是会提示找不到文件的。

RUN 就是执行命令的意思了,RUN 可以执行多条命令, 用 && 隔开就行,如果命令太长要换行的话在末尾加上 ‘\’ 就可以换行命令,RUN 的含义非常简单,就是执行命令,但其中有一些细节还是需要注意的,现在就通过上面的示例来看看需要注意的地方有哪些吧。其中 RUN rpm –import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 的作用就是导入软件包签名来验证软件包是否被修改过了,为做到安全除了系统要官方的之外软件也要保证是可信的。yum update -y 升级所有包,改变软件设置和系统设置,系统版本内核都升级,我们知道 linux 的软件存在依赖关系,有时我们安装新的软件他所依赖的工具软件也需要是最新的,如果没有用这个命令去更新原来的软件包,就很容易造成我们新安装上的软件出问题,报错提示不明显的情况下我们更是难找到问题了,为避免此类情况发生我们还是先更新一下软件包和系统,虽然这会使 docker 构建镜像时变慢但也是值得的,至于后面的命令自然是安装各种工具库了,接着来看这句 yum clean all ,把所有的 yum 缓存清掉,这可以减少构建出来的镜像大小,rm -rf /usr/local/src/ 清除用户源码文件,都是起到减少构建镜像大小的作用。RUN 指令是可以分步写的,比如上面的 RUN 可以拆成以下这样:

1
2
3
4
5
6
# 不推荐
RUN rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 \
RUN yum update -y \
RUN yum install -y vim less wget curl gcc automake autoconf libtool make gcc-c++ zlib zlib-devel openssl openssl-devel perl perl-devel pcre pcre-devel libxslt libxslt-devel \
RUN yum clean all \
RUN rm -rf /usr/local/src/*

这样也是可以的,但是最好不要这样,因为 dockerfile 构建镜像时每执行一个关键指令都会去创建一个镜像版本,这有点像 git 的版本管理,比如执行完第一个 RUN 命令后在执行第二个 RUN 命令时是会在一个新的镜像版本中执行,这会导致 yum clean all 这个命令失效,没有起到精简镜像的作用,虽然不推荐多写几个 RUN,但也不是说把所有的操作都放在一个 RUN 里面,这里有个原则就是把所有相关的操作都放在同一个 RUN 里面,就比如我把 yum 更新,安装工具库,清除缓存放在一个 RUN 里面,后面的编译安装 nginx 放在另外一个 RUN 里面。

WORKDIR 表示镜像活动目录变换到指定目录,就相当于 linux 里面 cd 到指定目录一样,其实完全没有必要使用这个指令的,在需要时可以直接使用 cd 命令就行,因为这里使用了 WORKDIR,所以后面的 RUN 编译安装 nginx 不用切换目录,讲到这里又想起了另外一个问题,如下:

1
2
3
4

RUN cd /tmp/nginx-1.13.7

RUN ./configure

这样可不可以呢,我想前面看懂的朋友应该知道答案了吧,这里还是再啰嗦一下,这样是会报找不到 configure 文件错误的,原因很简单,因为这个两个命令都不是在同一个镜像中执行的,第一个镜像 cd 进入的目录并不代表后面的镜像也进入了。

COPY 这个指令很简单,就是把文件拷贝到镜像中的某个目录,注意源文件也是需要在 dockerfile 所在目录的,示例的意思是拷贝一份 nginx 配置文件,现在就在 dockerfile 所在目录创建这个文件

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
user  www;
worker_processes 2;
daemon off;

pid logs/nginx.pid;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;

sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

配置很简单,就是对官方的配置文件把注释去掉了,注意里面的 daemon off; 配置,意思是关闭 nginx 后台运行,原因在上一篇文章中讲过,这里再来絮叨一下,容器默认会把容器内部第一个进程是否活动作为docker容器是否正在运行的依据,如果 docker 容器运行完就退出了,那么docker容器便会直接退出,docker run 的时候把 command 作为容器内部命令,如果使用 nginx,那么 nginx 程序将后台运行,这个时候 nginx 并不是第一个执行的程序,而是执行的 bash,这个 bash 执行了 nginx 指令后就挂了,所以容器也就退出了,如果我们设置了 daemon off 后
启动 nginx 那么 nginx 就会一直占用命令窗口,自然 bash 没法退出了所以容器一直保持活动状态。

EXPOSE 示例注释写的是映射端口,但我觉得用暴露端口来形容更合适,因为在使用 dockerfile 创建容器的时候不会映射任何端口,映射端口是在用 docker run 的时候来指定映射的端口,比如我把容器的 80 端口映射到本机的 8080 端口,要映射成功就要先把端口暴露出来,有点类似于防火墙的功能,把部分端口打开。

ENTRYPOINT 和 CMD 要放在一起来说,这两者的功能都类似,但又有相对独特的地方,他们的作用都是让镜像在创建容器时运行里面的命令。当然前提是这个镜像是使用这个 dockerfile 构建的,也就是说在执行 docker run 时 ENTRYPOINT 和 CMD 里面的命令是会执行的,两者是可以单独使用,并不一定要同时存在,当然这两者还是有区别的。

先从 CMD 说吧,CMD 的一个特点就是可被覆盖,比如把之前的 dockerfile 的 ENTRYPOINT 这一行删除,留下 CMD 填写[“nginx”],构建好镜像后直接使用 docker run lorenwe/centos_nginx 命令执行的话通过 docker ps 可以看到容器正常运行了,启动命令也是 “ngixn”,但是我们使用 docker run lorenwe/centos_nginx bin/bash 来启动的话通过 docker ps 查看到启动命令变成了 bin/bash,这就说明了 dockerfile 的 CMD 指令是可被覆盖的,也可以把他看做是容器启动的一个默认命令,可以手动修改的。而 ENTRYPOINT 恰恰相反,他是不能被覆盖,也就是说指定了值后再启动容器时不管你后面写的什么 ENTRYPOINT 里面的命令一定会执行,通常 ENTRYPOINT 用法是为某个镜像指定必须运行的应用,例如我这里构建的是一个 centos_nginx 镜像,也就是说这个镜像只运行 ngixn,那么我就可以在 ENTRYPOINT 写上[“nginx”],有些人在构建自己的基础镜像时(基础镜像只安装了一些必要的库)就只有 CMD 并写上 [‘bin/bash’],当 ENTRYPOINT 和 CMD 都存在时 CMD 中的命令会以 ENTRYPOINT 中命令的参数形式来启动容器,例如上面的示例 dockerfile,在启动容器时会以命令为 nginx -h 来启动容器,遗憾的是这样不能保持容器运行,所以可以这样启动 docker run -it lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf,那么容器启动时运行的命令就是 nginx -c /usr/local/nginx/conf/nginx.conf,是不是很有意思,可以自定义启动参数了。

当然还有一些没有用到的指令:

ARG,ARG指令用以定义构建时需要的参数,比如可以在 dockerfile中写上这句 ARG a_nother_name=a_default_value,ARG指令定义的参数,在docker build命令中以 –build -arg a_name=a_value 形式赋值,这个用的一般比较少。

VOLUME,VOLUME指令创建一个可以从本地主机或其他容器挂载的挂载点,用法是比较多的,都知道 docker 做应用容器比较方便,其实 docker 也可做数据容器,创建数据容器镜像的 dockerfile 就主要是用 VOLUME 指令,要讲明 VOLUME 用法有必要在开一篇文章,再此就不做介绍了,

USER,USER用来切换运行属主身份的。docker 默认是使用 root 用户,但若不需要,建议切换使用者身分,毕竟 root 权限太大了,使用上有安全的风险。LABEL,定义一个 image 标签。

四、构建演示

dockerfile 构建镜像的命令很简单,在我的示例中我的命令是 “docker build -t lorenwe/centos_nginx . “,注意后面的点不能省略,表示的从当前目录中寻找 dockerfile 来构建镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D:\docker\lorenwe>docker build -t lorenwe/centos_nginx .
Sending build context to Docker daemon 995.8kB
Step 1/13 : FROM centos
---> d123f4e55e12
Step 2/13 : MAINTAINER lorenwe
---> Running in e5c7274f50e8
---> 606f7222e69a
Removing intermediate container e5c7274f50e8
Step 3/13 : ENV PATH /usr/local/nginx/sbin:$PATH
---> Running in 23716b428809
---> 5d8ee1b5a899
....
Successfully built eaee6b40b151
Successfully tagged lorenwe/centos_nginx:latest

看到以上内容就说明成功,构建过程可能需要一点点时间,毕竟要安装一些软件,如果你跟我一样是配置的阿里云的容器源构建时应该不会出现什么问题,因为我之前是有拉取过 centos ,所以在 build 时直接使用本地的 centos,如果你没有拉取过 centos,那么在 build 时还会把 centos 拉取下来

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
D:\docker\lorenwe>docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
lorenwe/centos_nginx latest eaee6b40b151 7 minutes ago 427MB
lorenwe/centos_net_tools latest 35f8073cede1 6 days ago 277MB
centos latest d123f4e55e12 3 weeks ago 197MB
d4w/nsenter latest 9e4f13a0901e 14 months ago 83.8kB

D:\docker\lorenwe>docker run -itd --name nginx1 lorenwe/centos_nginx
15d4f108dab7c2f276209ebeb501cac0d3be828e1e81bae22d3fd97c617439eb

D:\docker\lorenwe>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

D:\docker\lorenwe>docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
15d4f108dab7 lorenwe/centos_nginx "nginx -h" nginx1

D:\docker\lorenwe>docker run -itd --name nginx2 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf
b6b0e962ca3056d67c24145b08975ffddb9cc050fce5f09f65310fb323ffc1c3

D:\docker\lorenwe>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b6b0e962ca30 lorenwe/centos_nginx "nginx -c /usr/loc..." 80/tcp nginx2

D:\docker\lorenwe>docker run -itd -p 8080:80 --name nginx3 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf
2f6997745641e3e3edbbfe5213e6235cab3b5a929f116a2c132df504156090c6

D:\docker\lorenwe>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2f6997745641 lorenwe/centos_nginx "nginx -c /usr/loc..." 0.0.0.0:8080->80/tcp nginx3
b6b0e962ca30 lorenwe/centos_nginx "nginx -c /usr/loc..." 80/tcp nginx2

D:\docker\lorenwe>docker stop nginx2
nginx2

其中 “docker run -itd -p 8080:80 –name nginx3 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf” 中的 -p 8080:80 表示把主机的 8080 端口映射到容器的 80 端口,因为之前我们在 dockerfile 中把 80 端口暴露出来了,做好端口映射后现在就可以在主机中打开浏览器访问 127.0.0.1:8080 就能看到 nginx 的欢迎页面了 (^v^).

1
2
D:\docker\lorenwe>docker run -itd -v D:/docker/lorenwe/html:/usr/local/nginx/html  -p 8081:80 --name nginx4 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf
cd2d4eb70a39057aed3bfcb64e1f03433e2054d7ff5d50098f49d2e6f2d9e02e

我再在原来的参数中加入了 -v 参数,其作用就是把一个本地主机的目录挂载到容器内部,这个目录是一个共享的状态,两边都可以进行修改,这就是容器的共享卷,其作用就不言而喻了,现在我们在 D:\docker\lorenwe 的目录下新建一个叫 html 的文件夹,再在 html 文件夹内新建一个 index.html 随便写上一点内容后再去主机浏览器上访问一下 127.0.0.1:8081 看看是不是你想要看到内容。虽然通过 -v 参数可以满足大部分应用场景,但是 docker 的 VOLUME 还有其他更好用法,欲知后事如何,请看下回分解!

Docke入门笔记

一、docker 是什么

其实从很早就开始注意到docker这么一个东西的,学习它陆陆续续踩了不少坑,当初为什么会被他吸引呢?其实很简单,就是被它的速度给惊艳到了,在那个虚拟机大行其道的年代,装好一套环境需要几个小时甚至更久,看见docker是有一种多么的不可思议,从此就走上采坑之旅。
Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在自己机器上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、bare metal、OpenStack 集群和其他的基础应用平台。这是docker的百度百科定义,看完这个好像并不能说明docker有什么用,没关系,对比一下就知道了,首先说明,这篇文章不会涉及很深奥的内容,旨在用通俗易懂的话语来阐述docke的优点及其基础用法。我们都用过虚拟机吧,第一映像肯定是笨重,启动慢,当然功能上是完全无可挑剔的,但这还不够,于是docker横空出世,还有什么语言能比图片更能说明一切呢?

docker

上面这张图就很好的说明一切了,传统的虚拟机是通过软件把计算机的一个个硬件给模拟出来,之后再在模拟出的计算机中安装系统,之后再在系统内部跑应用,就比如需要两个应用,一个跑Nginx一个跑MySQL,那么用虚拟机的话就要安装两次系统,宿主机也就是真实的非虚拟出来的计算机要保存这两个虚拟的系统,占用空间不说最主要的是给宿主机额外的开销,这两个虚拟机的系统都是需要cpu和内存资源的,再来看看docke,可看到的是它只在宿主机内安装个Docker Engine就支持隔离多个应用,注意还是环境隔离的哦,少了两个OS无疑是节约了巨大的资源,这两个应用给我们的感觉就是跑在两个不同的系统中一样,两者通信还要借助特定网络通信,但他们其实是共用的宿主机的系统,至于他们是怎么做到隔离的这又是一个高深的话题了,看到docker有这么多的优点是不是迫不及待的想要试一试了?

二、docker安装及基本配置

在win7中安装是要通过虚拟机来安装的,在win10中就方便多了,有Docker for Windows安装包,linux下下载对应源码编译安装即可,因为平时开发都是在win10上开发,故在这就简单说一下win10安装Docker for Windows后的配置吧,注意在win10中安装docker for window需要有 Hyper-V 支持,不支持这个的也只能是安装虚拟机来体验docker了,安装完后进入设置界面:

docker_settings

给需要共享的磁盘勾选,只有勾选了这个,容器才能访问宿主机的文件,设置这个是需要管理员密码的,没设密码的需要去设置管理员密码,不然不能配置成功,接着就是配置镜像加速了,docker容器是依赖镜像(在docker里面是image)创建的,镜像是从远程拉取的,就跟我们从github上拉取代码一样的,众所周知国内的网路想要拉取一个镜像是多么的不容易,所以就需要配置镜像加速,在Daemon那一栏的Registry mirrors 中填上加速地址就行了,我习惯是使用阿里云的 ‘ https://otsyn80i.mirror.aliyuncs.com ‘ 加速地址,一切都ok了之后就可以开始了我们喜欢的命令行敲命令了!

三、docker基本操作

1
2
->docker --version  #查看版本
Docker version 17.09.0-ce, build afdb6d4

搜索一个image并且拉取下来
下载镜像的命令非常简单,使用docker pull命令即可。在docker的镜像网站上面,镜像都是按照”用户名/镜像名”的方式来存储的。
可以使用docker images查看拉取下来的镜像,其中IMAGE ID就是镜像id,删除镜像时可以使用这个IMAGE ID删除 docker rmi [IMAGE ID,REPOSITORY]

1
2
3
4
5
6
7
8
9
->docker search centos
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
centos The official build of CentOS. 3824 [OK]
...
->docker pull learn/tutorial #拉取镜像
->docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest a7876479f1aa 4 years ago 128MB
->docker rmi a7876479f1aa #删除image

在docker容器中运行hello world,使用docker run创建一个容器,docker run之后就会产生一个容器并且保存起来,之后要运行就可以做直接运行容器而不用从新创建容器,这里的容器就是 container,docker的容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源,包括文件系统、系统类库、shell 环境等等。但这个沙盒默认是不会运行任何程序的。需要在沙盒中运行一个进程来启动某一个容器。这个进程是该容器的唯一进程,所以当该进程结束的时候,容器也会完全的停止。
可以通过 docker ps 查看正在运行的容器,当我们运行docker run centos echo “hello word”后看到输出 “hello word” 后再运行 docker ps 时没有任何容器输出出来,这是因为容器在运行完 echo “hello word” 后没有运行任何进程,所以容器就退出了。通过 docker ps -a 可以查看到所有的 container

1
2
3
4
5
->docker run centos echo "hello word" #通过 centos 镜像创建一个容器来输出 "hello word"
hello word
->docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e46ec634bbe1 centos "echo 'hello word'" 12 minutes ago Exited (0) 12 minutes ago

接下来就是给容器安装一个简单的程序。之前下载的是centos镜像,所以可以使用 yum 命令来安装程序。
备注:yum 命令执行完毕之后,容器就会停止,但对容器的改动不会丢失。但是从 image 重新 run 一个容器出来,之前安装的程序并不存在,因为所有的修改都是针对容器 (container) 的,并不针对 image,所以从 image run 出来的容器都是一个全新的

1
2
3
4
5
->docker run centos yum -y install net-tools
->docker ps -a #可以看到已经存在两个 container
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2f1bbd6c30e3 centos "yum install -y ne..." 1 minutes ago Exited (0) 1 minute ago competent
35129633c933 centos "echo hello word" 1 minutes ago Exited (0) 1 minute ago dazzling

现在容器是有了,需要的程序也安装好了,但就是只有一个容器,不能运行又有什么用,别急下面就来看看运行容器,但运行容器也有几种方法,一种就是在创建容器的时候并运行,之前说过,容器要保持运行就需要一个活动的进程,当容器内所有的进程都退出了,那么容器也就相应的停止了,所以要创建容器并保持运行容器的话很简单,用ping命令就可以了。

1
2
3
4
5
6
7
8
->docker run centos ping lozz.cc
PING lorencoll.coding.me (103.218.240.147) 56(84) bytes of data.
64 bytes from 103.218.240.147 (103.218.240.147): icmp_seq=1 ttl=37 time=16.0 ms
64 bytes from 103.218.240.147 (103.218.240.147): icmp_seq=2 ttl=37 time=16.4 ms
Ctrl+C
->docker ps #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" About a minute ago Up About a minute jolly

有启动就会有停止,停止可以用 docker stop container_id 命令来停止某个容器,好了现在开始讲第二种启动容器的方法,这种方法是在容器已经被创建的情况下使用的,我们知道每次 run 都是创建出一个新的容器,有的时候不需要创建,可以使用 docker start container_id 来启动一个容器,这时就需要注意了,启动后的容器没活动的进程容器依然是会退出的,因为容器是从镜像创建而来的,所以容器也是包含了创建这个容器的附加命令的,在 docker start 的时候又会从新执行一遍创建这个容器的附件命令,有点绕口是不是,敲一遍就知道了。

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
->docker ps    #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" 23 minutes ago Up 23 minutes jolly

->docker ps -a #查看所有存在的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" 21 minutes ago Up 21 minutes jolly
2f1bbd6c30e3 centos "yum install -y ne..." 36 minutes ago Exited (0) 3 minute ago competent
35129633c933 centos "echo hello word" About hour ago Exited (0) hour ago dazzling

# 尝试启动安装了 net-tools 的容器,
# -i 的意思是把信息输出到控制台中,没有这个参数执行后就直接返回一个CONTAINER ID
->docker start -i 2f1bbd6c30e3
Loaded plugins: fastestmirror, ovl
Loading mirror speeds from cached hostfile
* base: mirrors.aliyun.com
* extras: mirrors.aliyun.com
* updates: mirrors.cn99.com
Package net-tools-2.0-0.22.20131004git.el7.x86_64 already installed and latest version
Nothing to do
->docker ps #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" 30 minutes ago Up 30 minutes jolly
->docker stop 10c5c2754279 #停止执行ping命令的容器
10c5c2754279
->docker start -i 10c5c2754279 #启动执行ping命令的容器
PING lorencoll.coding.me (103.72.144.62) 56(84) bytes of data.
64 bytes from 103.72.144.62 (103.72.144.62): icmp_seq=1 ttl=37 time=15.1 ms
64 bytes from 103.72.144.62 (103.72.144.62): icmp_seq=2 ttl=37 time=16.3 ms
64 bytes from 103.72.144.62 (103.72.144.62): icmp_seq=3 ttl=37 time=16.8 ms
Ctrl+C

从上面的提示就可以知道,这个 start 命令把之前创建命令 docker run centos yum -y install net-tools 中的 yum -y install net-tools 也执行了一遍,因为这个容器已经安装过了 net-tools ,所以 yum 才提示不需要安装,查看活动的容器中还是只有之前的容器在运行。那么现在有一个问题,我安装 net-tools 的那个容器不能用了吗,其实不然,这个容器之所以“不能用”只是因为创建的时候没有指定合适的命令,要用它也是可以的,我们把他从新打包成一个 image 也就是镜像,打包好了后再通过它生成容器就可以了,实际上这也符合 Docker 的应用隔离思想,把容器打包成镜像的命令很简单 docker commit container_id image_name,我的打包命令是 docker commit 2f1bbd6c30e3 lorenwe/centos_net_tools,这个命名是有规矩的,前面是 docker 的用户id,如果想要把这个镜像推送到 docker hub 这个名字就不能随便命名了,如果不推送到 docker hub 就无所谓了,但也不能太随便对不对。

1
2
3
4
5
6
7
8
9
10
11
12
->docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" About an hour ago jolly
2f1bbd6c30e3 centos "yum install -y ne..." About an hour ago competent
35129633c933 centos "echo hello word" About an hour ago dazzling
->docker commit 2f1bbd6c30e3 lorenwe/centos_net_tools
sha256:35f8073cede14473601d9f138a9815bc9ab5c7d97f914ca2f5ce910bd78b5750
->docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
lorenwe/centos_net_tools latest 35f8073cede1 23 seconds ago 277MB
centos latest d123f4e55e12 2 weeks ago 197MB
d4w/nsenter latest 9e4f13a0901e 14 months ago 83.8kB

就这么简单,一个新的镜像(image)就创建好了,现在可以通过这个镜像来做些有趣的事情了,依然是 docker run 命令,只不过这次多增加一些参数,如 docker run -itd –name my_net_tools lorenwe/centos_net_tools /bin/bash
其中参数 itd 分别是表示 ‘标准输入给容器’,‘分配一个虚拟终端’,‘以守护进程方式运行(后台)’, –name 自然是指定创建后容器的名称了,/bin/bash 执行bash脚本,执行以上命令,就能创建一个后台运行的容器了。

1
2
3
4
5
6
->docker run -itd --name my_net_tools lorenwe/centos_net_tools /bin/bash
e1d843f7726f67d2635042695e2065b383736a341edd2e83753be9fabec03de0
->docker ps #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e1d843f7726f lorenwe/centos_net_tools "/bin/bash" 7 seconds ago my_net_tools
10c5c2754279 centos "ping lozz.cc" Up 25 minutes jolly

嗯,运行了,然后呢,当然是可以进入容器中玩玩呀,使用 docker attach container_id 进入Docker容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
->docker attach e1d843f7726f
[root@e1d843f7726f /]# ip addr
bash: ip: command not found
[root@e1d843f7726f /]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.3 netmask 255.255.0.0 broadcast 0.0.0.0
ether 02:42:ac:11:00:03 txqueuelen 0 (Ethernet)
RX packets 40 bytes 1900 (1.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

[root@e1d843f7726f /]#

这个因为这个容器是基于 centos 来的,而 docker 的基础 centos 镜像都是精简版的,故很多命令没有,比如这个 ip addr ,这也是之前选择安装 net-tools 的原因,因为安装了这个就可以使用 ifconfig 来查看网卡配置。至此,是不是对 docker 有了一个基本的了解了呢,是不是突然来了一个灵感,比如想要搭建一个 Nginx 静态服务器,步骤就是 pull 一个基础镜像下来,通过这个基础镜像 run 出一个容器后再容器内安装上 Nginx 之后再从新打包成一个新的镜像就可以了。实际上使用 docke 构建镜像不会那样去做,步骤繁琐不说,还不是很灵活,毕竟之前构建的 lorenwe/centos_net_tools 镜像也有277MB呢!分发起来不太方便,于是 Dockerfile 出现了,其作用就是通过特定的格式把一个镜像描述出来,通过 docker 来构建出这个镜像,描述的流程其实和之前的手动构建的流程差不多,通过同一个 Dockerfile 可以构建出一模一样的环境,这也是 docke 常用于工作中统一系统运行环境的原因。

四、把 docker 推送到 docker hub

推送镜像的命令很简单,只需要 docker push image_name 就可以了,在把镜像推送到 docker hub 之前还是有一些准备工作要做的,需要先去 docker hub 注册一个账号,之后再在 docker 中登入这个账号就可以推送镜像了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
->docker login  #登入到 docker hub
Login with your Docker ID to push and pull images from Docker Hub.
If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username (lorenwe): lorenwe
Password:
Login Succeeded
# 现在就可推送镜像到 docker hub
->docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
lorenwe/centos_net_tools latest 35f8073cede1 About an hour ago 277MB
centos latest d123f4e55e12 2 weeks ago 197MB
d4w/nsenter latest 9e4f13a0901e 14 months ago 83.8kB
->docker push lorenwe/centos_net_tools
The push refers to a repository [docker.io/lorenwe/centos_net_tools]
d0ba94ecc37e: Pushed
cf516324493c: Mounted from library/centos
latest: digest: sha256:276814315437cf5d416ed4b5713fe10c914beaea96bcf583b786a6778c80830f size: 741

由于墙和 docker hub 的服务器离天朝远的原因推送会很慢,成功后就能在 docker hub 的个人中心看到自己推送的镜像了,以后想要在使用这个镜像就可以直接 docker pull 就行了,也可以把镜像推送到国内的容器镜像服务平台,比如我用的是阿里云的容器Hub,可能是因为速度快吧所以用着舒服了,唯一不好的就是那又臭又长的镜像名称有点难受。

五、结语

docker 基础到这一步已经是介绍的差不多了,写这篇文章的目的就是用通俗易懂的语言及示例来讲明 docker 镜像与容器之间的关系,限于篇幅 docker 还有很多功能没有介绍,日后有时间再来做后续的文章,例如 Dockerfile,docker 卷的共享和网络通信等等都是一些特别有意思的功能,模拟构建一个可移植的分布式的开发平台已经不是梦了。处女作,各位轻喷!

PHP中的匿名函数和闭包

一:匿名函数 (在php5.3.0 或以上才能使用)

php中的匿名函数, 也叫闭包函数(closures), 允许指定一个没有名称的函数。最常用的就是回调函数的参数值。
匿名函数的定义:

1
2
3
$closureFunc = function(){
return true;
};

eg: 把匿名函数赋值给变量,通过变量来调用

1
2
3
4
5
$closureFunc = function($str){
   echo $str;
};
$closureFunc("hello world!");
// 输出: hello world!

二:闭包
2.1 将匿名函数放在普通函数中,也可以将匿名函数返回,这就构成了一个简单的闭包
1
2
3
4
5
6
7
8
function closureFunc1(){
$func = function(){
echo "hello";
};
$func();
}
closureFunc1();
//输出: hello
2.2 在匿名函数中引用局部变量
1
2
3
4
5
6
7
8
9
function closureFunc2(){
$num = 1;
$func = function(){
echo $num;
};
$func();
}
closureFunc2();
//Notice: Undefined variable: num

如果经常写js代码看到这个的话应该会感到很奇怪,因为类似的代码放在js里面一切都是那么的合理,根本就没有报错的理由。那是因为js语言是自动封装应用状态的,在php中,必须手动调用bindTo()方法或者使用use关键字把状态附加到php闭包中。

1
2
3
4
5
6
7
8
9
10
function enclosePerson ($name) {
return function ($doCommand) use ($name) {
return sprintf('%s, %s', $name, $doCommand);
};
}
// 把字符串“Clay”封装在闭包中
$clay = enclosePerson('Clay');
// 传入参数,调用闭包
echo $clay('get me sweet tea!');
// 输出 --> “Clay, get me sweet tea!”

可以看到在生成闭包的时候把变量附加到闭包中,那么在调用闭包时那个变量值依然存在并且不能被改变。

php的闭包是对象,与其他php对象类似,每个闭包实例都可以使用 $this 关键字来获取闭包内部状态,闭包的大部分状态是没什么用的,但有一个bindTo()方法为闭包添加一些有趣的潜力,可以使用这个方法把Closure对象的内部状态绑定到其他对象上。没看懂是吗?我来用通俗易懂的话来说一遍,通过这个bindTo()方法可以把其他对象的$this绑定到闭包对象中,那么就能在闭包对象中访问到被绑定的对象的受保护的属性。举个栗子:

下面的例子是一些框架中路由实现的mini版,仅为说明bindTo()的作用及用法。

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
class App 
{
protected $routes = array();
protected $responseStatus = '200 OK';
protected $responseContentType = 'text/html';
protected $responseBody = 'hello word';

public function addRoute($routePath, $routeCallback)
{
$this->routes[$routePath] = $routeCallback->bindTo($this, __CLASS__);
}

public function dispatch($currentPath)
{
foreach ($this->routes as $routePath => $callback){
if ($routePath === $CurrentPath) {
$callback();
}
}
header('HTTP/1.1' . $this->$responseStatus);
header('Content-type:' . $this->$responseContentType);
header('Content-length:' . $this->$responseBody);
echo $this->$responseBody;
}
}

可以看到,addRoute方法是用来添加(注册)路由的,注册的同时会要求传如一个闭包函数,那么在注册的同时就通过bindTo()方法绑定了App实例的$this,bindTo()方法第二个参数的作用就是指定需要绑定闭包对象的那个实例所属的PHP类。

1
2
3
4
5
6
$app = new App();
$app->addRoute('/user/info', function(){
$this->$responseContentType = 'application/json;charset=utf8';
$this->$responseBody = '{'name': 'july'}';
});
$app->dispatch('/user/info');

composer 笔记

一、composer 安装

1、局部安装

composer 中国官网 上下载 composer.phar 文件到工作目录
上述下载 Composer 的过程正确执行完毕后,可以将 composer.phar 文件复制到任意目录(比如项目根目录下),然后通过 php composer.phar 指令即可使用 Composer 了!
全局安装
全局安装是将 Composer 安装到系统环境变量 PATH 所包含的路径下面,然后就能够在命令行窗口中直接执行 composer 命令了。
Mac 或 Linux 系统:

打开命令行窗口并执行如下命令将前面下载的 composer.phar 文件移动到 /usr/local/bin/ 目录下面:

复制
sudo mv composer.phar /usr/local/bin/composer
Windows 系统:

找到并进入 PHP 的安装目录(和你在命令行中执行的 php 指令应该是同一套 PHP)。
将 composer.phar 复制到 PHP 的安装目录下面,也就是和 php.exe 在同一级目录。
在 PHP 安装目录下新建一个 composer.bat 文件,并将下列代码保存到此文件中。
复制

1
@php "%~dp0composer.phar" %*

最后重新打开一个命令行窗口试一试执行 composer –version 看看是否正确输出版本号。

不要忘了经常执行 composer selfupdate 以保持 Composer 一直是最新版本哦!

composer 查看配置命令

1
composer config -gl

配置中国镜像源
修改 composer 的全局配置文件(推荐方式)

1
composer config -g repo.packagist composer https://packagist.phpcomposer.com

修改当前项目的 composer.json 配置文件(只对当前项目生效)

1
composer config repo.packagist composer https://packagist.phpcomposer.com

或者在composer.json文件中手工添加

1
2
3
4
5
6
"repositories": {
"packagist": {
"type": "composer",
"url": "https://packagist.phpcomposer.com"
}
}

PHP调试必经之路xdebug

在我刚从事PHP开发的初期,一次偶然的机会,知道PHP有一个专门用来调试的工具“xbebug”,当时就抱着好奇的心态去了解它,没想到我会用时就感觉像是打开了新世界的大门样的,现在想想还真是有点搞笑,现在开通了博客后,我就想着把我所学的全部整理成日志,若是以后遗忘了,也可以再翻出来看看,同时也是希望那些PHP初学者看到这篇文章之后能够少走一些弯路,毕竟走过的才知道这条路到底有多难!废话少说,现在就来开始介绍PHP xdebug 本地调试以及远程调试的步骤。

xdebug安装就不多做介绍了,毕竟win和linux上安装方式不一样,现在就默认已经给PHP安装好了xdebug扩展,下面是xdebug的配置文件,在php.ini文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Xdebug]  
;指定Xdebug扩展文件的绝对路径
zend_extension="/usr/local/php5/zend_ext/xedbug.so"
;启用性能检测分析
xdebug.profiler_enable=on
;启用代码自动跟踪
xdebug.auto_trace=on
;允许收集传递给函数的参数变量
xdebug.collect_params=on
;允许收集函数调用的返回值
xdebug.collect_return=on
;指定堆栈跟踪文件的存放目录
xdebug.trace_output_dir="/tmp/log/php/debug"
;指定性能分析文件的存放目录
xdebug.profiler_output_dir="/tmp/log/php/debug"
xdebug.profiler_output_name = cachegrind.out.%t.%p

以上是本地调试的配置方式,远程调试与这大体相同,随后将奉上远程调试配置方式及详细说明,配置好了xdebug后(phpinfo能够看到xdebug扩展)接下来就来使用这个扩展来进行调试了,其中需要注意两个配置,第一就是xdebug.profiler_enable这个配置开启后,会在配置的性能分析文件的存放目录中自动生成cachegrind.out文件,该文件保存的是整个请求程序运行的性能分析数据,第二就是xdebug.auto_trace配置,该配置是开启自动代码运行跟踪,保存的是代码运行每一步的数据,开启后保存的是整个请求周期代码运行的跟踪数据,项目太大记录的数据更多那么这个文件也会相应的变大,所以一般配置时很少开启这个因为有时只需要跟踪某个函数或对象的运行情况,如果只要跟踪代码片段只需要在跟踪的代码加上以下代码即可:

1
2
3
xdebug_start_trace();
//* 业务代码 *
xdebug_stop_trace();

值得注意的地方就是当配置 xdebug.auto_trace=on 时,xdebug_start_trace()函数会报错,所以一般配置 xdebug.auto_trace=off ,在需要跟踪的地方手动加入调试代码即可。

在实际开发中还是很少用到上面的调试方式的,毕竟太麻烦了,而且xdebug输出的文件看的有点杂乱无章,无形增加了调试难度,有没有一种更友好的调试方式呢?当然有!配合IDE调试简直不要太舒服,这种调试就是俗称的远程链接调试,只不过我们开发跟服务都是在同一台电脑上,所以看起来像本地调试,但实际调试与程序之间数据是通过网络传输的,远程调试呢肯定是不能用上面的配置文件的,所以需要从新配置一个xdebug配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Xdebug]
;指定Xdebug扩展文件的绝对路径
zend_extension="/usr/local/php5/zend_ext/xedbug.so"
;开启远程调试
xdebug.remote_enable = on
;关闭性能检测分析(不输出分析文件了)
;xdebug.profiler_enable = off
;xdebug.profiler_enable_trigger = off
;xdebug.profiler_output_dir="D:\phpStudy\tmp\xdebug"

;2.1或以上版本只支持’dbgp’作为协议,所以这个值基本是固定的
xdebug.remote_handler = dbgp
;调试的机器的所在ip,如果是本机开发则写localhost即可
xdebug.remote_host = localhost
;调试机器的端口,选择一个不被占用的端口即可
xdebug.remote_port = 9001
;调试IDE标识
xdebug.idekey="PHPSTORM"

以上配置仅满足于本机当做服务器的情况下,当我的服务器是虚拟机虚拟出来的怎么办,其实一样的,改一改相应配置就行了,比如虚拟机地址是 192.168.10.10 ,本机的地址肯定就是 192.168.10.1 了,那么配置就如下:

1
2
3
4
5
6
7
;指定Xdebug扩展文件的绝对路径  
zend_extension="/usr/local/php5/zend_ext/xedbug.so"
xdebug.remote_enable = on
xdebug.remote_handler = dbgp
xdebug.remote_host = 192.168.10.1
xdebug.remote_port = 9001
xdebug.idekey="PHPSTORM"

其中xdebug.remote_port配置的也是调试机器的端口,所以被占用的话就需要更换了,以上的配置虽然能进行远程调试,但是只能单独一个人进行调试,如果需要多个开发都能调试,就必须用到另外一个配置 xdebug.remote_connect_back,把 xdebug.remote_host 去掉换成 xdebug.remote_connect_back=1 即可,换成这个配置后 xdebug 是通过 HTTP 头部来获取 IP 地址,这样也就支持多人调试了,但要记住的是这个配置千万不能在生产环境中出现,为什么你懂得!正常来说生产环境应该来 xdebug 都不应该开启的,但凡事都有特殊。

一切都配置完毕了后,重启服务,就开始配置IDE了,我用的是PhpStrom,所以就介绍这个了,配置起来也比较简单

写好后只需要运行一遍代码就行了,对没错,可以了,要看到调试的代码到底调用关系以及函数参数怎么办呢?看到上面配置的性能分析文件目录没有,就是在那里看,打开目录里面的文件就可以看到了,是不是感觉这样看的好难受,根本就没有像其他语言调试那种快感,要达到这种要求,就需要引入IDE了,我用的是PhpStrom,应该没有哪个phper不知道这个的吧,像其他的比如Sublime Text我也用来调试过,都没有PhpStrom这么爽,所以下面来介绍PhpStrom配置吧,配置这种描述是描述不清楚的,上图:

docker
docker
docker
docker
docker

配置 DBGp Proxy 的 Host 时候如果是远程服务器就需要填上远程服务器的 IP 地址,比如上面 xdebug 配置了一个 192.168.10.10 的远程服务器,那么此时这里的 host 就应该填上 192.168.10.10,其中配置 Server 的时候也需要特别注意,如果是本机做服务器的话就不需要配置目录映射,也就是在 Absolute path on the server 不需要填写内容,如果是通过远程服务器或者虚拟机做服务器的话就需要填上源码在远程服务器上的绝对路径,自此一切配置都以完成,就可以开始打上断点愉快的调试代码
了 ^v^ …