Docker#Dockfile容器镜像制作

Docker容器管理及镜像制作

Docker容器管理及镜像制作
一:创建自己的镜像
1.将容器的文件系统打包成tar包
将容器的文件系统打包成tar文件,也就是把正在运行的容器直接导出为tar包的镜像文件。
导出:

export    
    Export a container's filesystem as a tar archive
有两种方式(elated_lovelace为容器名):
第一种:
docker export -o elated_lovelace.tar elated_lovelace
第二种:
docker export 容器名称 > 镜像.tar

导入:

导入镜像归档文件到其他宿主机:
import    
    Import the contents from a tarball to create a filesystem image
docker import elated_lovelace.tar  elated_lovelace:v1 

注意:
如果导入镜像时没有起名字,随后可以单独起名字(没有名字和tag),可以手动加tag。

docker tag 镜像ID mycentos:7 

2.镜像迁移
保存一台宿主机上的镜像为tar文件,然后可以导入到其他的宿主机上。

save      
    Save an image(s) to a tar archive
    将镜像打包,与下面的load命令相对应
[root@xingdian ~]# docker save -o nginx.tar nginx
load      
    Load an image from a tar archive or STDIN
    与上面的save命令相对应,将上面sava命令打包的镜像通过load命令导入
docker load < nginx.tar

注:
1.tar文件的名称和保存的镜像名称没有关系
2.导入的镜像如果没有名称,自己打tag起名字
扩展:export和save的区别
export:相当于容器快照,容器快照文件将丢弃所有的历史记录和元数据信息
save:没有这个现象,就是完整的
3.通过容器创建本地镜像
背景:
容器运行起来后,又在里面做了一些操作,并且要把操作结果保存到镜像里。
方案:
使用 docker commit 指令,把一个正在运行的容器,直接提交为一个镜像。commit 是提交的意思,类似告诉svn服务器我要生成一个新的版本。
例子:
在容器内部新建了一个文件

[root@xingdian ~]# docker exec -it 4ddf4638572d /bin/sh  
root@4ddf4638572d:/app# touch test.txt
root@4ddf4638572d:/app# exit
#  将这个新建的文件提交到镜像中保存
[root@xingdian ~]# docker commit 4ddf4638572d xingdian/helloworld:v2

例子:

[root@xingdian ~]# docker commit -m "my images version1" -a "xingdian" 108a85b1ed99 daocloud.io/ubuntu:v2
sha256:ffa8a185ee526a9b0d8772740231448a25855031f25c61c1b63077220469b057
    -m                        添加注释
    -a                        作者
    108a85b1ed99              容器环境id
    daocloud.io/ubuntu:v2     镜像名称:hub的名称/镜像名称:tag 
    -p,–pause=true           提交时暂停容器运行

二:利用Dockerfile创建镜像
通过Dockerfile创建镜像,虽然可以自己制作 rootfs(根文件系统),但Docker 提供了一种更便捷的方式,叫作 Dockerfile。
docker build命令用于根据给定的Dockerfile和上下文以构建Docker镜像。
docker build语法

[root@xingdian ~]# docker build [OPTIONS] <PATH | URL | ->
  1. 常用选项说明
    –build-arg,设置构建时的变量
    –no-cache,默认false。设置该选项,将不使用Build Cache构建镜像
    –pull,默认false。设置该选项,总是尝试pull镜像的最新版本
    –compress,默认false。设置该选项,将使用gzip压缩构建的上下文
    –disable-content-trust,默认true。设置该选项,将对镜像进行验证
    –file, -f,Dockerfile的完整路径,默认值为‘PATH/Dockerfile’
    –isolation,默认–isolation=“default”,即Linux命名空间;其他还有process或hyperv
    –label,为生成的镜像设置metadata
    –squash,默认false。设置该选项,将新构建出的多个层压缩为一个新层,但是将无法在多个镜像之间共享新层;设置该选项,实际上是创建了新image,同时保留原有image。
    –tag, -t,镜像的名字及tag,通常name:tag或者name格式;可以在一次构建中为一个镜像设置多个tag
    –network,默认default。设置该选项,Set the networking mode for the RUN instructions during build
    –quiet, -q ,默认false。设置该选项,Suppress the build output and print image ID on success
    –force-rm,默认false。设置该选项,总是删除掉中间环节的容器
    –rm,默认–rm=true,即整个构建过程成功后删除中间环节的容器
  2. PATH | URL | -说明
    给出命令执行的上下文。
    上下文可以是构建执行所在的本地路径,也可以是远程URL,如Git库、tarball或文本文件等。如果是Git库,如https://github.com/docker/rootfs.git#container:docker,则隐含先执行git clone --depth 1 --recursive,到本地临时目录;然后再将该临时目录发送给构建进程。
    构建镜像的进程中,可以通过ADD命令将上下文中的任何文件(注意文件必须在上下文中)加入到镜像中。
    -表示通过STDIN给出Dockerfile或上下文。
    示例:
