构建镜像docker bulid

简介

镜像构建的完整命令为docker build [OPTIONS] PATH | URL | -。在这条语句中,并没有指定Dockerfile路径,这是因为大家都习惯使用默认的文件名,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

PATH指的是上下文目录,那么什么是上下文呢?首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker daemon(也就是服务端守护进程)和docker client客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。所以才引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

 

所以在一般情况下,我们会在构建镜像的时候,会先创建一个空目录,然后把所需要的文件都放置在这个目录下,包括Dockerfile文件,需要构建时,使用docker bulid -t name:verison .来运行。而 . 表示PATH,即上下文,docker client会把这个目录里面的文件都打包,传给docker daemon。正是由于这个特性,所有Dockerfile的指令中,COPY、ADD这两个都只能使用相对绝对,即COPY ./a.txt /test/

实例

使用Dockerfile来构建一个tank大战的image。先下载源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@master ~]# git clone https://codehub.devcloud.huaweicloud.com/Demo04260/tank.git
Cloning into 'tank'...
remote: Enumerating objects: 90, done.
remote: Counting objects: 100% (90/90), done.
remote: Compressing objects: 100% (86/86), done.
remote: Total 90 (delta 9), reused 0 (delta 0)
Unpacking objects: 100% (90/90), done.
[root@master ~]# cd tank/
[root@master tank]# cat Dockerfile
FROM nginx
LABEL maintainer "fangdm<8@8994.cn>"

EXPOSE 80
COPY . /usr/share/nginx/html

开始创建,运行命令之后,从第一个输出内容可以看到,docker有包内容全部打包给了Docker daemon。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@master tank]# docker build -t tank .
Sending build context to Docker daemon  2.315MB
Step 1/4 : FROM nginx
 ---> 62c261073ecf
Step 2/4 : LABEL maintainer "fangdm<8@8994.cn>"
 ---> Running in 33da0adbde73
Removing intermediate container 33da0adbde73
 ---> ca6c7c4571b7
Step 3/4 : EXPOSE 80
 ---> Running in 74879a5d980b
Removing intermediate container 74879a5d980b
 ---> d0649d7b613d
Step 4/4 : COPY . /usr/share/nginx/html
 ---> 46a940d07f6d
Successfully built 46a940d07f6d
Successfully tagged tank:latest
[root@master tank]# docker images tank
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
tank                latest              46a940d07f6d        3 minutes ago       111MB

这样就创建成功了。运行:

1
2
3
4
[root@master tank]# docker run --name tank -d -P tank
11c8314f6f5db62fe0783b52c6fccb45b36ed0539c192076be3f56bf7605fd0b
[root@master tank]# docker port tank
80/tcp -> 0.0.0.0:32769

访问docker服务器的32769端口即可开始坦克大战了。

由于在构建的过程中docker会采用缓存的机制,如果需要重新构建,不想使用cache需要添加—no-cache。

Dockerfile指令

Dockerfile是一个包含用户能够构建镜像的所有命令的文本文档,它有自己的语法以及命令,docker能够从dockerfile中读取指令自动的构建镜像。

FROM

FROM 就是指定 基础镜像,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令。有以下格式:

1
2
3
4
FROM scratch
FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]

scratch 这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。需要自己来一层一层地创建。直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

如果没有指定 tag ,latest 将会被指定为要使用的基础镜像版本。

允许多个from,即就是多阶段构建。后续介绍

MAINTAINER

格式为MAINTAINER user_name user_email,指定维护者信息。可以使用LABEL代替。

LABEL

给镜像添加元数据,可以用LABEL命令替换MAINTAINER命令。指定一些作者、邮箱等信息。设置之后,使用docker inspect来查看。

具体格式:LABEL <key>=<value> <key>=<value> <key>=<value> ...

1
2
[root@master tank]# docker inspect -f '{{.Config.Labels}}' tank
map[maintainer:fangdm<8@8994.cn>]

RUN

格式为:RUN ["executable", "param1", "param2"] 或者 RUN <command>

RUN会在当前镜像的最上面创建一个新层,并且能执行任何的命令,然后对执行的结果进行提交。提交后的结果镜像在dockerfile的后续步骤中可以使用。

如果有多个 RUN 指令,最佳建议是使用 && 合并为一个,并且格式要统一。

1
2
3
4
5
6
FROM php:7.1-fpm

RUN apt-get update && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng-dev \
    && docker-php-ext-install -j$(nproc) iconv mcrypt mysqli pdo_mysql \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    && docker-php-ext-install -j$(nproc) gd

WORKDIR

格式为: WORKDIR /path/to/workdir

WORKDIR 用来指定工作目录的。Docker 默认的工作目录是/,只有 RUN 能执行 cd 命令切换目录,而且还只作用在当下下的 RUN,也就是说每一个 RUN 都是独立进行的。如果想让其他指令在指定的目录下执行,就得靠 WORKDIR。WORKDIR 动作的目录改变是持久的,不用每个指令前都使用一次 WORKDIR。如果该目录不存在,则会创建。

ENV

