F's Blog

博客 收藏夹
Docker

11 Apr 2017

Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。

Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。运行的容器的操作其实是基于 Docker 服务层的。

概念

-仓库(Registry):仓库用来保存镜像,可以理解为代码控制中的代码仓库。 -Docker Hub 提供了庞大的镜像集合供使用。 -Docker Machine:Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure。

镜像

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

容器

Docker 容器通过 Docker 镜像来创建。

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。

一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。这一点就更像进程了,除非把运行时存在磁盘,进程运行结束内存里的数据就会消失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器可以随意删除、重新 run,数据却不会丢失。

Docker 起一个容器其实就是启动了一个进程。

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。 传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。 容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。

安装

一句安装:

wget -qO- https://get.docker.com/ | sh

当要以非root用户可以直接运行docker时,需要执行命令:

sudo usermod -aG docker myname

然后重新登陆,否则会 permision deny.

hellow world:

sudo service docker status

# 下载并运行 hello-world 镜像
docker run hello-world

快速使用

Docker 允许你在容器内运行应用程序, 使用 docker run 命令来在容器内运行一个应用程序。

docker run  ubuntu /bin/echo "hello world"

通过docker的两个参数 -i -t,让docker运行的容器实现”对话”的能力:

docker run -it ubuntu bash

各个参数解析: -t:在新容器内指定一个伪终端或终端。 -i:允许你对容器内的标准输入 (STDIN) 进行交互。

后台执行:

docker run -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"
# 返回 [container_id]

docker ps

docker logs [container_id]

docker stop [container_id]

运行 web 应用:

docker run -d -P training/webapp python app.py

docker logs -f 7a38a1ad55c6
docker top 7a38a1ad55c6
docker inspect 7a38a1ad55c6

docker stop 7a38a1ad55c6
# 重启容器
docker start 7a38a1ad55c6
docker rm 7a38a1ad55c6

通过 docker ps 可以看到端口映射 0.0.0.0:32768->5000/tcp,可以将 Flask 的端口映射到了 Host 的 32768。

参数说明:

-d:让容器在后台运行。 -P:将容器内部使用的网络端口映射到我们使用的主机上。 -f:让 dokcer logs 像使用 tail -f 一样来输出容器内部的标准输出。

详细使用

镜像使用

当运行容器时,使用的镜像如果在本地中不存在,docker 就会自动从 docker 镜像仓库中下载,默认是从 Docker Hub 公共镜像源下载。

查看镜像

docker images

各个选项说明:

同一仓库源可以有多个 TAG,代表这个仓库源的不同个版本,如ubuntu仓库源里,有15.10、14.04 等多个不同的版本,我们使用 REPOSTITORY:TAG 来定义不同的镜像。如果不指定一个镜像的版本标签,例如你只使用 ubuntu,docker 将默认使用 ubuntu:latest 镜像。

其他命令:

更新镜像

docker run -it ubuntu
apt-get update
exit

docker commit -m="has update" -a="someone" e218edb10161 someone/ubuntu:v2

个参数说明:

-m:提交的描述信息 -a:指定镜像作者

构建镜像

通过 Dockerfile 来构建镜像。

docker build -t centos:6.7 .

参数说明:

-t :指定要创建的目标镜像名 . :构建的上下文,一般是Dockerfile 文件所在目录

设置标签:

docker tag 860c279d2fec xx/centos:dev

容器连接

docker run 参数:

-a, --attach=[]             Attach to STDIN, STDOUT or STDERR
--add-host=[]               Add a custom host-to-IP mapping (host:ip)
--blkio-weight=0            Block IO (relative weight), between 10 and 1000
-c, --cpu-shares=0          CPU shares (relative weight)
--cap-add=[]                Add Linux capabilities
--cap-drop=[]               Drop Linux capabilities
--cgroup-parent=            Optional parent cgroup for the container
--cidfile=                  Write the container ID to the file
--cpu-period=0              Limit CPU CFS (Completely Fair Scheduler) period
--cpu-quota=0               Limit the CPU CFS quota
--cpuset-cpus=              CPUs in which to allow execution (0-3, 0,1)
--cpuset-mems=              MEMs in which to allow execution (0-3, 0,1)
-d, --detach=false          Run container in background and print container ID
--device=[]                 Add a host device to the container
--dns=[]                    Set custom DNS servers
--dns-search=[]             Set custom DNS search domains
-e, --env=[]                Set environment variables
--entrypoint=               Overwrite the default ENTRYPOINT of the image
--env-file=[]               Read in a file of environment variables
--expose=[]                 Expose a port or a range of ports
-h, --hostname=             Container host name
--help=false                Print usage
-i, --interactive=false     Keep STDIN open even if not attached
--init=                     Run container following specified init system container method (systemd)
--ipc=                      IPC namespace to use
-l, --label=[]              Set meta data on a container
--label-file=[]             Read in a line delimited file of labels
--link=[]                   Add link to another container
--log-driver=               Logging driver for container
--log-opt=[]                Log driver options
--lxc-conf=[]               Add custom lxc options
-m, --memory=               Memory limit
--mac-address=              Container MAC address (e.g. 92:d0:c6:0a:29:33)
--memory-swap=              Total memory (memory + swap), '-1' to disable swap
--name=                     Assign a name to the container
--net=bridge                Set the Network mode for the container
--oom-kill-disable=false    Disable OOM Killer
-P, --publish-all=false     Publish all exposed ports to random ports
-p, --publish=[]            Publish a container's port(s) to the host
--pid=                      PID namespace to use
--privileged=false          Give extended privileges to this container
--read-only=false           Mount the container's root filesystem as read only
--restart=no                Restart policy to apply when a container exits
--rm=false                  Automatically remove the container when it exits
--security-opt=[]           Security Options
--sig-proxy=true            Proxy received signals to the process
-t, --tty=false             Allocate a pseudo-TTY
-u, --user=                 Username or UID (format: <name|uid>[:<group|gid>])
--ulimit=[]                 Ulimit options
--uts=                      UTS namespace to use
-v, --volume=[]             Bind mount a volume
--volumes-from=[]           Mount volumes from the specified container(s)
-w, --workdir=              Working directory inside the container