[root@xingdian ~]#  docker build - < Dockerfile

说明:该构建过程只有Dockerfile,没有上下文

说明:其中Dockerfile位于context.tar.gz的根路径

[root@xingdian ~]# docker build -t champagne/bbauto:ltest -t champagne/bbauto:v2.1 .
[root@xingdian ~]# docker build -f dockerfiles/Dockerfile.debug -t myapp_debug .

  1. 创建镜像所在的文件夹和Dockerfile文件
    命令:
mkdir sinatra 
cd sinatra 
touch Dockerfile 
  1. 在Dockerfile文件中写入指令,每一条指令都会更新镜像的信息
# This is a comment 
FROM daocloud.io/library/ubuntu 
MAINTAINER xingdian [email protected]
RUN apt-get update && apt-get install -y ruby ruby-dev 

格式说明:
每行命令都是以 INSTRUCTION statement 形式,就是命令+ 清单的模式。命令要大写,"#"是注解。
FROM 命令是告诉docker 我们的镜像什么。
MAINTAINER 是描述 镜像的创建人。
RUN 命令是在镜像内部执行。就是说他后面的命令应该是针对镜像可以运行的命令。
5.创建镜像
命令:

docker build -t xingdian/sinatra:v2 .

docker build 是docker创建镜像的命令
-t 是标识新建的镜像
sinatra是仓库的名称
:v2 是tag
"."是用来指明 我们的使用的Dockerfile文件当前目录的
详细执行过程:

 [root@master sinatra]# docker build -t xingdian/sinatra:v2 . 
        Sending build context to Docker daemon 2.048 kB
        Step 1 : FROM daocloud.io/ubuntu:14.04
        Trying to pull repository daocloud.io/ubuntu ... 
        14.04: Pulling from daocloud.io/ubuntu
        f3ead5e8856b: Pull complete 
        Digest: sha256:ea2b82924b078d9c8b5d3f0db585297a5cd5b9c2f7b60258cdbf9d3b9181d828
         ---> 2ff3b426bbaa
        Step 2 : MAINTAINER xingdian [email protected]
         ---> Running in 948396c9edaa
         ---> 227da301bad8
        Removing intermediate container 948396c9edaa
        Step 3 : RUN apt-get update && apt-get install -y ruby ruby-dev
         ...
        Step 4 : RUN gem install sinatra
        ---> Running in 89234cb493d9

6.创建完成后,从镜像创建容器

[root@xingdian ~]# docker run -t -i xingdian/sinatra:v2 /bin/bash

三:理解容器文件系统(课外阅读了解)
问题:
Namespace 的作用是"隔离",它让应用进程只能看到该 Namespace 内的"世界";而 Cgroups 的作用是"限制",它给这个"世界"围上了一圈看不见的墙。这么一折腾,进程就真的被"装"在了一个与世隔绝的房间里,而这些房间就是 PaaS 项目赖以生存的应用"沙盒"。
可是,还有一个问题:这个房间四周虽然有了墙,但是如果容器进程低头一看地面,又是怎样一副景象呢?
换句话说,容器里的进程看到的文件系统又是什么样子的呢?
可能你立刻就能想到,这一定是一个关于 Mount Namespace 的问题:容器里的应用进程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目录(比如 /tmp)下进行操作,而完全不会受宿主机以及其他容器的影响。
那么,真实情况是这样吗?
一段小程序:作用是,在创建子进程时开启指定的 Namespace。使用它来验证一下刚刚提到的问题。