设置环境变量,设置的变量可供后面指令使用。可以使用docker inspect查的。使用docker exec进入容器之后,使用env命令也是可以输出的。

设置的方法是: ENV <key1>=<value1> <key2>=<value2>... 或者 ENV <key> <value>,建议还加上等号,比较直观。

如果在容器运行时想修改环境变量的值 ,可以使用docker run --env <key>=<value>来修改环境变量。

1
2
3
4
5
6
7
[root@master tank]# docker exec tank env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=11c8314f6f5d
NGINX_VERSION=1.17.0
NJS_VERSION=0.3.2
PKG_RELEASE=1~stretch
HOME=/root

ARG

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。即ARG 定义的变量只在建立 image 时有效,建立完成后变量就失效消失

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

使用时,类似shell的变量一样,前面加$即可。

COPY

格式:COPY [--chown=<user>:<group>] <源路径>... <目标路径> 或者 COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

COPY 将文件从路径 复制添加到容器内部路径 。 必须是想对于源文件夹的一个文件或目录,也可以是一个远程的url, 是目标容器中的绝对路径。所有的新文件和文件夹都会创建UID 和 GID 。事实上如果 是一个远程文件URL,那么目标文件的权限将会是600。用法是:COPY [--chown=<user>:<group>] <源路径>... <目标路径>

标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

ADD

跟COPY的作用一样,都是复制文件到容器。但比COPY更高级,会自动下载或者自动解压,如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

VOLUME

定义数据卷,格式为:VOLUME ["<路径1>", "<路径2>"...] 或者 VOLUME <路径>

定义完成之后,docker在创建容器时,会自动创建volume数据卷,这时即使容器删除了,其数据也不是丢失。

EXPOSE

指定端口。格式为 EXPOSE <端口1> [<端口2>...]

指定容器运行时对外暴露的端口,但是该指定实际上不会发布该端口,它的功能是镜像构建者和容器运行者之间的记录文件。run命令有-p和-P两个参数,如果是-P(大写)就是会随机端口映射,容器内会随机映射到EXPOSE指定的端口。

CMD

格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD [“可执行文件”, “参数1”, “参数2”…]

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。注意,如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。即如果CMD echo $HOME,在实际执行中,会将其变更为:CMD [ "sh", "-c", "echo $HOME" ]

指定容器启动时默认运行的命令,在一个Dockerfile文件中,如果有多个CMD命令,只有一个最后一个会生效。RUN指令是在构建镜像时候执行的,而CMD指令是在每次容器运行的时候执行的。而如果docker run命令接了command,会覆盖CMD的命令。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。如果写为 CMD service nginx start,会解析为 CMD [ "sh", "-c", "service nginx start"],这样主进程是sh了。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

ENTRYPOINT和CMD同时存在时, docker把CMD的命令拼接到ENTRYPOINT命令之后,即<ENTRYPOINT> "<CMD>", 拼接后的命令才是最终执行的命令。

在一个Dockerfile文件中,如果有多个ENTRYPOINT命令,也只有一个最后一个会生效。而不同点是通过docker run command命令会覆盖CMD的命令,但如果有ENTRYPOINT,则执行的命令不会覆盖ENTRYPOINT,docker run命令中指定的任何参数都会被当做参数传递给ENTRYPOINT。

例如,如果Dockerfile只有 CMD [ "curl", "-s", "https://ip.cn" ],那么运行 docker run myip -i 会出错,如果是 ENTRYPOINT [ "curl", "-s", "https://ip.cn" ],运行 docker run myip -i 就会输出Http头部信息,这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

有关这两者的区别,可以参考:https://yeasy.gitbooks.io/docker_practice/content/image/dockerfile/entrypoint.html 以及 Dockerfile: ENTRYPOINT和CMD的区别

HEALTHCHECK

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。支持下列选项:

  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

如下的Dockerfile

1
2
3
4
FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -fs http://localhost/ || exit 1

运行之后就会检测其健康,如下是正常的,healthy:

1
2
3
root@fdm:~/test# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS         NAMES
6cd513fdb549        myweb:v1            "nginx -g 'daemon ..."   19 minutes ago      Up 19 minutes (healthy)   80/tcp        sleepy_booth

.dockerignore

这个不是指令,只是一个文件,用来忽略上下文目录中包含的一些image用不到的文件,它们不会传送到docker daemon。

其他的一些指令,可以参考:

https://docs.docker.com/v17.09/engine/reference/builder/

Dockerfile多阶段构建

Dockerfile最佳实践

实例

ubuntu ssh

官方镜像都没有ssh的功能,给他加上吧。先创建一个空的目录,然后创建以下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@fdm:~/sshd_ubuntu# cat Dockerfile
FROM ubuntu:18.04
MAINTAINER fangdm <5608381@qq.com>
ADD run.sh /
RUN apt-get update && apt-get install -y openssh-server && \
    mkdir -p /var/run/sshd && \
    echo 'root:tjvIa1fp' | chpasswd && \
    echo "PermitRootLogin yes" >>/etc/ssh/sshd_config && \
    sed -i 's/#PasswordAuthentication/PasswordAuthentication/' /etc/ssh/sshd_config && \
    echo 'tjvIa1fp' >/root/passwd.txt && \
    chmod 755 /run.sh && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