端口

docker run -d -P training/webapp python app.py

也可以用 -p 来映射,两种方式的区别是:

-P :是容器内部端口随机映射到主机的高端口。 -p : 是容器内部端口绑定到指定的主机端口。

docker run -d -p 127.0.0.1:5001:5002 training/webapp python app.py

上面的例子中,默认都是绑定 tcp 端口,如果要绑定 UDP 端口,可以在端口后面加上 /udp

目录

docker run -d -p 80:80 --name mynginx -v $PWD/www:/www -v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf -v $PWD/logs:/wwwlogs nginx

命令说明: -d 后台运行 -p 80:80:将容器的80端口映射到主机的80端口 –name mynginx:将容器命名为mynginx -v $PWD/www:/www:将主机中当前目录下的www挂载到容器的/www -v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf:将主机中当前目录下的nginx.conf挂载到容器的/etc/nginx/nginx.conf -v $PWD/logs:/wwwlogs:将主机中当前目录下的logs挂载到容器的/wwwlogs

更新

比如之前自动启动的容易现在不想了,可以用 update 命令:

docker update --restart=no my-container

镜像

Dockerfile 命令

Dockerfile 定义了一个镜像,一般分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

这里一个例子 Dockerfile

# 基础镜像
FROM ubuntu:16.04

# 对镜像进行配置 -->
RUN apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get -y upgrade \
    && DEBIAN_FRONTEND=noninteractive apt-get -y install strongswan iptables uuid-runtime ndppd openssl \
    && rm -rf /var/lib/apt/lists/* # cache busted 20160406.1

RUN rm /etc/ipsec.secrets
RUN mkdir /config
RUN (cd /etc && ln -s /config/ipsec.secrets .)

ADD ./etc/* /etc/
ADD ./bin/* /usr/bin/

VOLUME /etc
VOLUME /config

# http://blogs.technet.com/b/rrasblog/archive/2006/06/14/which-ports-to-unblock-for-vpn-traffic-to-pass-through.aspx
EXPOSE 500/udp 4500/udp
# <-- 镜像配置结束

# 容器的主进程,每次启动时都会执行
CMD /usr/bin/start-vpn

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。 这是很重要的一步,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

比如下面这条命令就不对:

CMD service nginx start

使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。 而 CMD service nginx start 会被理解为 CMD [ “sh”, “-c”, “service nginx start”],因此主进程实际上是 sh。 那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

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

构建

让我构建一个简单的 Nginx 镜像:

mkdir mynginx
cd mynginx

Dockfile:

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

Build:

docker build -t mynginx:v1 .

之后就会在 docker images 里看到了。

docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。

不要忘了上面命令中的这个 .,实际上是在指定上下文的目录,Dockerfile 里的 COPY 等命令的执行环境。

之后就可以跑起来了:

docker run -P mynginx:v1

这是方便啊!

docker-compose

Compose 如其名,复合,把多个 Docker 组合起来。

在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。

Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。

Compose 中有两个重要的概念:

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。

Compose 项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。

# 在有 docker-compose.yaml 文件目录下运行就可启动项目
docker-compose up

docker-compose.yaml

restart

其它

到在一个已经运行的容器里看看:

docker exec -i -t 740e78a3406f bash

感想

Docker 把程序员从环境部署中解脱了出来。而且不想虚拟机,是轻量级的进程。

把 Docker 容器当成一个进程,就能更好理解怎么去用它,如何配置等。比如数据要和容器分离,需要有一个前台持续运行的主程序,即这个容器运行的程序,其它的配置等都是为它铺垫的啊!

有了 Docker,就可以把所有服务 Docker 化,这样可以做到有效隔离,又方便部署、扩展和升级。真是让我激动不已!

参考

本文由 付豪 创作,采用署名 4.0 国际(CC BY 4.0)创作共享协议进行许可,详细声明