#define _GNU_SOURCE
#include <sys/mount.h> 
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
    
    
  "/bin/bash",
  NULL
};
int container_main(void* arg)
{
    
      
  printf("Container - inside the container!\n");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}
int main()
{
    
    
  printf("Parent - start a container!\n");
  int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
  waitpid(container_pid, NULL, 0);
  printf("Parent - container stopped!\n");
  return 0;
}

代码功能非常简单:在 main 函数里,通过 clone() 系统调用创建了一个新的子进程 container_main,并且声明要为它启用 Mount Namespace(即:CLONE_NEWNS 标志)。
而这个子进程执行的,是一个"/bin/bash"程序,也就是一个 shell。所以这个 shell 就运行在了 Mount Namespace 的隔离环境中。
编译一下这个程序:

[root@xingdian ~]# gcc -o ns ns.c
[root@xingdian ~]# ./ns
Parent - start a container!
Container - inside the container!

这样,就进入了这个"容器"当中(表面上看不大出来-xingdian注)。可是,如果在"容器"里执行一下 ls 指令的话,就会发现一个有趣的现象: /tmp 目录下的内容跟宿主机的内容是一样的。

[root@xingdian ~]# ls /tmp
# 你会看到好多宿主机的文件

也就是说:
即使开启了 Mount Namespace,容器进程看到的文件系统也跟宿主机完全一样。
这是怎么回事呢?
仔细思考一下,你会发现这其实并不难理解:Mount Namespace 修改的,是容器进程对文件系统"挂载点"的认知。但是,这也就意味着,只有在"挂载"这个操作发生之后,进程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。
这时,你可能已经想到了一个解决办法:创建新进程时,除了声明要启用 Mount Namespace 之外,还可以告诉容器进程,有哪些目录需要重新挂载,就比如这个 /tmp 目录。于是,在容器进程执行前可以添加一步重新挂载 /tmp 目录的操作:

int container_main(void* arg)
{
    
    
  printf("Container - inside the container!\n");
  // 如果你的机器的根目录的挂载类型是 shared,那必须先重新挂载根目录
  // mount("", "/", NULL, MS_PRIVATE, "");
  mount("none", "/tmp", "tmpfs", 0, "");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}

在修改后的代码里,在容器进程启动之前,加上了一句 mount(“none”, “/tmp”, “tmpfs”, 0, “”) 语句。就这样,告诉了容器以 tmpfs(内存盘)格式,重int container_main(void* arg)

{
    
      
  printf("Container - inside the container!\n");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}新挂载了 /tmp 目录。

编译执行修改后的代码结果又如何呢?试验一下:

[root@xingdian ~]# gcc -o ns ns.c
[root@xingdian ~]# ./ns
Parent - start a container!
Container - inside the container!
[root@xingdian ~]# ls /tmp
这次 /tmp 变成了一个空目录,这意味着重新挂载生效了。
用 mount -l 检查:
[root@xingdian ~]# mount -l | grep tmpfs
none on /tmp type tmpfs (rw,relatime)

容器里的 /tmp 目录是以 tmpfs 方式单独挂载的。可以卸载一下/tmp目录看看效果。
更重要的是,因为创建的新进程启用了 Mount Namespace,所以这次重新挂载的操作,只在容器进程的 Mount Namespace 中有效。如果在宿主机上用 mount -l 来检查一下这个挂载,你会发现它是不存在的:
在宿主机上

[root@xingdian ~]# mount -l | grep tmpfs

这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。
我们希望的是:每当创建一个新容器时,我希望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎么才能做到这一点呢?
可以在容器进程启动之前重新挂载它的整个根目录"/"。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。
在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。顾名思义,它的作用就是帮你"change root file system",即改变进程的根目录到你指定的位置。
假设,现在有一个 /home/xingdian/test 目录,想要把它作为一个 /bin/bash 进程的根目录。
首先,创建一个 test 目录和几个 lib 文件夹:

[root@xingdian ~]# mkdir -p /home/xingdian/test
[root@xingdian ~]# mkdir -p /home/xingdian/test/{bin,lib64,lib}
然后,把 bash 命令拷贝到 test 目录对应的 bin 路径下:
# cp -v /bin/{bash,ls}  /home/xingdian/test/bin

接下来,把 ls和bash命令需要的所有 so 文件,也拷贝到 test 目录对应的 lib 路径下。找到 so 文件可以用 ldd 命令:(ldd 列出动态依赖库)


```bash
[root@xingdian ~]# list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
[root@xingdian ~]# for i in $list; do cp -v "$i" "/home/xingdian/test/${i}"; done
[root@xingdian ~]# list="$(ldd /bin/bash | egrep -o '/lib.*\.[0-9]')"
[root@xingdian ~]# for i in $list; do cp -v "$i" "/home/xingdian/test/${i}"; done

最后,执行 chroot 命令,告诉操作系统,将使用 /home/xingdian/test 目录作为 /bin/bash 进程的根目录:

```bash
[root@xingdian ~]# chroot /home/xingdian/test /bin/bash

这时,执行 “ls /”,就会看到,它返回的都是 /home/xingdian/test 目录下面的内容,而不是宿主机的内容。
更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被"修改"成 /home/xingdian/test 了。
这种视图被修改的原理,是不是跟我之前介绍的 Linux Namespace 很类似呢?
实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
为了能够让容器的这个根目录看起来更"真实",一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,在容器里通过执行 “ls /” 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。
而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的"容器镜像"。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。
所以,一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等:

[root@xingdian ~]# ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var

而进入容器之后执行的 /bin/bash,就是 /bin 目录下的可执行文件,与宿主机的 /bin/bash 完全不同。
所以,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
1.启用 Linux Namespace 配置;
2.设置指定的 Cgroups 参数;
3.切换进程的根目录(Change Root)。
这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用功能类似
rootfs和kernel:
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核,同一台机器上的所有容器,都共享宿主机操作系统的内核。如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个"全局变量",牵一发而动全身。
在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
这是容器相比于虚拟机的缺陷之一:虚拟机不仅有模拟出来的硬件机器充当沙盒,而且每个沙盒里还运行着一个完整的 Guest OS 给应用随便用。
容器的一致性 :
由于云端与本地服务器环境不同,应用的打包过程,一直是使用 PaaS 时最"痛苦"的一个步骤。
但有了容器镜像(即 rootfs)之后,这个问题被解决了。
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如 Golang 的 Godeps.json。但实际上,一个一直以来很容易被忽视的事实是,对一个应用来说,操作系统本身才是它运行所需要的最完整的"依赖库"。
有了容器镜像"打包操作系统"的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。
这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。
联合文件系统:Union File System 也叫 UnionFS
Docker 公司在实现 Docker 镜像时并没有沿用以前制作 rootfs 的标准流程,做了一个创新:
Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。用到了一种叫作联合文件系统(Union File System)的能力。
主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。
比如,现在有两个目录 A 和 B,它们分别有两个文件:

[root@xingdian ~]# tree
        ├── A
        │  ├── a
        │  └── x
        └── B
          ├── b
          └── x

然后,使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

[root@xingdian ~]# mkdir C
[root@xingdian ~]# yum install funionfs -y   //我这里用的是centos7自带的联合文件系统,效果一样
[root@xingdian ~]# funionfs  -o dirs=./A:./B none ./C

再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

[root@xingdian ~]# tree ./C
        ./C
        ├── a
        ├── b
        └── x

可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是"合并"的含义。
此外,如果在目录 C 里对 a、b、x 文件做修改,这些修改也会在对应的目录 A、B 中生效。

[root@xingdian ~]# echo hello >> C/a
[root@xingdian ~]# cat C/a
hello
[root@xingdian ~]# cat A/a
hello
[root@xingdian ~]# echo hello1 >> A/a
[root@xingdian ~]# cat A/a
hello
hello1
[root@xingdian ~]# cat C/a
hello
hello1