EXPOSE 22
CMD ["/run.sh"]

root@fdm:~/sshd_ubuntu# cat run.sh
#!/bin/bash
echo "root password: tjvIa1fp"
/usr/sbin/sshd -D

然后直接docker build -t ubuntu-sshd:v1 .即可。完成之后,可以看到,其大小比原来的ubuntu大了三分之二。仅仅只是安装了openssh-server。

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
[root@master ubuntu_ssh]# docker images |grep ubuntu
ubuntu-sshd         v1                  d7c322e5bcf5        10 minutes ago      179MB
ubuntu              18.04               2ca708c1c9cc        3 days ago          64.2MB
ubuntu              latest              a2a15febcdf3        5 weeks ago         64.2MB
[root@master ubuntu_ssh]# docker run -itd -P --name ssh ubuntu-sshd:v1
fba8152f891cad00ef2e5aeb4cdeede5d17e59b0182e5ea049bb2fae6f8f2732
[root@master ubuntu_ssh]# docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                   NAMES
fba8152f891c        ubuntu-sshd:v1      "/run.sh"           10 seconds ago      Up 6 seconds        0.0.0.0:32771->22/tcp   ssh
#查密码
[root@master ubuntu_ssh]# docker logs ssh
root password: tjvIa1fp
#查IP
[root@master ubuntu_ssh]# docker inspect -f '{{ .NetworkSettings.IPAddress }}' ssh
172.17.0.4
#登陆
[root@master ubuntu_ssh]# ssh 172.17.0.4
The authenticity of host '172.17.0.4 (172.17.0.4)' can't be established.
ECDSA key fingerprint is SHA256:bS6WcqPKrrbhgAJ9LgRdabc5kKbtRMH1Hgykz71tatY.
ECDSA key fingerprint is MD5:a5:3d:88:2f:2b:c7:a0:78:0e:c1:95:51:1d:72:4d:2b.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.17.0.4' (ECDSA) to the list of known hosts.
root@172.17.0.4's password:
root@fba8152f891c:~# cat /etc/issue
Ubuntu 18.04.3 LTS \n \l

root@fba8152f891c:~#

在此基本上,可以再次制作nginx,不过是简单版:

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
root@fdm:~# cat nginx/Dockerfile
FROM ubuntu-sshd:v1
LABEL maintainer="fangdm <8@8994.cn>"

ADD run.sh /run.sh
WORKDIR /etc/nginx

RUN apt-get install -y nginx && \
    rm -rf /var/lib/apt/lists/* && \
    echo "daemon off;" >>/etc/nginx/nginx.conf && \
    chown -R www-data:www-data /var/lib/nginx && \
    rm -f /usr/share/nginx/html/index.html && \
    echo "Asia/Shanghai" > /etc/timezone && \
    #dpkg-reconfigure -f noninteractive tzdata && \
    chmod 755 /run.sh

COPY index.html /var/www/html
VOLUME ["/etc/nginx/sites-enabled","/etc/nginx/certs","/etc/nginx/conf.d","/var/log/nginx"]

CMD ["/run.sh"]
EXPOSE 80
EXPOSE 443

root@fdm:~# cat nginx/run.sh
#!/bin/bash

/usr/sbin/sshd &
/usr/sbin/nginx

可以使用supervisord来控制服务,https://my.oschina.net/renguijiayi/blog/365087

https://blog.csdn.net/Ricky110/article/details/78360229

nginx-alpine版

alpine介绍

首先先介绍下busybox以及alpine。

两者的大小为:

1
2
3
[root@master ~]# docker images |egrep 'busybox|alpine'
busybox             latest              19485c79a9bb        2 weeks ago         1.22MB
alpine              latest              961769676411        4 weeks ago         5.58MB

由于alpine小巧、安全、简单以及功能完备的特点,被广泛应用于众多Docker容器中。不同于centos使用yum、ubuntu使用apt-get去安装软件,alpine使用了apk这个指令去安装相应的指令。以下是修改apk使用阿里的源。

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
# alpine 只有/bin/sh,没有/bin/bash
[root@master ~]# docker exec -it alpine /bin/bash
OCI runtime exec failed: exec failed: container_linux.go:345: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown
# 修改源
[root@master ~]# docker exec -it alpine /bin/sh
/ # cat /etc/apk/repositories
http://dl-cdn.alpinelinux.org/alpine/v3.10/main
http://dl-cdn.alpinelinux.org/alpine/v3.10/community
/ # echo -e "http://mirrors.aliyun.com/alpine/latest-stable/main\nhttp://mirrors.aliyun.com/alpine/latest-stable/community" >/etc/apk/repositories
/ # cat /etc/apk/repositories
http://mirrors.aliyun.com/alpine/latest-stable/main
http://mirrors.aliyun.com/alpine/latest-stable/community
/ # vim /etc/apk/repositories
/bin/sh: vim: not found

# 安装 vim 命令
/ # apk add vim
fetch http://mirrors.aliyun.com/alpine/latest-stable/main/x86_64/APKINDEX.tar.gz
fetch http://mirrors.aliyun.com/alpine/latest-stable/community/x86_64/APKINDEX.tar.gz
(1/5) Installing lua5.3-libs (5.3.5-r2)
(2/5) Installing ncurses-terminfo-base (6.1_p20190518-r0)
(3/5) Installing ncurses-terminfo (6.1_p20190518-r0)
(4/5) Installing ncurses-libs (6.1_p20190518-r0)
(5/5) Installing vim (8.1.1365-r0)
Executing busybox-1.30.1-r2.trigger
OK: 40 MiB in 19 packages
/ # exit

使用apk --update add --no-cache <package>来安装命令。安装完成vim,就增加了40MB了。

根据官方的alpine Dockerfile来看,其实非常简单:

1
2
3
FROM scratch
ADD alpine-minirootfs-3.10.2-x86_64.tar.gz /
CMD ["/bin/sh"]

构建nginx

首先先看一下官方的写法 docker-nginx Dockerfile ,可以看出,其思路是直接nginx提供的源,然后直接使用apk去安装。这是比较方便,但是无法自定义参数,所以还是需要使用编译大法。

先下载必要的软件包:

1
2
3
git clone https://github.com/alibaba/nginx-http-concat.git
git clone https://github.com/FRiCKLE/ngx_cache_purge.git
wget https://nginx.org/download/nginx-1.16.1.tar.gz

编写Dockerfile,nginx默认安装在/usr/local/nginx目录下,如下:

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
[root@master nginx]# cat Dockerfile
FROM alpine:3.10 as builder

LABEL maintainer="fangdm<8@8994.cn>"

ENV NGINX_VERSION 1.16.1
ENV CONFIG "--prefix=/usr/local/nginx --user=www --group=www --with-http_auth_request_module --with-http_realip_module --with-http_v2_module --with-debug --with-http_random_index_module --with-http_sub_module --with-http_addition_module --with-http_secure_link_module --with-http_ssl_module --with-stream_ssl_module --with-stream_realip_module --with-stream_ssl_preread_module --with-stream --add-module=../ngx_cache_purge --add-module=../nginx-http-concat --with-http_slice_module --with-threads --with-http_gzip_static_module --with-http_gunzip_module --with-http_stub_status_module"

ADD . /nginx/

RUN set -x \
    && addgroup -g 101 -S www \
    && adduser -S -D -H -u 101 -h /var/cache/www -s /sbin/nologin -G www -g www www \
    && echo -e "http://mirrors.aliyun.com/alpine/latest-stable/main\nhttp://mirrors.aliyun.com/alpine/latest-stable/community" >/etc/apk/repositories \
    && apk add --no-cache --virtual .bulidDeps gcc g++ make libxslt-dev libxml2-dev gd-dev wget linux-headers pcre pcre-dev zlib zlib-dev openssl openssl-dev geoip geoip-dev \
    && cd /nginx \
    && tar zxf nginx-1.16.1.tar.gz \
    && cd nginx-1.16.1 \
    && ./configure $CONFIG \
    && make \
    && make install \
    && cd / \
    && tar zcvf nginx.tar.gz /usr/local/nginx


FROM alpine:3.10

ENV NGINX_VERSION="1.16.1" LANG="en_US.UTF-8" GLIBC_PKG_VERSION="2.30-r0"
COPY --from=builder /nginx.tar.gz /

RUN set -x \
    && cd / \
    && tar zxvf /nginx.tar.gz \
    && addgroup -g 101 -S www \
    && adduser -S -D -H -u 101 -h /var/cache/www -s /sbin/nologin -G www -g www www \
    && echo -e "http://mirrors.aliyun.com/alpine/latest-stable/main\nhttp://mirrors.aliyun.com/alpine/latest-stable/community" >/etc/apk/repositories \
    && apk add --no-cache --virtual .nginx-rundeps gettext gd libxslt geoip pcre\
    && mv /usr/bin/envsubst /tmp/ \
    && runDeps="$( \
        scanelf --needed --nobanner /tmp/envsubst \
            | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
            | sort -u \
            | xargs -r apk info --installed \
            | sort -u \
    )" \
    && apk add --no-cache $runDeps \
    && mv /tmp/envsubst /usr/local/bin/ \
    && apk add --no-cache tzdata \
    && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo "Asia/Shanghai" > /etc/timezone \
    && ln -sf /dev/stdout /usr/local/nginx/logs/access.log \
    && ln -sf /dev/stderr /usr/local/nginx/logs/error.log \
    \
    #安装中文字体库
    && cd /usr/local/src \
    && curl -Lo /etc/apk/keys/sgerrand.rsa.pub "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/sgerrand.rsa.pub" \
    && curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" \
    && curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" \
    && curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" \
    && apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk \
    && /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true \
    && echo "export LANG=$LANG" > /etc/profile.d/locale.sh \
    && rm -fr /usr/local/src /var/cache/apk/*

EXPOSE 80

CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

为了减少镜像的体积,使用了多阶段构建的方法。同时多阶段构建时,只能复制文件,不能复制目录,所以需要对/usr/local/nginx进行打包,使用“docker build -t nginx_alpine .构建完成之后,可以看到nginx_alpine大小为31.1M,而编译过程中,产生了284M。

1
2
3
4
[root@master nginx]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx_alpine        latest              7c74ceae2589        24 minutes ago      31.1MB
<none>              <none>              bc55a29db5af        25 minutes ago      284MB

另外,alpine是不支持中文,要集成中文环境,可以参考:https://www.clxz.top/2019/05/09/160241/

创建centos镜像

tar打包构建

第一种方法,通过tar打包构建基础镜像:

1
2
3
yum clean all
rm -rf /var/cache/yum
tar --numeric-owner --exclude=/proc --exclude=/sys --exclude=/mnt --exclude=/var/cache --exclude=/usr/share/{foomatic,backgrounds,perl5,fonts,cups,qt4,groff,kde4,icons,pixmaps,emacs,gnome-background-properties,sounds,gnome,games,desktop-directories} --exclude=/var/log -zcvf /mnt/CentOS-7.6-BaseImage.tar.gz /

安装docker:

1
2
3
4
5
6
7
# 安装EPEL源和REMI源
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -Uvh https://rpms.remirepo.net/enterprise/remi-release-7.rpm
# 安装Docker软件包
yum install -y docker-io
# 启动Docker服务
systemctl start docker.service

将打包好的文件导入到docker中,然后验证:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# cat /mnt/CentOS-7.-BaseImage.tar.gz |docker import - centos-tar:7.6
sha256:b629e1dd6b5365d96cc8aa0d4918215a1609fd2d8d41688a3507c78f428451d9
[root@localhost ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos-tar          7.6                 b629e1dd6b53        2 minutes ago       751 MB
[root@localhost ~]# docker run --rm -it centos-tar:7.6 bash
[root@949f13b77435 /]# w
 09:11:52 up  1:12,  2 users,  load average: 0.12, 0.62, 0.42
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT

mkimage-yum.sh脚本构建

第二种方法,运行mkimage-yum.sh脚本,创建CentOS的基础镜像,首先下载
wget https://raw.githubusercontent.com/moby/moby/master/contrib/mkimage-yum.sh,然后运行脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@localhost ~]# sh mkimage-yum.sh centos-mkimage
+ mkdir -m 755 /tmp/mkimage-yum.sh.sB2M3f/dev
+ mknod -m 600 /tmp/mkimage-yum.sh.sB2M3f/dev/console c 5 1
+ mknod -m 600 /tmp/mkimage-yum.sh.sB2M3f/dev/initctl p
+ mknod -m 666 /tmp/mkimage-yum.sh.sB2M3f/dev/full c 1 7
...
+ version=
+ for file in '"$target"/etc/{redhat,system}-release'
+ '[' -r /tmp/mkimage-yum.sh.7yJbJ7/etc/redhat-release ']'
++ sed 's/^[^0-9\]*\([0-9.]\+\).*$/\1/' /tmp/mkimage-yum.sh.7yJbJ7/etc/redhat-release
+ version=7.6.1810
+ break
+ '[' -z 7.6.1810 ']'
+ tar --numeric-owner -c -C /tmp/mkimage-yum.sh.7yJbJ7 .
+ docker import - centos-mkimage:7.6.1810
sha256:e3156476a6818c2f12453da3b6f182a38cb70a055b75e153b9dcee36c1adbd68
+ docker run -i -t --rm centos-mkimage:7.6.1810 /bin/bash -c 'echo success'
success
+ rm -rf /tmp/mkimage-yum.sh.7yJbJ7

[root@localhost ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos-mkimage      7.6.1810            e3156476a681        49 seconds ago      281 MB
centos-tar          7.6                 b629e1dd6b53        13 minutes ago      751 MB

由上述两图可知,通过tar打包创建的基础镜像的体积大约是通过mkimage-yum.sh脚本创建的基础镜像的3倍左右。这是由于前者是基于最小化安装的CentOS系统而创建的,而后者是从CentOS官方源安装必要的软件包而创建的,前者比后者多安装了很多软件包,包括邮件工具、设备驱动程序,等等

http://ghoulich.xninja.org/2017/10/13/how-to-create-docker-base-image-of-centos/

mysql 5.7

基础知识

想对mysql进行镜像化,必须掌握mysql的一些操作命令。

my_print_defaults为查询my.cnf配置文件的命令,第一个参数为section

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@ee1995b6bc44 /]# my_print_defaults mysqld 
--port=3306
--datadir=/var/lib/mysql
--max_connections=200
--character-set-server=utf8
--bind-address=0.0.0.0
--socket=/var/sock/mysqld/mysqld.sock
--pid-file=/var/run/mysqld/mysqld.pid
--log-error=/var/log/mysql/error.log
--slow_query_log_file=/var/log/mysql/slow.log
--general_log_file=/var/log/mysql/query.log
[root@ee1995b6bc44 /]# 
[root@ee1995b6bc44 /]# 
[root@ee1995b6bc44 /]# 
[root@ee1995b6bc44 /]# my_print_defaults mysqld |grep "^--datadir=" | cut -d= -f2- | tail -n 1
/var/lib/mysql

另外,也可以使用mysqld —verbose —help方法来获取

1
2
3
4
[root@xmxyk bin]#./mysqld --verbose --help  2>/dev/null | awk -v key="datadir" '$1 == key { print $2; exit }'
/usr/local/mysql/var/
[root@xmxyk bin]#./mysqld --verbose --help  2>/dev/null | awk -v key="socket" '$1 == key { print $2; exit }'
/tmp/mysql.sock

以空密码的方式初始化数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@ee1995b6bc44 /]# mysqld --initialize-insecure --datadir="/var/lib/mysql/" --user=${MYSQL_USER}
[root@ee1995b6bc44 /]# ll /var/lib/mysql
total 110620
-rw-r----- 1 mysql mysql       56 Mar 16 12:35 auto.cnf
-rw-r----- 1 mysql mysql      419 Mar 16 12:35 ib_buffer_pool
-rw-r----- 1 mysql mysql 50331648 Mar 16 12:35 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 Mar 16 12:35 ib_logfile1
-rw-r----- 1 mysql mysql 12582912 Mar 16 12:35 ibdata1
drwxr-x--- 2 mysql mysql     4096 Mar 16 12:35 mysql
drwxr-x--- 2 mysql mysql     4096 Mar 16 12:35 performance_schema
drwxr-x--- 2 mysql mysql    12288 Mar 16 12:35 sys
[root@ee1995b6bc44 /]# cat /var/log/mysql/error.log 
2019-03-16T12:35:48.881747Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2019-03-16T12:35:49.345885Z 0 [Warning] InnoDB: New log files created, LSN=45790
2019-03-16T12:35:49.439030Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2019-03-16T12:35:49.521977Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 077e6497-47e8-11e9-9508-0242ac110002.
2019-03-16T12:35:49.524840Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2019-03-16T12:35:49.526731Z 1 [Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

Dockerfile

从Dockerfile文件可以看到是使用了yum进行安装的mysql 5.7,安装好了之后,生成了/etc/my.cnf文件,而启动mysql则是交给了docker-entrypoint.sh

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
[root@host mysql]# cat Dockerfile 
FROM centos:7
MAINTAINER fangdm <fang2000@vip.qq.com>

LABEL image="mysql-5.7" vendor=fangdm build-date="2019-03-15"
ENV REPO_URL="https://repo.mysql.com//mysql57-community-release-el7-11.noarch.rpm" \
    MYSQL_ROOT_PASSWORD="**Random**" \
    MYSQL_VERSION=5.7 \
    MYSQL_USER=mysql \
    MYSQL_GROUP=mysql \
    MYSQL_UID=27 \
    MYSQL_GID=27 

COPY docker-entrypoint.sh /
RUN groupadd -g ${MYSQL_GID} -r ${MYSQL_GROUP} && chmod 755 /docker-entrypoint.sh && \
    adduser ${MYSQL_USER} -u ${MYSQL_UID} -s /sbin/nologin -M -g ${MYSQL_GROUP}

RUN yum install -y epel-release && \
    rpm -ivh ${REPO_URL} && \
    yum-config-manager --disable mysql55-community && \
    yum-config-manager --disable mysql56-community && \
    yum-config-manager --disable mysql80-community && \
    yum-config-manager --enable mysql57-community && \
    yum install -y mysql-community-server  && \
    yum -y autoremove && \
    yum clean all
	
ENV MYSQL_BASE_INCL="/etc/my.cnf.d" \
	MYSQL_CUST_INCL1="/etc/mysql/conf.d" \
	MYSQL_MY_CNF="/etc/my.cnf" \
	MYSQL_CUST_INCL2="/etc/mysql/docker-default.d" \
	MYSQL_DEF_DATA="/var/lib/mysql" \
	MYSQL_DEF_PID="/var/run/mysqld" \
	MYSQL_DEF_SOCK="/var/sock/mysqld" \
	MYSQL_DEF_LOG="/var/log/mysql"

ENV	MYSQL_LOG_SLOW="${MYSQL_DEF_LOG}/slow.log" \
	MYSQL_LOG_ERROR="${MYSQL_DEF_LOG}/error.log" \
	MYSQL_LOG_QUERY="${MYSQL_DEF_LOG}/query.log"
	
RUN rm -rf $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG && \
	mkdir -p $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG && \
	chmod 0755 $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG && \
	chown ${MYSQL_USER}:${MYSQL_GROUP} $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG
	
RUN echo "[client]"					>$MYSQL_MY_CNF && \
	echo "socket = ${MYSQL_DEF_SOCK}/mysqld.sock" 	>>$MYSQL_MY_CNF && \
	echo -e "default-character-set=utf8\n" 		>>$MYSQL_MY_CNF && \
	echo "[mysql]" 					>>$MYSQL_MY_CNF && \
	echo "socket = ${MYSQL_DEF_SOCK}/mysqld.sock" 	>>$MYSQL_MY_CNF && \
	echo -e "default-character-set=utf8\n" 		>>$MYSQL_MY_CNF && \
	echo "[mysqld]"					>>$MYSQL_MY_CNF && \
	echo "skip-name-resolve"			>>$MYSQL_MY_CNF && \
	echo "skip-host-cache"				>>$MYSQL_MY_CNF && \
	echo "port = 3306" 				>>$MYSQL_MY_CNF && \
	echo "user = ${MYSQL_USER}"			>>$MYSQL_MY_CNF && \
	echo "datadir=$MYSQL_DEF_DATA" 			>>$MYSQL_MY_CNF && \
	echo "max_connections = 200" 			>>$MYSQL_MY_CNF && \
	echo "character-set-server = utf8" 		>>$MYSQL_MY_CNF && \
	echo "bind-address = 0.0.0.0" 			>>$MYSQL_MY_CNF && \
	echo "socket = ${MYSQL_DEF_SOCK}/mysqld.sock" 	>>$MYSQL_MY_CNF && \
	echo "pid-file = ${MYSQL_DEF_PID}/mysqld.pid" 	>>$MYSQL_MY_CNF && \
	echo "log-error = ${MYSQL_LOG_ERROR}" 		>>$MYSQL_MY_CNF && \
	echo "general_log_file = ${MYSQL_LOG_QUERY}" 	>>$MYSQL_MY_CNF && \
	echo "slow_query_log_file = ${MYSQL_LOG_SLOW}" 	>>$MYSQL_MY_CNF && \
	echo "!includedir ${MYSQL_BASE_INCL}/" 		>>$MYSQL_MY_CNF && \
	echo "!includedir ${MYSQL_CUST_INCL1}/" 	>>$MYSQL_MY_CNF && \
	echo "!includedir ${MYSQL_CUST_INCL2}/" 	>>$MYSQL_MY_CNF
	
EXPOSE 3306
VOLUME ["$MYSQL_DEF_DATA","$MYSQL_DEF_LOG","$MYSQL_CUST_INCL1","$MYSQL_CUST_INCL2"]
ENTRYPOINT ["/docker-entrypoint.sh"]

docker-entrypoint.sh

docker-entrypoint.sh内容如下,主要作用是启动Mysql,且进行初始化。此脚本由国外大神cytopia制作,实现了shell的debug模式。值得认真学习。

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
[root@host mysql]# cat docker-entrypoint.sh 
#!/bin/bash

run() {
	_cmd="${1}"
	_debug="0"

	_red="\033[0;31m"
	_green="\033[0;32m"
	_reset="\033[0m"
	_user="$(whoami)"

	# If 2nd argument is set and enabled, allow debug command
	if [ "${#}" = "2" ]; then
		if [ "${2}" = "1" ]; then
			_debug="1"
		fi
	fi

	if [ "${DEBUG_COMMANDS}" = "1" ] || [ "${_debug}" = "1" ]; then
		printf "${_red}%s \$ ${_green}${_cmd}${_reset}\n" "${_user}"
	fi
	sh -c "LANG=C LC_ALL=C ${_cmd}"
}

log() {
	_lvl="${1}"
	_msg="${2}"

	_clr_ok="\033[0;32m"	#绿
	_clr_info="\033[0;34m"	#蓝
	_clr_warn="\033[0;33m"	#黄
	_clr_err="\033[0;31m"	#红
	_clr_rst="\033[0m"		#结束标记

	if [ "${_lvl}" = "ok" ]; then
		printf "${_clr_ok}[OK]   %s${_clr_rst}\n" "${_msg}"
	elif [ "${_lvl}" = "info" ]; then
		printf "${_clr_info}[INFO] %s${_clr_rst}\n" "${_msg}"
	elif [ "${_lvl}" = "warn" ]; then
		printf "${_clr_warn}[WARN] %s${_clr_rst}\n" "${_msg}" 1>&2	# stdout -> stderr
	elif [ "${_lvl}" = "err" ]; then
		printf "${_clr_err}[ERR]  %s${_clr_rst}\n" "${_msg}" 1>&2	# stdout -> stderr
	else
		printf "${_clr_err}[???]  %s${_clr_rst}\n" "${_msg}" 1>&2	# stdout -> stderr
	fi
}

get_mysql_default_config() {
	_key="${1}"
	mysqld --verbose --help  2>/dev/null  | awk -v key="${_key}" '$1 == key { print $2; exit }'
}

if [ "$MYSQL_ROOT_PASSWORD" = "**Random**" ]; then
    export MYSQL_ROOT_PASSWORD=`cat /dev/urandom | tr -dc A-Z-a-z-0-9 | head -c${1:-16}`
fi

if [ "$MYSQL_SOCKET_DIR"X = ""X ]; then
    export MYSQL_SOCKET_DIR=`get_mysql_default_config socket`
fi

if [ "$DB_DATA_DIR"X = ""X ]; then
    export DB_DATA_DIR="$( get_mysql_default_config "datadir" )"
fi

if [ -d "${DB_DATA_DIR}/mysql" ] && [ "$( ls -A "${DB_DATA_DIR}/mysql" )" ]; then
	log "info" "Found existing data directory. MySQL already setup."
else
	log "info" "No existing MySQL data directory found. Setting up MySQL for the first time."
	# Create datadir if not exist yet
	if [ ! -d "${DB_DATA_DIR}" ]; then
		log "info" "Creating empty data directory in: ${DB_DATA_DIR}."
		run "mkdir -p ${DB_DATA_DIR}"
		run "chown -R ${MY_USER}:${MY_GROUP} ${DB_DATA_DIR}"
		run "chmod 0777 ${MY_USER}:${MY_GROUP} ${DB_DATA_DIR}"
	fi
	#initialize no password
	run "mysqld --initialize-insecure --datadir=${DB_DATA_DIR} --user=${MYSQL_USER}"
	run "mysqld --skip-networking &"
	for i in `seq 1 60`;do
		if echo 'SELECT 1' |  mysql --protocol=socket -uroot  > /dev/null 2>&1; then
			break
		fi
		log "info" "Initializing ..."
		sleep 1s
		i=$(( i + 1 ))
	done
	pid="$(pgrep mysqld | head -1)"
	if [ "${pid}" = "" ]; then
		log "err" "Could not find running MySQL PID."
		log "err" "MySQL init process failed."
		exit 1
	fi
	
	# Bootstrap MySQL
	log "info" "Setting up root user permissions."
	echo "DELETE FROM mysql.user ;" | mysql --protocol=socket -uroot
	echo "CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;" | mysql --protocol=socket -uroot
	echo "GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;" | mysql --protocol=socket -uroot
	echo "DROP DATABASE IF EXISTS test ;" | mysql --protocol=socket -uroot
	echo "FLUSH PRIVILEGES ;" | mysql --protocol=socket -uroot
	
	log "info" "Shutting down MySQL."
	run "kill -s TERM ${pid}"
	sleep 5
	if pgrep mysqld >/dev/null 2>&1; then
		log "err" "Unable to shutdown MySQL server."
		log "err" "MySQL init process failed."
		exit 1
	fi
	log "info" "MySQL successfully installed."
fi

log "info" "Starting $(mysqld --version)"
log "info" "MYSQL Config: /etc/my.cnf"
log "info" "MYSQL Password: $MYSQL_ROOT_PASSWORD"
log "info" "MYSQL Data_db_dir: $DB_DATA_DIR"
log "info" "Auther: https://github.com/fangdm/docker-mysql"
exec mysqld

运行之后,使用docker logs mysql就可以看出mysql输出的密码。如果想自定义密码,可以使用-e MYSQL_ROOT_PASSWORD=password来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@host mysql]# docker logs mysql 
[INFO] No existing MySQL data directory found. Setting up MySQL for the first time.
[INFO] Initializing ...
[INFO] Setting up root user permissions.
[INFO] Shutting down MySQL.
[INFO] MySQL successfully installed.
[INFO] Starting mysqld  Ver 5.7.25 for Linux on x86_64 (MySQL Community Server (GPL))
[INFO] MYSQL Config: /etc/my.cnf
[INFO] MYSQL Password: fVKtzGsS06fQ3o9c
[INFO] MYSQL Data_db_dir: /var/lib/mysql/
[INFO] Auther: https://github.com/fangdm/docker-mysql
[root@host mysql]# docker exec -it mysql mysql -uroot -pfVKtzGsS06fQ3o9c -e "show databases"
mysql: [Warning] Using a password on the command line interface can be insecure.
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
夜河资源网提供的所有内容仅供学习与交流。通过使用本站内容随之而来的风险以及法律责任与本站无关,所承担的法律责任由使用者承担。
一、如果您发现本站侵害了相关版权,请附上本站侵权链接和您的版权证明一并发送至邮箱:yehes#qq.com(#替换为@)我们将会在五天内处理并断开该文章下载地址。
二、本站所有资源来自互联网整理收集,全部内容采用撰写共用版权协议,要求署名、非商业用途和相同方式共享,如转载请也遵循撰写共用协议。
三、根据署名-非商业性使用-相同方式共享 (by-nc-sa) 许可协议规定,只要他人在以原作品为基础创作的新作品上适用同一类型的许可协议,并且在新作品发布的显著位置,注明原作者的姓名、来源及其采用的知识共享协议,与该作品在本网站的原发地址建立链接,他人就可基于非商业目的对原作品重新编排、修改、节选或者本人的作品为基础进行创作和发布。
四、基于原作品创作的所有新作品都要适用同一类型的许可协议,因此适用该项协议, 对任何以他人原作为基础创作的作品自然同样都不得商业性用途。
五、根据二〇〇二年一月一日《计算机软件保护条例》规定:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可不经软件著作权人许可,无需向其支付报酬!
六、鉴此,也望大家按此说明转载和分享资源!本站提供的所有信息、教程、软件版权归原公司所有,仅供日常使用,不得用于任何商业用途,下载试用后请24小时内删除,因下载本站资源造成的损失,全部由使用者本人承担!