企业级Dockerfile文件构建容器

1.Dockerfile文件构建nginx

[root@xingdian ~]# cat Dockerfile 
FROM centos:7.2.1511
ENV TZ=Asia/Shanghai
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
RUN yum -y install gcc openssl openssl-devel  pcre-devel zlib-devel make
ADD nginx-1.14.0.tar.gz /opt/
WORKDIR /opt/nginx-1.14.0
RUN ./configure --prefix=/opt/nginx 
RUN make && make install
WORKDIR /opt/nginx
RUN rm -rf /opt/nginx-1.14.0
ENV NGINX_HOME=/opt/nginx
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/nginx/sbin
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
注意:
Nginx的docker仓库原文说明如下:
If you add a custom CMD in the Dockerfile, be sure to include -g daemon off; in the CMD in order fornginx to stay in the foreground, so that Docker can track the process properly (otherwise your container will stop immediately after starting)!
Running nginx in debug mode
Images since version 1.9.8 come with nginx-debug binary that produces verbose output when using higher log levels. It can be used with simple CMD substitution: 
$ docker run --name my-nginx -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx nginx -g 'daemon off;'
Similar configuration in docker-compose.yml may look like this:

web:
  image: nginx
  volumes:
    - ./nginx.conf:/etc/nginx/nginx.conf:ro
  command: [nginx, '-g', 'daemon off;']

2.Dockerfile文件构建redis

FROM centos:7.2.1511
MAINTAINER zhaokun redis4 jichujingxiang
ENV TZ=Asia/Shanghai
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
RUN yum -y install gcc make
ADD redis-4.0.9.tar.gz /opt/
RUN cd /opt/ && mv redis-4.0.9  redis  && cd /opt/redis && make && make install
RUN mkdir -p /opt/redis/logs && mkdir -p /opt/redis/data && mkdir -p /opt/redis/conf && cp /opt/redis/redis.conf /opt/redis/conf/ && cp /opt/redis/src/redis-trib.rb /usr/local/bin/
EXPOSE 6379
CMD ["redis-server","/opt/redis/conf/redis.conf"]
基于哨兵模式的redis镜像
FROM centos:7.2.1511
MAINTAINER  redis4 jichujingxiang
ENV TZ=Asia/Shanghai
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
RUN yum -y install gcc make
ADD redis-4.0.9.tar.gz /opt/
ADD run.sh /
RUN cd /opt/ && mv redis-4.0.9  redis  && cd /opt/redis && make && make install
RUN mkdir -p /opt/redis/logs && mkdir -p /opt/redis/data && mkdir -p /opt/redis/conf && cp /opt/redis/redis.conf /opt/redis/conf/ && cp /opt/redis/src/redis-trib.rb /usr/local/bin/ && cp /opt/redis/sentinel.conf /opt/redis/conf/ && chmod 777 /run.sh
EXPOSE 6379
CMD ["./run.sh"]
#cat /run.sh
#!/usr/bin/bash
#2018/10/24
#行癫
cd /opt/redis/src/
./redis-server /opt/redis/conf/redis.conf &                    #这一个要放在后台运行,不然下面的启动不了
./redis-sentinel /opt/redis/conf/sentinel.conf

3.Dockerfile文件构建jenkins

FROM local/c7-systemd
ADD jdk-9.0.1_linux-x64_bin.tar.gz /usr/local/
ADD apache-tomcat-9.0.14.tar.gz /usr/local/
WORKDIR /usr/local/
ENV JAVA_HOME=/usr/local/java
ENV PATH=$JAVA_HOME/bin:$PATH
ENV CATALINA_HOME=/usr/local/tomcat
ENV export JAVA_HOME CATALINA PATH
RUN mv jdk-9.0.1 java && mv apache-tomcat-9.0.14 tomcat
COPY jenkins.war /usr/local/tomcat/webapps/
EXPOSE 8080
CMD ["/usr/local/tomcat/bin/catalina.sh run"]

猜你喜欢

转载自blog.csdn.net/kakaops_qing/article/details/109136914