Docker


Docker

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 LinuxWindows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。

Docker为什么出现?

一款产品:开发-上线 两套环境,各不相同,维护麻烦!

开发–运维 开发在自己的电脑上运行良好,然后把项目打成jar包或是war包,丢给运维,运维需要配置各种环境,各种集群,压力超大,而且还很有可能失败!

版本更新,导致服务不可用!

Docker可以把项目和它所依赖的环境整合打包,非常方便!

Docker的思想就来自于集装箱,应用之间相互隔离,隔离的思想

Docker通过隔离机制,将服务器利用到极致

docker架构

基础

  1. 安装docker引擎

    • CentOS中安装Docker 引擎

    地址:https://docs.docker.com/engine/install/centos/

    • 设置存储库

    安装yum-utils包(提供yum-config-manager 实用程序)并设置稳定存储库。

     sudo yum install -y yum-utils
     sudo yum-config-manager \
        --add-repo \
        https://download.docker.com/linux/centos/docker-ce.repo
    

    Another app is currently holding the yum lock解决方法

    rm -f /var/run/yum.pid
    
    • 安装 Docker 引擎
    1. 安装最新版本的 Docker Engine 和 containerd

      yum install docker-ce docker-ce-cli containerd.io
      
    2. 启动 Docker

      systemctl start docker
      
    3. 通过运行hello-world 映像验证 Docker Engine 是否已正确安装。

      docker run hello-world
      

      此命令下载测试映像并在容器中运行它。当容器运行时,它会打印一条消息并退出。

  2. 启动docker服务

    systemctl start docker
    
  3. 查找镜像

    docker search mysql
    

    docker search的输出是按评星数量排序的

  4. 下载镜像

    一旦选择了一个镜像,就可以通过对其名称执行docker pull命令来下载它:

    docker pull mysql
    
  5. 启动容器

    接着,可以使用-t和-i标志以交互方式运行它。-t标志指明创建一个TTY设备(一个终端),而-i标志指明该Docker会话是交互式的:

    docker run -e MYSQL_ROOT_PASSWORD=root --name mysql -it -d -p 5001:3306  mysql:5.7
    
    -p 5001:3306  //宿主机使用5001端口,容器使用3306端口
    -d 守护进程,后台运行
    --name mysql:给容器命名为:mysql
    
  6. 在启动的容器中执行命令

    docker exec  -it mysql /bin/bash
    

数据卷

前言:

在生产环境中使用Docker,往往需要对数据进行持久化,或者需要再多个容器之间进行数据共享,而这个必然涉及到容器的数据管理操作

在容器中对数据进行管理的方式主要有两种:

数据卷 Data Volume:容器内部的数据直接映射到本地主机环境中

数据卷容器 Data Volume Container:指定一个容器进行管理管理维护数据卷

什么是数据卷

数据卷 Data Volumes

简单来讲的话,数据卷就是在本地主机中可以在容器之间进行共享和重用的一些文件或者文件夹,通过docker run -v 的形式进行对数据卷挂载到对应的容器目录空间,进行文件读取

它可以提供有很多的有用的特性

  1. 数据卷可以在容器之间共享和重用
  2. 对数据卷的修改会即刻生效
  3. 对数据卷进行升级,不会影响到镜像
  4. 数据卷默认情况下会一直存在,即使是容器被删除

数据卷的操作方式类似于linux中对目录或者文件进行mount的方式,将一个容器或者多个容器中的目录进行以挂载的形式挂载到宿主机

为什么要使用数据卷

在创建容器的时候不使用数据卷的时候一般会遇到一下几个问题:

  1. 当创建一个容器的时候,容器在运行的时候,数据能不能持久化?
  2. 如果能持久化,数据存储在哪里?由于Docker是个例的,数据能否存储在容器外部?
  3. 如果部署很多的容器,每次都需要进入到容器中进行配置吗?能不能在外部进行部署?

Docker数据卷在呈现给Docker容器的形式就是一个目录,而且,该目录支持多个容器间进行共享,修改数值并不会对镜像产生影响。上面也说了,使用Docker数据卷,类似于系统中使用mount进行挂载一个文件系统来修改差不多

Volume的作用恰巧就能尽量的解决这些问题:

Volume的作用

  1. 通过数据卷可以在容器之间实现共享和重用
  2. 对数据卷的修改会直接生效,非常适合作为开发环境
  3. 数据卷在更新的时候,并不会去修改容器镜像中的数值
  4. 数据卷本身会一直存在,并不会因为某个连接的容器挂到或者删除而消失

创建数据卷

创建容器并且挂载到数据卷

docker run -it -p 8808:80 --name nginx_1 -v /opt/test:/opt nginx:1.12 /bin/bash
#创建一个nginx容器,映射端口是8808,名字为nginx_1,将容器中opt目录以挂载的形式到宿主机目录中的opt下的test目录

挂载完成之后,进行测试

容器
    #先到容器中到达共享的目录中,上面我们测试的就是opt目录
     cd /opt/
     #到opt下先查看当前目录下有什么文件
     ls
     #当前还没有进行创建文件或者目录,所以是空的
 
宿主机
    #到宿主机中,同样是到指定好的目录中
     pwd
     /opt/test
    #到达之后先随便创建一个文件
    touch 1.txt
    #创建之后,再去容器中查看一下,就可以看到已经有了一个相同的文件
    
容器
    ls
    1.txt
    #并且,在宿主机找那个对这个文件进行管理的话,会实时同步到容器中

上面,将一个容器添加到数据卷中已经完成了,上面也说了,可以在一个数据卷中可以添加多个容器

docker run -it -p 8809:80 --name nginx_2 -v /opt/test:/opt nginx:1.12 /bin/bash

创建第二个容器之后,不用再进行在数据卷中常见新的文件,因为现在已经存在有文件。直接到容器中的共享目录中查看即可

容器中
    cd /opt/
    ls
    1.txt
#就可以看到上面我们创建的文件

最后再试试,将两个容器都关闭之后,数据卷是否还存在

docker rm -f `docker ps -qa`
#批量删除容器,因为我这就是测试使用的,所以只运行了更改创建的两个,可以直接无脑全清
cd /opt/test/ && ls
1.txt
#查看之后,刚刚创建的文件还是正常存在,当创建新的容器的时候,指定该目录,可以直接将其目录下文件给共享过去

注:容器在共享目录到主机中的时候,数据是双向的也就是,容器中创建文件的时候,主机中也会多一个文件,删除的时候,主机中也会少一个文件。

如果只想要容器中拥有只读权限,可以在-v指定路径的最后,添加上:ro即可,例如:

docker run -it --name nginx_1 -v /opt/test:/opt:ro nginx:1.12 /bin/bash
#在容器中的目录后添加ro,设置为只读则不能对文件进行更改动作
rm -f 1.txt
#结果如下
rm: cannot remove '1.txt': Read-only file system

vi 1.txt
#对内容进行修改之后保存的结果如下
E45: 'readonly' option is set (add ! to override)

mysql

  1. 运行mysql

    docker run \
    -p 5001:3306 \
    --name mysql \
    -v /docker/mysql/conf:/etc/mysql/conf.d \
    -v /docker/mysql/logs:/logs \
    -v /docker/mysql/data:/var/lib/mysql \
    -e MYSQL_ROOT_PASSWORD=root \
    -d mysql:5.7
    
  2. 使用navicat连接mysql

  1. 进入docker mysql

    docker exec -it mysql /bin/bash
    
  2. 在mysql容器中执行命令

    mysql -uroot -proot
    
  3. 对挂载数据卷解释

    在宿主机上面简历挂在的数据卷,防止数据丢失

    1.1创建本地数据库目录、配置文件以及日志目录(方便进行容器数据卷挂载)
    注:因为mysql容器一旦销毁,数据库也就随之销毁,为了解决这个问题,docker官方提出了容器数据卷技术,就是在宿主机上新建一些目录与容器内的目录映射,当容器销毁时,宿主机上的目录文件不会消失,依然存在.
    新建目录命令:

    # 建立宿主机数据库目录
    mkdir /root/mysql/datadir
    # 建立宿主机数据库配置文件
    mkdir /root/mysql/conf
    # 建立宿主机数据库日志目录
    mkdir /root/mysql/log
    

    1.2启动容器并挂载数据卷

    docker run --name mysql -p 3306:3306 -v /root/mysql/datadir:/var/lib/mysql -v /root/mysql/conf:/etc/mysql/conf.d -v /root/mysql/logs:/var/log/mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
    

    对应的参数解释如下:

    -p 3306:3306
    端口映射,将宿主机3306端口与容器3306端口做映射,这样就可以通过宿主机IP+端口访问容器的3306端口了
    格式:-p 宿主机端口:容器端口
    –name mysql:
    指定容器名字为mysql,也可以不指定,不指定会给容器默认制定一个名字

    -v /root/mysql/datadir:/var/lib/mysql -v /root/mysql/conf:/etc/mysql/conf.d -v /root/mysql/logs:/var/log/mysql
    数据容器卷挂载:
    -v /root/mysql/datadir:/var/lib/mysql:对宿主机数据库目录与容器数据库目录进行映射挂载
    -v /root/mysql/conf:/etc/mysql/conf.d:对宿主机数据库配置文件与容器数据库配置文件进行映射挂载
    -v /root/mysql/logs:/var/log/mysql:对宿主机数据库日志与容器数据库日志进行映射挂载
    -e MYSQL_ROOT_PASSWORD=123456
    配置mysql的root账号的密码为123456(可以根据需要自行修改密码)
    -d:后台执行
    mysql:5.7.32 :镜像id,容器第一次启动要根据镜像来启动,所以镜像id必不可少。可以通过明林docker iamges查看镜像id

    最后,使用Navicat连接mysql测试,测试成功!

Docker 安装 Nginx 容器

https://blog.csdn.net/BThinker/article/details/123507820

Docker 安装 (完整详细版)

Docker 日常命令大全(完整详细版)

说明:
Docker如果想安装软件 , 必须先到 Docker 镜像仓库下载镜像。

Docker官方镜像

1、寻找Nginx镜像

2、下载Nginx镜像
命令 描述

docker pull nginx	下载最新版Nginx镜像 (其实此命令就等同于 : docker pull nginx:latest )
docker pull nginx:xxx	下载指定版本的Nginx镜像 (xxx指具体版本号)

检查当前所有Docker下载的镜像

docker images

3、创建Nginx配置文件
启动前需要先创建Nginx外部挂载的配置文件( /home/nginx/conf/nginx.conf)
之所以要先创建 , 是因为Nginx本身容器只存在/etc/nginx 目录 , 本身就不创建 nginx.conf 文件
当服务器和容器都不存在 nginx.conf 文件时, 执行启动命令的时候 docker会将nginx.conf 作为目录创建 , 这并不是我们想要的结果 。

首先启动一个Nginx容器

docker run --name my-nginx -p 80:80 -d nginx

测试:

http://192.168.136.129

进入容器

docker exec -it my-nginx bash
查看Nginx的html、配置和日志目录
/etc/nginx:配置文件的目录
/usr/share/nginx/html:html目录
/var/log/nginx:日志目录

root@33aab93c60f7:/# find / -name nginx
/etc/default/nginx
/etc/init.d/nginx
/etc/logrotate.d/nginx
/etc/nginx
find: '/proc/1/map_files': Operation not permitted
find: '/proc/31/map_files': Operation not permitted
find: '/proc/32/map_files': Operation not permitted
find: '/proc/38/map_files': Operation not permitted
/usr/lib/nginx
/usr/sbin/nginx
/usr/share/doc/nginx
/usr/share/nginx
/var/cache/nginx
/var/log/nginx

exit退出容器,在/opt下创建nginx目录用来存放html、配置和日志目录

mkdir /opt/nginx

拷贝容器中nginx的配置目录到/opt/nginx,并改名为conf

docker cp my-nginx:/etc/nginx/conf.d /opt/nginx/conf.d
docker cp my-nginx:/usr/share/nginx/html /opt/nginx
docker cp my-nginx:/var/log/nginx /opt/nginx/log

删除容器

docker rm -f my-nginx

启动nginx容器并挂载目录

docker run -p 80:80 --name nginx \
    -v /opt/nginx/conf.d:/etc/nginx.d \
    -v /opt/nginx/html:/usr/share/nginx/html \
    -v /opt/nginx/log:/var/log/nginx \
    -d nginx

5、结果检测

6、修改内容进行展示

6.删除容器,重新挂载,测试修改后的内容是否还在

docker rm -f nginx

docker run -p 80:80 --name nginx \
    -v /opt/nginx/conf.d:/etc/nginx.d \
    -v /opt/nginx/html:/usr/share/nginx/html \
    -v /opt/nginx/log:/var/log/nginx \
    -d nginx

Docker安装Redis

一、Docker搜索redis镜像
命令:docker search <镜像名称>

docker search redis

可以看到有很多redis的镜像,此处因没有指定版本,所以下载的就是默认的最新版本 。redis latest.

二、Docker拉取镜像
命令::docker pull <镜像名称>:<版本号>

docker pull redis

三、Docker挂载配置文件
接下来就是要将redis 的配置文件进行挂载,以配置文件方式启动redis 容器。(挂载:即将宿主的文件和容器内部目录相关联,相互绑定,在宿主机内修改文件的话也随之修改容器内部文件)

1)、挂载redis的配置文件

2)、挂载redis 的持久化文件(为了数据的持久化)。

本人的配置文件是放在

liunx 下redis.conf文件位置: /home/redis/myredis/redis.conf

liunx 下redis的data文件位置 : /home/redis/myredis/data

位置可以自己随便选择哈

mkdir -p /home/redis/myredis 命令 是不存在就直接创建/home/redis/myredis 文件夹

myredis.conf 是我手动上传的。 (文件在文末,redis.conf的标准文件在redis官网也可以找到)

四、启动redis 容器

docker run --restart=always --log-opt max-size=100m --log-opt max-file=2 -p 6379:6379 --name myredis -v /home/redis/myredis/myredis.conf:/etc/redis/redis.conf -v /home/redis/myredis/data:/data -d redis redis-server /etc/redis/redis.conf  --appendonly yes  --requirepass 000415

–restart=always 总是开机启动
–log是日志方面的
-p 6379:6379 将6379端口挂载出去
–name 给这个容器取一个名字
-v 数据卷挂载
/home/redis/myredis/myredis.conf:/etc/redis/redis.conf 这里是将 liunx 路径下的myredis.conf 和redis下的redis.conf 挂载在一起。
/home/redis/myredis/data:/data 这个同上
-d redis 表示后台启动redis
redis-server /etc/redis/redis.conf 以配置文件启动redis,加载容器内的conf文件,最终找到的是挂载的目录 /etc/redis/redis.conf 也就是liunx下的/home/redis/myredis/myredis.conf
–appendonly yes 开启redis 持久化
–requirepass 000415 设置密码 (如果你是通过docker 容器内部连接的话,就随意,可设可不设。但是如果想向外开放的话,一定要设置,我被搞过,可以看这篇文章“阿里云服务器中毒‘Kirito666’经历”)
成功界面

五、测试
1、通过docker ps指令查看启动状态

docker ps -a |grep myredis # 通过docker ps指令查看启动状态,是否成功.

2、查看容器运行日志
命令:docker logs –since 30m <容器名>

此处 –since 30m 是查看此容器30分钟之内的日志情况。

docker logs --since 30m myredis

3、容器内部连接进行测试
进入容器

命令:docker exec -it <容器名> /bin/bash

此处跟着的redis-cli是直接将命令输在上面了。

docker exec -it myredis redis-cli

进入之后,我直接输入查看命令:

error是没有权限验证。(因为设置了密码的。)

验证密码:

auth 密码

查看当前redis有没有设置密码:(得验证通过了才能输入的)

config get requirepass

六、配置文件
myredis.conf

# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#bind 127.0.0.1

protected-mode no

port 6379

tcp-backlog 511

requirepass 000415

timeout 0

tcp-keepalive 300

daemonize no

supervised no

pidfile /var/run/redis_6379.pid

loglevel notice

logfile ""

databases 30

always-show-logo yes

save 900 1
save 300 10
save 60 10000

stop-writes-on-bgsave-error yes

rdbcompression yes

rdbchecksum yes

dbfilename dump.rdb

dir ./

replica-serve-stale-data yes

replica-read-only yes

repl-diskless-sync no

repl-disable-tcp-nodelay no

replica-priority 100

lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no

appendonly yes

appendfilename "appendonly.aof"

no-appendfsync-on-rewrite no

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

aof-load-truncated yes

aof-use-rdb-preamble yes

lua-time-limit 5000

slowlog-max-len 128

notify-keyspace-events ""

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

list-max-ziplist-size -2

list-compress-depth 0

set-max-intset-entries 512

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

hll-sparse-max-bytes 3000

stream-node-max-bytes 4096
stream-node-max-entries 100

activerehashing yes

hz 10

dynamic-hz yes

aof-rewrite-incremental-fsync yes

rdb-save-incremental-fsync yes

七、Docker删除Redis
教了大家怎么装,咱们也得学会怎么卸载哈,不然没法成为熟练工人(手动狗头)

6.1、删除Redis 容器
查看所有在运行的容器:
命令:

docker ps -a

停止运行的Redis

停止命令:docker stop <容器名>

docker stop myredis # myredis 是我启动redis 命名的别

删除redis 容器:

删除容器命令: docker rm <容器名>

docker rm myredis

6.2、删除Redis镜像
删除容器后,我们开始删除redis镜像。

查看全部镜像 命令:

docker images

删除镜像 命令 docker rmi <容器 id>

docker rmi 739b59b96069 # 这是我镜像redis id

可以看到Redis 的镜像已经被删除啦。

原文链接:https://blog.csdn.net/weixin_45821811/article/details/116211724

docker安装tomcat

1、安装tomcat镜像
访问docker hub仓库找出你想安装的版本:docker hub
网址:https://registry.hub.docker.com/

直接在搜索框里搜tomcat就可以,如果想把自己镜像放到上边就自己注册一个账号

选择tomcat

这里先看下啥镜像都没有,容器也没有。

[root@wangazure ~]# docker images   #查看所有镜像
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE
[root@wangazure ~]# docker ps 		#查看所有容器
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
[root@wangazure ~]# 

把复制官网上的命令贴在这等着就好了

docker pull tomcat:8.0 		#拉取tomcat镜像

再看一下docker里的镜像,这里就有了tomcat镜像

[root@wangazure ~]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
tomcat       8.0       ef6a7c98d192   3 years ago   356MB
[root@wangazure ~]# 

2、运行tomcat镜像

 docker run -p 8080:8080 -d --name tomcat01 tomcat:8.0

注意:最新版tomcat10中,webapps目录中没有内容,需要从webapps.dist中拷贝所有内容到webapps中

docker exec -it tomcat bash

root@2779cb76c827:/usr/local/tomcat/webapps.dist# cp * ../webapps -r

这里的 -p 8080:8080 的意思就是把容器的端口号8080映射到linux系统里的8080端口这里的两个端口8080前边的是linux里的端口号,后边的是运行的tomcat里的端口,外边也就是linux里的端口号可以随便换的,而容器里的是不能换的。
这里的 -d 的意思是后台运行
这里 –name 是给容器起一个别名

再次查看docker里的容器

docker ps

这里去访问浏览器8080端口就可以看见tom猫了,版本也是8.0的(如果用的阿里云的服务器注意要开放8080端口再访问)

3、开多个端口给tomcat
这里我又开放了一个8082的作为tomcat,名字为tomcat02,注意:这里的名字(tomcat02)和Linux端口不能和上边的名字(tomcat01)一样!

docker run -p 8082:8080 -d --name tomcat02 tomcat:8.0

上浏览器访问8082,又见tomcat猫了

4、tomcat容器的操作
关闭容器

docker stop 1adf1a9b21a1 

再次查看容器,就只剩一个8080端口的了

启动关闭的容器

docker start tomcat02

暂停容器

docker pause tomcat02

暂停之后这里的STATUS会显示Up About a minute (Paused)表示已暂停,你访问8082也访问不到。

恢复容器

docker unpause tomcat02

恢复之后Up About a minute (Paused)就消失了,8082也能访问了。

杀死容器

 docker kill tomcat02

删除容器

docker rm tomcat02

rm如果不行就加-f

删除所有的容器

docker rm -f $(docker ps -aq)

查看容器运行的日志

docker logs tomcat02

也可以用docker logs -f id/name(容器的id或名字),用这个命令之后就进去了,想出来就按CTRL+C

进入容器内部
之后就可以到容器里了,镜像是不能动的,容器是可以进去的

docker exec -it tomcat01 bash #-it进入交互模式最后记得加bash

退出容器

 exit

容器与宿主机文件相互复制拷贝
用vi命令建一个test.html

[root@wangazure ~]# vi test.html #建一个test.html
[root@wangazure ~]# cat test.html #查看test.html
<html>
<body>
hello,docker!!!
</body>
</html>
[root@wangazure ~]# 

CP命令

[root@wangazure ~]# docker cp test.html tomcat01:/usr/local/tomcat/webapps #复制到webapps里
[root@wangazure ~]# docker exec -it tomcat01 bash #进入到容器里
root@91ff3bca3aee:/usr/local/tomcat# ls
LICENSE  NOTICE  RELEASE-NOTES	RUNNING.txt  bin  conf	include  lib  logs  native-jni-lib  temp  webapps  work
root@91ff3bca3aee:/usr/local/tomcat# cd webapps/
root@91ff3bca3aee:/usr/local/tomcat/webapps# ls
ROOT  docs  examples  host-manager  manager  test.html
root@91ff3bca3aee:/usr/local/tomcat/webapps# mkdir test #创建test文件
root@91ff3bca3aee:/usr/local/tomcat/webapps# cp test.html test #把test.html复制到test文件夹里
root@91ff3bca3aee:/usr/local/tomcat/webapps# cd test
root@91ff3bca3aee:/usr/local/tomcat/webapps/test# ls
test.html
root@91ff3bca3aee:/usr/local/tomcat/webapps/test# 

如果想把容器里的东西复制出来,那就把两个地址一换就好了

[root@wangazure ~]# ls
install.sh
[root@wangazure ~]# docker cp tomcat01:/usr/local/tomcat/webapps/examples /root
[root@wangazure ~]# ls
examples  install.sh
[root@wangazure ~]# cd examples/
[root@wangazure examples]# ls
index.html  jsp  servlets  WEB-INF  websocket
[root@wangazure examples]# 

5、查看所有容器

[root@wangazure ~]# docker  ps
CONTAINER ID   IMAGE        COMMAND             CREATED          STATUS          PORTS                                       NAMES
1adf1a9b21a1   tomcat:8.0   "catalina.sh run"   17 minutes ago   Up 5 minutes    0.0.0.0:8082->8080/tcp, :::8082->8080/tcp   tomcat02
643b0adcfd17   tomcat:8.0   "catalina.sh run"   23 minutes ago   Up 23 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   tomcat01
[root@wangazure ~]# docker ps -aq
1adf1a9b21a1
643b0adcfd17
[root@wangazure ~]# docker ps -qa
1adf1a9b21a1
643b0adcfd17
[root@wangazure ~]# docker ps -a
CONTAINER ID   IMAGE        COMMAND             CREATED          STATUS          PORTS                                       NAMES
1adf1a9b21a1   tomcat:8.0   "catalina.sh run"   16 minutes ago   Up 4 minutes    0.0.0.0:8082->8080/tcp, :::8082->8080/tcp   tomcat02
643b0adcfd17   tomcat:8.0   "catalina.sh run"   22 minutes ago   Up 22 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   tomcat01
[root@wangazure ~]# 

原文链接:https://blog.csdn.net/weixin_44953395/article/details/122639514

创建Docker镜像

  1. 创建文件:dockerfile

    FROM node
    LABEL maintainer pcbhyy@163.com
    RUN git clone -q https://gitee.com/null_476_2723/todo.git
    WORKDIR todo
    RUN npm install > /dev/null
    EXPOSE 8000
    CMD ["npm","start"]
    
  2. 构建镜像(在dockerfile文件所在目录下执行)

    docker build .
    
  3. 查看镜像

    docker images
    
  4. 打包镜像

    docker tag 2654988fe97b todoapp
    
  5. 运行容器

    docker run -i -t -p 8000:8000 --name example1 todoapp
    //后台运行
    docker run -i -t -d -p 8000:8000 --name example1 todoapp
    
  6. 测试

    • 开放端口:8000

      firewall-cmd --zone=public --add-port=5672/tcp --permanent  # 开放5672端口
      
      firewall-cmd --zone=public --remove-port=5672/tcp --permanent #关闭5672端口
      
      firewall-cmd --reload  # 配置立即生效
      
      //关闭防火墙
      systemctl stop firewalld.service
      
    • 测试

      http://192.168.136.129:8000
      

在Docker Hub上分享镜像

  1. 在 Docker Hub创建账号(如:pcbhyy)

  2. 在账号中创建仓库(如:mytest),一个仓库可以放置一个镜像的多个tag版本

  3. 在本地创建 镜像(可从远程拉取,tomcat:9.0)

  4. 给镜像打标签

    docker tag 仓库名:版本号 dockerhub用户名/仓库名:版本号
    
    如:
    docker tag tomcat:9.0 pcbhyy/mytest:9
    
  5. 登录docker hub

    docker login
    
    --输入用户名和密码
    
  6. 推送镜像到docker hub

    docker push dockerhub用户名/仓库名:版本号
    
    如:
    docker push pcbhyy/mytest:9
    
  7. 拉取自己的镜像

    docker pull pcbhyy/mytest:9
    

中文手册:http://www.dockerinfo.net/document

简而言之,容器运行着由镜像定义的系统。这些镜像由一个或多个层(或差异集)加上一些Docker的元数据组成。

Docker的中心功能是构建、分发及在任何具有Docker的地方运行软件。对终端用户而言,Docker是他们运行的一个命令行程序。就像Git(或任何源代码控制工具)一样,这个程序具有用于执行不同操作的子命令。表1-1中列出了将在宿主机上使用的主要的Docker子命令。

docker build 构建一个Docker镜像

docker run 以容器形式运行一个Docker镜像

docker commit 将一个Docker容器作为一个镜像提交

docker tag 给一个Docker镜像打标签

看待镜像和容器的一种方式是将它们类比成程序与进程。一个进程可以视为一个“被执行的应用程序”,同样,一个Docker容器可以视为一个运行中的Docker镜像。

如果读者熟悉面向对象原理,看待镜像和容器的另一种方法是将镜像看作类而将容器看作对象。对象是类的具体实例,同样,容器是镜像的实例。用户可以从单个镜像创建多个容器,就像对象一样,它们之间全都是相互隔离的。不论用户在对象内修改了什么,都不会影响类的定义——它们从根本上就是不同的东西。

创建Docker镜像的方式

方法 描述 技巧
Docker命令/“手工” 使用docker run 启动一个容器,并在命令行输入命令来创建镜像。使用docker commit来创建一个新镜像 详见技巧15
Dockerfile 从一个已知基础镜像开始构建,并指定一组有限的简单命令来构建 稍后讨论
Dockerfile及配置管理(configuration management,CM)工具 与Dockerfile相同,不过将构建的控制权交给了更为复杂的CM工具 详见技巧55
从头创建镜像并导入一组文件 从一个空白镜像开始,导入一个含有所需文件的TAR文件 详见技巧11

编写一个Dockerfile

Dockerfile是一个包含一系列命令的文本文件。本示例中我们将使用的Dockerfile如代码清单1-1所示。创建一个新目录,移动到这个目录里,然后使用这些内容创建一个名为“Dockerfile”的文件。

代码清单1-1 todoapp Dockerfile

FROM node  ⇽--- 定义基础镜像
LABEL maintainer ian.miell@gmail.com  ⇽--- 声明维护人员
RUN git clone -q https://gitee.com/null_476_2723/todo.git  ⇽--- 克隆todoapp代码
WORKDIR todo  ⇽--- 移动到新的克隆目录
RUN npm install > /dev/null  ⇽--- 执行node包管理器的安装命令(npm)
EXPOSE 8000  ⇽--- 指定从所构建的镜像启动的容器需要监听这个端口
CMD ["npm","start"]  ⇽--- 指定在启动时需要执行的命令
FROM node
LABEL maintainer ian.miell@gmail.com
RUN git clone -q https://gitee.com/null_476_2723/todo.git
WORKDIR todo
RUN npm install > /dev/null
EXPOSE 8000
CMD ["npm","start"]

Dockerfile的开始部分是使用FROM命令定义基础镜像。本示例使用了一个Node.js镜像以便访问Node.js程序。官方的Node.js镜像名为node。

接下来,使用LABEL命令声明维护人员。在这里,我们使用的是其中一个人的电子邮件地址,读者也可以替换成自己的,因为现在它是你的Dockerfile了。这一行不是创建可工作的Docker镜像所必需的,不过将其包含进来是一个很好的做法。到这个时候,构建已经继承了node容器的状态,读者可以在它上面做操作了。
接下来,使用RUN命令克隆todoapp代码。这里使用指定的命令获取应用程序的代码:在容器内运行git。在这个示例中,Git是安装在基础node镜像里的,不过读者不能对这类事情做假定。

现在使用WORKDIR命令移动到新克隆的目录中。这不仅会改变构建环境中的目录,最后一条WORKDIR命令还决定了从所构建镜像启动容器时用户所处的默认目录。
接下来,执行node包管理器的安装命令(npm)。这将为应用程序设置依赖。我们对输出的信息不感兴趣,所以将其重定向到/dev/null上。
由于应用程序使用了8000端口,使用EXPOSE命令告诉Docker从所构建镜像启动的容器应该监听这个端口。

最后,使用CMD命令告诉Docker在容器启动时将执行哪条命令。

这个简单的示例演示了Docker及Dockerfile的几个核心功能。Dockerfile是一组严格按顺序执行的有限的命令集的简单序列。它影响了最终镜像的文件和元数据。这里的RUN命令通过签出并安装应用程序影响了文件系统,而EXPOSE、CMD和WORKDIR命令影响了镜像的元数据。

输出看起来和下面类似。

Sending build context to Docker daemon 2.048kB  ⇽---  Docker会上传docker build指定目录下的文件和目录
Step 1/7 : FROM node  ⇽--- 每个构建步骤从 1 开始按顺序编号,并与命令一起输出
 ---> 2ca756a6578b  ⇽--- 每个命令会导致一个新镜像被创建,其镜像ID在此输出
Step 2/7 : LABEL maintainer ian.miell@gmail.com
 ---> Running in bf73f87c88d6
 ---> 5383857304fc
Removing intermediate container bf73f87c88d6  ⇽--- 为节省空间,在继续前每个中间容器会被移除
Step 3/7 : RUN git clone -q https://github.com/docker-in-practice/todo.git
 ---> Running in 761baf524cc1
 ---> 4350cb1c977c
Removing intermediate container 761baf524cc1
Step 4/7 : WORKDIR todo
 ---> a1b24710f458
Removing intermediate container 0f8cd22fbe83
Step 5/7 : RUN npm install > /dev/null
 ---> Running in 92a8f9ba530a
npm info it worked if it ends with ok  ⇽--- 构建的调试信息在此输出(限于篇幅,本代码清单做了删减)
 [...]
 npm info ok
 ---> 6ee4d7bba544
Removing intermediate container 92a8f9ba530a
Step 6/7 : EXPOSE 8000
 ---> Running in 8e33c1ded161
 ---> 3ea44544f13c
Removing intermediate container 8e33c1ded161
Step 7/7 : CMD npm start
 ---> Running in ccc076ee38fe
 ---> 66c76cea05bb
Removing intermediate container ccc076ee38fe
Successfully built 66c76cea05bb  ⇽--- 此次构建的最终镜像ID,可用于打标签

现在,拥有了一个具有镜像ID(前面示例中的“66c76cea05bb”,不过读者的ID会不一样)的Docker镜像。总是引用这个ID会很麻烦,可以为其打标签以方便引用,如图1-8所示。

输入图1-8所示的命令,将66c76cea05bb替换成读者生成的镜像ID。
现在就能从一个Dockerfile构建自己的Docker镜像副本,并重现别人定义的环境了!

运行一个Docker容器

运行一个Docker容器

$ docker run -i -t -p 8000:8000 --name example1 todoapp  ⇽---  docker run子命令启动容器,-p将容器的 8000 端口映射到宿主机的8000端口上,--name给容器赋予一个唯一的名字,最后一个参数是镜像
npm install
npm info it worked if it ends with ok
npm info using npm@2.14.4
npm info using node@v4.1.1
npm info prestart todomvc-swarm@0.0.1

> todomvc-swarm@0.0.1 prestart /todo  ⇽--- 容器的启动进程的输出被发送到终端中
> make all

npm install
npm info it worked if it ends with ok
npm info using npm@2.14.4
npm info using node@v4.1.1
npm WARN package.json todomvc-swarm@0.0.1 No repository field.
npm WARN package.json todomvc-swarm@0.0.1 license should be a valid SPDX
➥ license expression
npm info preinstall todomvc-swarm@0.0.1
npm info package.json statics@0.1.0 license should be a valid SPDX license
➥ expression
npm info package.json react-tools@0.11.2 No license field.
npm info package.json react@0.11.2 No license field.
npm info package.json node-
     jsx@0.11.0 license should be a valid SPDX license expression
npm info package.json ws@0.4.32 No license field.
npm info build /todo
npm info linkStuff todomvc-swarm@0.0.1
npm info install todomvc-swarm@0.0.1
npm info postinstall todomvc-swarm@0.0.1
npm info prepublish todomvc-swarm@0.0.1
npm info ok
if [ ! -e dist/ ]; then mkdir dist; fi
cp node_modules/react/dist/react.min.js dist/react.min.js

LocalTodoApp.js:9:    // TODO: default english version
LocalTodoApp.js:84:            fwdList = this.host.get('/TodoList#'+listId);
 // TODO fn+id sig
TodoApp.js:117:        // TODO scroll into view
TodoApp.js:176:        if (i>=list.length()) { i=list.length()-1; } // TODO
➥ .length
local.html:30:    <!-- TODO 2-split, 3-split -->
model/TodoList.js:29:        // TODO one op - repeated spec? long spec?
view/Footer.jsx:61:        // TODO: show the entry's metadata
view/Footer.jsx:80:            todoList.addObject(new TodoItem()); // TODO
➥ create default
view/Header.jsx:25:        // TODO list some meaningful header (apart from the
➥ id)

npm info start todomvc-swarm@0.0.1

> todomvc-swarm@0.0.1 start /todo
> node TodoAppServer.js

Swarm server started port 8000
^Cshutting down http-server...   ⇽--- 在此按组合键Ctrl+C终止进程和容器
 closing swarm host...
swarm host closed
npm info lifecycle todomvc-swarm@0.0.1~poststart: todomvc-swarm@0.0.1
npm info ok

$ docker ps -a  ⇽--- 执行这个命令查看已经启动和移除的容器,以及其ID和状态(就像进程一样)
 CONTAINER ID  IMAGE    COMMAND      CREATED        STATUS PORTS NAMES
b9db5ada0461  todoapp  "npm start"  2 minutes ago  Exited (0) 2 minutes ago
➥                example1

$ docker start example1  ⇽--- 重新启动容器,这次是在后台运行
example1
$ docker ps
CONTAINER ID  IMAGE    COMMAND      CREATED       STATUS
➥ PORTS                    NAMES
b9db5ada0461  todoapp  "npm start"  8 minutes ago  Up 10 seconds
➥ 0.0.0.0:8000->8000/tcp example1  ⇽--- 再次执行ps命令查看发生变化的状态
$ docker diff example1  ⇽---  docker diff子命令显示了自镜像被实例化成一个容器以来哪些文件受到了影响
C /root
C /root/.npm
C /root/.npm/_locks
C /root/.npm/anonymous-cli-metrics.json
C /todo  ⇽--- 修改了/todo目录(C)
A /todo/.swarm  ⇽--- 增加了/todo/.swarm目录(A)
A /todo/.swarm/_log
A /todo/dist
A /todo/dist/LocalTodoApp.app.js
A /todo/dist/TodoApp.app.js
A /todo/dist/react.min.js
C /todo/node_modules

docker run子命令启动容器。-p标志将容器的8000端口映射到宿主机的8000端口上,读者现在应该可以使用浏览器访问http://localhost:8000来查看这个应用程序了。--name标志赋予了容器一个唯一的名称,以便后面引用。最后的参数是镜像名称。

一旦容器启动,我们就可以按组合键Ctrl+C终止进程和容器。读者可以执行ps命令查看被启动且未被移除的容器。注意,每个容器都具有自己的容器 ID 和状态,与进程类似。它的状态是Exited(已退出),不过读者可以重新启动它。这么做之后,注意状态已经改变为Up(运行中),且容器到宿主机的端口映射现在也显示出来了。

docker diff子命令显示了自镜像被实例化成一个容器以来哪些文件受到了影响。在这个示例中,todo目录被修改了(C),而其他列出的文件是新增的(A)。没有文件被删除(D),这是另一种可能性。

Docker的架构

Docker守护进程

Docker守护进程(见图2-2)是用户与Docker交互的枢纽,因而它是理解所有相关部分的最佳切入点。它控制着用户机器上的Docker访问权限,管理着容器与镜像的状态,同时代理着与外界的交互。

守护进程是运行在后台的一个进程,不在用户的直接控制之下。服务器是负责接受客户端请求,并执行用于满足该请求所需的操作的一个进程。守护进程通常也是服务器,接收来自客户端的请求,为其执行操作。docker命令是一个客户端,而Docker守护进程则作为服务器对Docker容器和镜像进行操作。

Docker客户端

Docker客户端(见图2-4)是Docker架构中最简单的部件。在主机上输入docker run或docker pull这类命令时运行的便是它。它的任务是通过HTTP请求与Docker守护进程通信。

CentOS中安装Docker 引擎

地址:https://docs.docker.com/engine/install/centos/

设置存储库

安装yum-utils包(提供yum-config-manager 实用程序)并设置稳定存储库。

 sudo yum install -y yum-utils
 sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

Another app is currently holding the yum lock解决方法

rm -f /var/run/yum.pid

安装 Docker 引擎

  1. 安装最新版本的 Docker Engine 和 containerd

    yum install docker-ce docker-ce-cli containerd.io
    
  2. 启动 Docker

    systemctl start docker
    
  3. 通过运行hello-world 映像验证 Docker Engine 是否已正确安装。

    docker run hello-world
    

    此命令下载测试映像并在容器中运行它。当容器运行时,它会打印一条消息并退出。

卸载 Docker 引擎

  1. 卸载 Docker Engine、CLI 和 Containerd 包:

    $ sudo yum remove docker-ce docker-ce-cli containerd.io
    
  2. 主机上的映像、容器、卷或自定义配置文件不会自动删除。删除所有镜像、容器和卷:

    $ sudo rm -rf /var/lib/docker
    $ sudo rm -rf /var/lib/containerd
    

您必须手动删除任何已编辑的配置文件。

常用命令

docker images 查看所有镜像
docker history 镜像ID 查看镜像历史
docker stats 查看状态
docker run centos /bin/bash 运行镜像
docker build . 使用 dockerfile 创建镜像
docker build –tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签
docker tag 镜像id 镜像标签名 给镜像加标签名
exit 退出centos容器
ctrl +p ,ctrl+q 不退出 容器,回到系统
docker ps -a 查看所有容器
docker stop 容器id 停止容器
docker attach <容器ID或者容器名> 进入已经启动的容器
docker exec -it centos /bin/bash 在启动的容器中执行命令
docker rmi 仓库名:标签名 删除镜像的某个标签
docker rmi 镜像ID 删除镜像
docker rm -f <容器ID或者容器名> <容器ID或者容器名> 强制删除一个或多个容器
docker rm -f $(docker ps -a -q) 强制删除所有运行容器
docker volume prune -f 删除未被任何容器使用的本地卷
docker cp /root/my.cnf 16ff8a4cd5d9:/etc/mysql/my.cnf 拷贝文件到容器中
docker logs nginx 查看容器日志

使用socat监控Docker API流量

安装

yum install -y socat

要创建这个代理,会用到socat

$ socat -v UNIX-LISTEN:/tmp/dockerapi.sock,fork \
 UNIX-CONNECT:/var/run/docker.sock &

socat是一个强大的命令,能让用户在两个几乎任意类型的数据通道之间中继数据。如果熟悉netcat,可以将其看作是加强版的netcat。你可使用系统的标准包管理器来安装它。

在这条命令中,-v用于提高输出的可读性,带有数据流的指示。UNIX-LISTEN部分是让socat在一个Unix套接字上进行监听,fork确保socat不会在首次请求后退出,而UNIX-CONNECT是让socat连接到Docker的Unix套接字。&符号指定在后台执行该命令。如果你通常使用sudo来运行Docker客户端,这里也需要这么做。
发往守护进程的请求所经过的新路由如图2-6所示。所有双向流量都会被socat看到,并与Docker客户端所提供的任何输出一起记录到终端日志中。

现在一个简单的docker命令的输出看起来将类似下面这样:

docker -H unix:///tmp/dockerapi.sock ps -a  ⇽--- 用于查看请求与响应所发送的命令

使用socat不仅对Docker来说是一种强大的调试方式,对工作过程中可能碰到的任何其他网络服务也是如此。

  • Socat就像一把瑞士军刀,可以处理很多不同的协议。上述示例演示的是在Unix套接字上进行监听,不过你也可以使用TCP-LISTEN:2375,fork取代UNIX-LISTEN:…参数,让它监听外部端口。这相当于技巧1的一个更简单的版本。使用这个方法无须重启Docker守护进程(这会杀掉所有运行中的容器),可根据需要启动或停止socat监听器。
  • 由于前一条设置起来如此简单,并且是临时的,你可以将其与技巧47结合起来,以便远程加入同事的运行容器中,协助他们调试问题。你也可以使用很少用到的docker attach命令加入他们以docker run启动的同一个终端中,以便直接协作。
  • 如果你有一台共享的Docker服务器(或许是使用技巧1设置的),你可以使用对外公开的功能将socat设置为外界与Docker套接字之间的代理,将其作为原始的审计日志,记录下所有请求来源及所进行的操作。

使用端口连接容器

Docker容器从一开始就被设计用于运行服务。在大多数情况下,都是这样或那样的 HTTP服务。其中很大一部分是可以使用浏览器访问的Web服务。

这会造成一个问题。如果有多个Docker容器运行在其内部环境中的80端口上,它们将无法全部通过宿主机的80端口进行访问。本技巧将展示如何通过公开和映射容器的端口来管理这一常见场景。

问题

想要将多个运行在同一个端口的Docker容器服务公开到宿主机上。

解决方案

使用Docker的-p标志将容器的端口映射到宿主机上。

在这个示例中,我们将使用tutum-wordpress镜像。这里假定我们想在宿主机上运行两个实例来服务不同的博客。
由于此前有很多人想这么做,已经有人准备了任何人都可以获取并启动的镜像。要从外部地址获取镜像,可以使用docker pull命令。在默认情况下,镜像将从Docker Hub下载:

docker pull tutum/wordpress

如果镜像在你的机器上还不存在,当你试图运行它们时,也会自动获取。

要运行第一个博客,可使用如下命令:

docker run -d -p 10001:80 --name blog1 tutum/wordpress

这里的docker run命令以守护进程方式(-d)及发布标志(-p)运行容器。它指定将宿主机端口(10001)映射到容器端口(80)上,并赋予该容器一个名称用于识别它(–name blog1 tutum/wordpress)。

可以对第二个博客做相同操作:

docker run -d -p 10002:80 --name blog2 tutum/wordpress

如果现在执行这个命令:

docker ps | grep blog

将看到列出的两个博客容器及其端口映射,看起来像下面这样:

$ docker ps | grep blog
9afb95ad3617 tutum/wordpress:latest "/run.sh" 9 seconds ago
➥ Up 9 seconds  3306/tcp, 0.0.0.0:10001->80/tcp blog1
31ddc8a7a2fd tutum/wordpress:latest "/run.sh" 17 seconds ago
➥ Up 16 seconds 3306/tcp, 0.0.0.0:10002->80/tcp blog2

现在可以通过浏览http://localhost:10001和http://localhost:10002来访问自己的容器。
要在完成后删除这些容器(假设不想保留它们——我们将在技巧7中利用它们),可执行下面这个命令:

docker rm -f blog1 blog2

如果需要,现在就可以通过管理端口分配在宿主机上运行多个相同的镜像和服务了

Docker注册中心

Docker注册中心允许多个用户使用REST风格API将镜像推送到一个中央存储中,也可以从中拉取镜像。
与Docker自身一样,注册中心代码也是开源的。很多公司(如我们公司)建立了私有注册中心在内部存储和共享专有的镜像。这是在我们进一步说明Docker公司的注册中心之前,我们将要讨论的东西。

问题

想要一个在本地托管镜像的方法。

解决方案

在本地网络上建立一个注册中心服务器。
简单地在一台具有大量磁盘空间的机器上发起以下命令:

docker run -d -p 5000:5000 -v $HOME/registry:/var/lib/registry registry:2

这条命令让注册中心运行于Docker宿主机的5000端口上(-p 5000:5000)。使用-v标志,它可以使宿主机(/var/lib/registry)上的registry在容器中用作$HOME/registry。因此,该registry的文件将存储在/var/lib/registry目录下。
在所有想访问这个注册中心的机器上,将下列内容添加到守护进程选项中(HOSTNAME是新的注册中心服务器的主机名或IP地址):–insecure-registry HOSTNAME(有关如何执行此操作的详细信息,参见附录B)。现在可以发出docker push HOSTNAME:5000/image:tag命令。

Docker Hub

Docker Hub(见图2-10)是由Docker公司维护的一个注册中心。它拥有成千上万个镜像可供下载和运行。任何Docker用户都可以在上面创建免费账号,并存储公共Docker镜像。除了用户提供的镜像,上面还维护着一些作为参考的官方镜像。

镜像受用户认证的保护,同时具有一个与GitHub类似的支持率打星系统。这些官方镜像的表现形式可能是Linux发行版(如Ubuntu或Cent OS)、预装软件包(如Node.js)或完整的软件栈(如WordPress)。

查找并运行一个Docker镜像

问题

想要查找一个Docker镜像形式的应用程序或工具,并进行尝试。

解决方案

使用docker search命令来查找要拉取的镜像,然后运行它。

docker search mysql

docker search的输出是按评星数量排序的

一旦选择了一个镜像,就可以通过对其名称执行docker pull命令来下载它:

docker pull mysql

接着,可以使用-t和-i标志以交互方式运行它。-t标志指明创建一个TTY设备(一个终端),而-i标志指明该Docker会话是交互式的:

docker run -e MYSQL_ROOT_PASSWORD=root --name mysql -it -d -p 5001:3306  mysql

-p 5001:3306  //宿主机使用5001端口,容器使用3306端口
-d 守护进程,后台运行
--name mysql:给容器命名为:mysql

使用 tomcat 镜像

  1. 拉取

    docker pull tomcat:9.0
    
  2. 运行

    docker run -it -d -p 8889:8080 --name tomcat9 tomcat:9.0
    
  3. 进入容器

    docker exec -it  tomcat9 bash 
    
  4. 拷贝文件

    cp webapps.dist/. webapps -r
    
  5. 退出容器

    exit
    
  6. 重启容器

    docker restart tomcat9
    
  7. 测试

    http://ip:8889
    

使用 redis 镜像

  1. 拉取

    docker pull redis
    
  2. 运行容器

    docker run -d --name redis -p 6379:6379 redis redis-server --appendonly yes --requirepass "123456"
    

    –appendonly yes :使用 aof 模式持久化数据

    requirepass “123456” :服务器连接密码

  3. 在windows 中安装了 redis,使用 redis-cli 连接 docker reids

    redis-cli -h 111.229.80.53 -p 6379 -a 123456
    

    -a 123456:使用密码 123456 连接服务器

  4. 可以进入到docker容器中的redis测试

    进入容器:
    docker exec -it redis /bin/bash
    
    到redis-cli目录
    cd /usr/local/bin
    
    执行命令:
    ./redis-cli -a 123456
    
    测试:
    set name tom
    get name
    

将一个系统拆成微服务容器

在Docker的世界里,公认的最佳实践是尽可能多地把系统拆分开,直到在每个容器上都只运行一个“服务”,并且所有容器都通过网络互相连通。

使用一个容器一个服务的主要原因在于可以更容易通过单一职责原则(single responsibility principle)实现关注点分离(separation of concerns)。如果用户的容器执行的是单一任务,那么可以很方便地把该容器应用到从开发、测试到生产的整个软件开发生命周期里,而无须太担心它与其他组件的交互问题。这就使软件项目可以更敏捷地交付并且具备更好的扩展性。但是,它的确带来了一些管理上的负担,因此,最好思量一下在自己的用例场景下这样做是否真的值得。
暂且不论哪种方案更适合,最佳实践方法至少拥有一个明显的优势——正如所见,在使用Dockerfile时实验和重新构建都比前一套方案快上不少。

问题

想要将应用程序拆分为各个单独的且更易于管理的服务。

解决方案

为每个单独的服务进程对应创建一个容器。

在Docker Hub上分享镜像

  1. 在 Docker Hub创建账号(如:pcbhyy)

  2. 在账号中创建仓库(如:mytest),一个仓库可以放置一个镜像的多个tag版本

  3. 在本地创建 镜像(可从远程拉取,tomcat:9.0)

  4. 给镜像打标签

    docker tag 仓库名:版本号 dockerhub用户名/仓库名:版本号
    
    如:
    docker tag tomcat:9.0 pcbhyy/mytest:9
    
  5. 登录docker hub

    docker login
    
    --输入用户名和密码
    
  6. 推送镜像到docker hub

    docker push dockerhub用户名/仓库名:版本号
    
    如:
    docker push pcbhyy/mytest:9
    

构建镜像

使用ADD指令将文件注入镜像里

问题

想要以一种简洁的方式下载并解压一个压缩包到镜像里。

解决方案

打包并压缩目标文件,然后在Dockerfile里使用ADD指令。
通过mkdir add_example && cd add_example来为这次Docker构建创造一个全新的环境。随后检索一个压缩包,并给它指定一个名字作为后续引用的标识

  1. 下载一个TAR文件

    $ curl \
    https://www.flamingspork.com/projects/libeatmydata/
    ➥ libeatmydata-105.tar.gz > my.tar.g
    
  2. 添加一个TAR文件到镜像里

    FROM debian
    RUN mkdir -p /opt/libeatmydata
    ADD my.tar.gz /opt/libeatmydata/
    RUN ls -lRt /opt/libeatmydata
    

    通过docker build –no-cache .来构建这个Dockerfile

  3. 带有TAR文件的镜像构建

    $ docker build --no-cache .
    

    从输出中可以看到,压缩包被Docker守护进程解压到了目标目录(所有文件的扩展输出已被省略)。Docker支持解压绝大多数标准类型的压缩文件(.gz、.bz2、.xz、.tar)。

    值得留意的是,尽管用户可以从指定的URL下载压缩包,但是只有当它们被存储在本地文件系统时才会被自动解压。这一点可能会导致混淆。

    文件名里带有空格如果指定的文件名里带有空格,用户将需要在ADD(或COPY)时带上引号的形式

    ADD "space file.txt" "/tmp/space file.txt"
    

    人们常问的一个问题是如何添加一个压缩文件,但不对其进行解压。为此,用户应该使用的是COPY命令,该命令看上去和ADD命令完全相同,但是它不会解压任何文件,也不支持通过互联网下载文件。docker –

privileged=true 参数作用

大约在0.6版,privileged被引入docker。
使用该参数,container内的root拥有真正的root权限。
否则,container内的root只是外部的一个普通用户权限。
privileged启动的容器,可以看到很多host上的设备,并且可以执行mount。
甚至允许你在docker容器中启动docker容器

IPv4 forwarding is disabled

装完了Docker,然后启动镜像,发现没有网络,而且不能ifconfig,因网桥配置完后,需要开启转发,不然容器启动后,就会没有网络,配置/etc/sysctl.conf,添加net.ipv4.ip_forward=1即可,操作如下:

配置转发
vim /etc/sysctl.conf

#配置转发
net.ipv4.ip_forward=1

#重启服务,让配置生效
systemctl restart network

#查看是否成功,如果返回为“net.ipv4.ip_forward = 1”则表示成功

sysctl net.ipv4.ip_forward


vim /etc/sysctl.conf

#配置转发
net.ipv4.ip_forward=1

#重启服务,让配置生效
systemctl restart network

#查看是否成功,如果返回为“net.ipv4.ip_forward = 1”则表示成功

sysctl net.ipv4.ip_forward

检查容器是否正常访问网络
#重启docker服务
service docker restart 

#查看运行过的容器
docker ps -a

#启动gitlab 容器
docker start gitblab2 

#进入gitlab容器
docker attach gitlab2

#试试获取百度信息看是否成功
curl baidu.com


#重启docker服务
service docker restart 

#查看运行过的容器
docker ps -a

#启动gitlab 容器
docker start gitblab2 

#进入gitlab容器
docker attach gitlab2

#获取百度信息
curl baidu.com
完美 

Shell教程

为什么学Shell

链接:https://www.zhihu.com/question/309875771/answer/579235911

你看,每一种语言,它都封装了一些东西——除了汇编。

这些封装,使得它的使用者可以更简洁的完成日常功能的同时,也不可避免的要付出一些底层控制能力/灵活性方面的代价——毕竟你只能通过它提供的、“封装”的代理,才能完成任务。

其中,汇编可以控制一切;但用它时,就连if你都得自己拆开,用cmp/jnz/jz等指令的组合实现类if控制结构——在程序流程控制可以无限精确、无限复杂的同时……你不觉得每次都得根据实际情况,把if这俩字母拆成三五条甚至十几条语句,太过繁琐了吗?

而C/C++呢,你近似可以控制一切,汇编能做到的,你99.99%都能做到。这就大大提高了开发效率。

等到了java,你失去了对内存的大部分控制权;但当你要写一个商业/工业程序时,却前所未有的简单。

脚本语言,如python、php等,也都类似——它们比java更简单,但也更慢。

再往上,就是shell脚本了。

shell脚本是一种比python更“糙”、因此性能等方面更为受限的语言;但它有个极大的优点,那就是可以把其他人写的程序本身,像其他语言的库函数那样使用。

为了做到这个,它要求每个程序都把命令行参数当作“函数输入”、向stdout/stderr的输出当作函数输出,同时以程序返回值说明执行成功与否。

这个约定是强制性的,在操作系统设计时就确定了的——这也是C语言的main函数需要返回一个int的原因。

但是,这个“命令行参数”的约定也束缚了shell,使得它比起其他语言更在乎空格/回车等符号——因为它的每一行,都必须是“命令 参数1 参数2”的格式。

这个约定就使得它语法怪异,也很难像其他编程语言那样,支持语法结构的任意嵌套——不管有多像,它毕竟是“用命令行模拟出来的语法结构”。

那么,这就是shell脚本的定位:它是一种只能支持较为简单逻辑的、可以直接把任意现有程序当作函数无缝集成的“超高级语言”;但因为“命令行模拟”这个本质,它的语法较为笨拙,很难像正规的脚本语言那样得到很多很多的语法糖或者其它便利。

但,同样的,其它脚本语言语法结构是灵活了;但它们也就不可能很方便的和系统安装的现有程序交互了——注意是不方便,不是不能。你完全可以通过popen/shellexec等方法调用外部程序。

程序员都是一群特别特别“懒”的人。他们会根据目标的不同,选择能够最轻易达到目标的方法——这种选择同样包括编程语言。这反过来也导致了诸多不同定位的语言的出现。

什么是Shell

Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。

Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。

Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 Shell。

Shell 脚本

Shell 脚本(shell script),是一种为 shell 编写的脚本程序。

业界所说的 shell 通常都是指 shell 脚本,但读者朋友要知道,shell 和 shell script 是两个不同的概念。

由于习惯的原因,简洁起见,本文出现的 “shell编程” 都是指 shell 脚本编程,不是指开发 shell 自身。

shell中文乱码

修改文件类型为:utf-8

Shell 环境

Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。

Linux 的 Shell 种类众多,常见的有:

  • Bourne Shell(/usr/bin/sh或/bin/sh)
  • Bourne Again Shell(/bin/bash)
  • C Shell(/usr/bin/csh)
  • K Shell(/usr/bin/ksh)
  • Shell for Root(/sbin/sh)
  • ……

本教程关注的是 Bash,也就是 Bourne Again Shell,由于易用和免费,Bash 在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的 Shell。

在一般情况下,人们并不区分 Bourne Shell 和 Bourne Again Shell,所以,像 #!/bin/sh,它同样也可以改为 #!/bin/bash

#! 告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序。

第一个shell脚本

打开文本编辑器(可以使用 vi/vim 命令来创建文件),新建一个文件 test.sh,扩展名为 sh(sh代表shell),扩展名并不影响脚本执行,见名知意就好,如果你用 php 写 shell 脚本,扩展名就用 php 好了。

输入一些代码,第一行一般是这样:

实例

#!/bin/bash
echo "Hello World !"

#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。

echo 命令用于向窗口输出文本。

注意:#!和 /bin/bash中间不能有空格

运行 Shell 脚本有两种方法:

1、作为可执行程序

将上面的代码保存为 test.sh,并 cd 到相应目录:

chmod +x ./test.sh  #使脚本具有执行权限
./test.sh  #执行脚本

注意,一定要写成 ./test.sh,而不是 test.sh,运行其它二进制的程序也一样,直接写 test.sh,linux 系统会去 PATH 里寻找有没有叫 test.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 test.sh 是会找不到命令的,要用 ./test.sh 告诉系统说,就在当前目录找。

2、作为解释器参数

这种运行方式是,直接运行解释器,其参数就是 shell 脚本的文件名,如:

/bin/sh test.sh
/bin/php test.php

这种方式运行的脚本,不需要在第一行指定解释器信息,写了也没用。

Shell 变量

定义变量时,变量名不加美元符号($,PHP语言中变量需要),如:

your_name="runoob.com"

注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语言都不一样。同时,变量名的命名须遵循如下规则:

  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_)。
  • 不能使用标点符号。
  • 不能使用bash里的关键字(可用help命令查看保留关键字)。

有效的 Shell 变量名示例如下:

RUNOOB
LD_LIBRARY_PATH
_var
var2

无效的变量命名:

?var=123
user*name=runoob

除了显式地直接赋值,还可以用语句给变量赋值,如:

for file in `ls /etc`
或
for file in $(ls /etc)

以上语句将 /etc 下目录的文件名循环出来。

使用变量

使用一个定义过的变量,只要在变量名前面加美元符号即可,如:

your_name="qinjx"
echo $your_name
echo ${your_name}

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,比如下面这种情况:

for skill in Ada Coffe Action Java; do
    echo "I am good at ${skill}Script"
done

如果不给skill变量加花括号,写成echo “I am good at $skillScript”,解释器就会把$skillScript当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。

推荐给所有变量加上花括号,这是个好的编程习惯。

已定义的变量,可以被重新定义,如:

your_name="tom"
echo $your_name
your_name="alibaba"
echo $your_name

这样写是合法的,但注意,第二次赋值的时候不能写$your_name=”alibaba”,使用变量的时候才加美元符($)。

只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

下面的例子尝试更改只读变量,结果报错:

#!/bin/bash
myUrl="https://www.google.com"
readonly myUrl
myUrl="https://www.runoob.com"

运行脚本,结果如下:

/bin/sh: NAME: This variable is read only.

删除变量

使用 unset 命令可以删除变量。语法:

unset variable_name

变量被删除后不能再次使用。unset 命令不能删除只读变量。

实例

#!/bin/sh
myUrl="https://www.runoob.com"
unset myUrl
echo $myUrl

以上实例执行将没有任何输出。

变量类型

运行shell时,会同时存在三种变量:

  • 1) 局部变量 局部变量在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量。
  • 2) 环境变量 所有的程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。
  • 3) shell变量 shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell的正常运行

Shell 字符串

字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号。

单引号

str='this is a string'

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
  • 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。

双引号

your_name='runoob'
str="Hello, I know you are \"$your_name\"! \n"
echo -e $str

输出结果为:

Hello, I know you are "runoob"! 

双引号的优点:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符

拼接字符串

your_name="runoob"
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting  $greeting_1
# 使用单引号拼接
greeting_2='hello, '$your_name' !'
greeting_3='hello, ${your_name} !'
echo $greeting_2  $greeting_3

输出结果为:

hello, runoob ! hello, runoob !
hello, runoob ! hello, ${your_name} !

获取字符串长度

string="abcd"
echo ${#string} #输出 4
```

#### 提取子字符串

以下实例从字符串第 **2** 个字符开始截取 **4** 个字符:

```
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo
```

**注意**:第一个字符的索引值为 **0**。

#### 查找子字符串

查找字符 **i** 或 **o** 的位置(哪个字母先出现就计算哪个):

```
string="runoob is a great site"
echo `expr index "$string" io`  # 输出 4
```

**注意:** 以上脚本中 **`** 是反引号,而不是单引号 **'**,不要看错了哦。

没有找到返回:0

### Shell 数组

bash支持一维数组(不支持多维数组),并且没有限定数组的大小。

类似于 C 语言,数组元素的下标由 0 开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于 0。

#### 定义数组

在 Shell 中,用括号来表示数组,数组元素用"空格"符号分割开。定义数组的一般形式为:

```
数组名=(值1 值2 ... 值n)
```

例如:

```
array_name=(value0 value1 value2 value3)
```

或者

```
array_name=(
value0
value1
value2
value3
)
```

还可以单独定义数组的各个分量:

```
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen
```

可以不使用连续的下标,而且下标的范围没有限制。

#### 读取数组

读取数组元素值的一般格式是:

```
${数组名[下标]}
```

例如:

```
valuen=${array_name[n]}
```

使用 **@** 符号可以获取数组中的所有元素,例如:

```
echo ${array_name[@]}
```

#### 获取数组的长度

获取数组长度的方法与获取字符串长度的方法相同,例如:

```
# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}
```

### Shell 注释

以 **#** 开头的行就是注释,会被解释器忽略。

通过每一行加一个 **#** 号设置多行注释,像这样:

```
#--------------------------------------------
# 这是一个注释
# author:菜鸟教程
# site:www.runoob.com
# slogan:学的不仅是技术,更是梦想!
#--------------------------------------------
##### 用户配置区 开始 #####
#
#
# 这里可以添加脚本描述信息
# 
#
##### 用户配置区 结束  #####
```

如果在开发过程中,遇到大段的代码需要临时注释起来,过一会儿又取消注释,怎么办呢?

每一行加个#符号太费力了,可以把这一段要注释的代码用一对花括号括起来,定义成一个函数,没有地方调用这个函数,这块代码就不会执行,达到了和注释一样的效果。

#### 多行注释

多行注释还可以使用以下格式:

```
:< **注意:**
>
> - 乘号(*)前边必须加反斜杠(\)才能实现乘法运算;
> - if...then...fi 是条件语句,后续将会讲解。
> - 在 MAC 中 shell 的 expr 语法是:**$((表达式))**,此处表达式中的 "*" 不需要转义符号 "\" 。

------

#### 关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:

| 运算符 | 说明                                                  | 举例                       |
| :----- | :---------------------------------------------------- | :------------------------- |
| -eq    | 检测两个数是否相等,相等返回 true。                   | [ $a -eq $b ] 返回 false。 |
| -ne    | 检测两个数是否不相等,不相等返回 true。               | [ $a -ne $b ] 返回 true。  |
| -gt    | 检测左边的数是否大于右边的,如果是,则返回 true。     | [ $a -gt $b ] 返回 false。 |
| -lt    | 检测左边的数是否小于右边的,如果是,则返回 true。     | [ $a -lt $b ] 返回 true。  |
| -ge    | 检测左边的数是否大于等于右边的,如果是,则返回 true。 | [ $a -ge $b ] 返回 false。 |
| -le    | 检测左边的数是否小于等于右边的,如果是,则返回 true。 | [ $a -le $b ] 返回 true。  |

#### 实例

关系运算符实例如下:

#### 实例

*#!/bin/bash*
*# author:菜鸟教程*
*# url:www.runoob.com*

a=10
b=20

**if** **[** $a -eq $b **]**
**then**
  **echo** "$a -eq $b : a 等于 b"
**else**
  **echo** "$a -eq $b: a 不等于 b"
**fi**
**if** **[** $a -ne $b **]**
**then**
  **echo** "$a -ne $b: a 不等于 b"
**else**
  **echo** "$a -ne $b : a 等于 b"
**fi**
**if** **[** $a -gt $b **]**
**then**
  **echo** "$a -gt $b: a 大于 b"
**else**
  **echo** "$a -gt $b: a 不大于 b"
**fi**
**if** **[** $a -lt $b **]**
**then**
  **echo** "$a -lt $b: a 小于 b"
**else**
  **echo** "$a -lt $b: a 不小于 b"
**fi**
**if** **[** $a -ge $b **]**
**then**
  **echo** "$a -ge $b: a 大于或等于 b"
**else**
  **echo** "$a -ge $b: a 小于 b"
**fi**
**if** **[** $a -le $b **]**
**then**
  **echo** "$a -le $b: a 小于或等于 b"
**else**
  **echo** "$a -le $b: a 大于 b"
**fi**

执行脚本,输出结果如下所示:

```
10 -eq 20: a 不等于 b
10 -ne 20: a 不等于 b
10 -gt 20: a 不大于 b
10 -lt 20: a 小于 b
10 -ge 20: a 小于 b
10 -le 20: a 小于或等于 b
```

------

#### 布尔运算符

下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:

| 运算符 | 说明                                                | 举例                                     |
| :----- | :-------------------------------------------------- | :--------------------------------------- |
| !      | 非运算,表达式为 true 则返回 false,否则返回 true。 | [ ! false ] 返回 true。                  |
| -o     | 或运算,有一个表达式为 true 则返回 true。           | [ $a -lt 20 -o $b -gt 100 ] 返回 true。  |
| -a     | 与运算,两个表达式都为 true 才返回 true。           | [ $a -lt 20 -a $b -gt 100 ] 返回 false。 |

#### 实例

布尔运算符实例如下:

#### 实例

*#!/bin/bash*
*# author:菜鸟教程*
*# url:www.runoob.com*

a=10
b=20

**if** **[** $a **!**= $b **]**
**then**
  **echo** "$a != $b : a 不等于 b"
**else**
  **echo** "$a == $b: a 等于 b"
**fi**
**if** **[** $a -lt 100 -a $b -gt 15 **]**
**then**
  **echo** "$a 小于 100 且 $b 大于 15 : 返回 true"
**else**
  **echo** "$a 小于 100 且 $b 大于 15 : 返回 false"
**fi**
**if** **[** $a -lt 100 -o $b -gt 100 **]**
**then**
  **echo** "$a 小于 100 或 $b 大于 100 : 返回 true"
**else**
  **echo** "$a 小于 100 或 $b 大于 100 : 返回 false"
**fi**
**if** **[** $a -lt 5 -o $b -gt 100 **]**
**then**
  **echo** "$a 小于 5 或 $b 大于 100 : 返回 true"
**else**
  **echo** "$a 小于 5 或 $b 大于 100 : 返回 false"
**fi**

执行脚本,输出结果如下所示:

```
10 != 20 : a 不等于 b
10 小于 100 且 20 大于 15 : 返回 true
10 小于 100 或 20 大于 100 : 返回 true
10 小于 5 或 20 大于 100 : 返回 false
```

------

#### 逻辑运算符

以下介绍 Shell 的逻辑运算符,假定变量 a 为 10,变量 b 为 20:

| 运算符 | 说明       | 举例                                       |
| :----- | :--------- | :----------------------------------------- |
| &&     | 逻辑的 AND | [[ $a -lt 100 && $b -gt 100 ]] 返回 false  |
| \|\|   | 逻辑的 OR  | [[ $a -lt 100 \|\| $b -gt 100 ]] 返回 true |

#### 实例

逻辑运算符实例如下:

#### 实例

*#!/bin/bash*
*# author:菜鸟教程*
*# url:www.runoob.com*

a=10
b=20

**if** **[****[** $a -lt 100 **&&** $b -gt 100 **]****]**
**then**
  **echo** "返回 true"
**else**
  **echo** "返回 false"
**fi**

**if** **[****[** $a -lt 100 **||** $b -gt 100 **]****]**
**then**
  **echo** "返回 true"
**else**
  **echo** "返回 false"
**fi**

执行脚本,输出结果如下所示:

```
返回 false
返回 true
```

------

#### 字符串运算符

下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":

| 运算符 | 说明                                         | 举例                     |
| :----- | :------------------------------------------- | :----------------------- |
| =      | 检测两个字符串是否相等,相等返回 true。      | [ $a = $b ] 返回 false。 |
| !=     | 检测两个字符串是否不相等,不相等返回 true。  | [ $a != $b ] 返回 true。 |
| -z     | 检测字符串长度是否为0,为0返回 true。        | [ -z $a ] 返回 false。   |
| -n     | 检测字符串长度是否不为 0,不为 0 返回 true。 | [ -n "$a" ] 返回 true。  |
| $      | 检测字符串是否为空,不为空返回 true。        | [ $a ] 返回 true。       |

#### 实例

字符串运算符实例如下:

#### 实例


*#!/bin/bash*
*# author:菜鸟教程*
*# url:www.runoob.com*

a="abc"
b="efg"

**if** **[** $a = $b **]**
**then**
  **echo** "$a = $b : a 等于 b"
**else**
  **echo** "$a = $b: a 不等于 b"
**fi**
**if** **[** $a **!**= $b **]**
**then**
  **echo** "$a != $b : a 不等于 b"
**else**
  **echo** "$a != $b: a 等于 b"
**fi**
**if** **[** -z $a **]**
**then**
  **echo** "-z $a : 字符串长度为 0"
**else**
  **echo** "-z $a : 字符串长度不为 0"
**fi**
**if** **[** -n "$a" **]**
**then**
  **echo** "-n $a : 字符串长度不为 0"
**else**
  **echo** "-n $a : 字符串长度为 0"
**fi**
**if** **[** $a **]**
**then**
  **echo** "$a : 字符串不为空"
**else**
  **echo** "$a : 字符串为空"
**fi**

执行脚本,输出结果如下所示:

```
abc = efg: a 不等于 b
abc != efg : a 不等于 b
-z abc : 字符串长度不为 0
-n abc : 字符串长度不为 0
abc : 字符串不为空
```

------

#### 文件测试运算符

文件测试运算符用于检测 Unix 文件的各种属性。

属性检测描述如下:

| 操作符  | 说明                                                         | 举例                      |
| :------ | :----------------------------------------------------------- | :------------------------ |
| -b file | 检测文件是否是块设备文件,如果是,则返回 true。              | [ -b $file ] 返回 false。 |
| -c file | 检测文件是否是字符设备文件,如果是,则返回 true。            | [ -c $file ] 返回 false。 |
| -d file | 检测文件是否是目录,如果是,则返回 true。                    | [ -d $file ] 返回 false。 |
| -f file | 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 | [ -f $file ] 返回 true。  |
| -g file | 检测文件是否设置了 SGID 位,如果是,则返回 true。            | [ -g $file ] 返回 false。 |
| -k file | 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。  | [ -k $file ] 返回 false。 |
| -p file | 检测文件是否是有名管道,如果是,则返回 true。                | [ -p $file ] 返回 false。 |
| -u file | 检测文件是否设置了 SUID 位,如果是,则返回 true。            | [ -u $file ] 返回 false。 |
| -r file | 检测文件是否可读,如果是,则返回 true。                      | [ -r $file ] 返回 true。  |
| -w file | 检测文件是否可写,如果是,则返回 true。                      | [ -w $file ] 返回 true。  |
| -x file | 检测文件是否可执行,如果是,则返回 true。                    | [ -x $file ] 返回 true。  |
| -s file | 检测文件是否为空(文件大小是否大于0),不为空返回 true。     | [ -s $file ] 返回 true。  |
| -e file | 检测文件(包括目录)是否存在,如果是,则返回 true。          | [ -e $file ] 返回 true。  |

其他检查符:

- **-S**: 判断某文件是否 socket。
- **-L**: 检测文件是否存在并且是一个符号链接。



#### 实例

变量 file 表示文件 **/var/www/runoob/test.sh**,它的大小为 100 字节,具有 **rwx** 权限。下面的代码,将检测该文件的各种属性:



#### 实例

*#!/bin/bash*
*# author:菜鸟教程*
*# url:www.runoob.com*

file="/var/www/runoob/test.sh"
**if** **[** -r $file **]**
**then**
  **echo** "文件可读"
**else**
  **echo** "文件不可读"
**fi**
**if** **[** -w $file **]**
**then**
  **echo** "文件可写"
**else**
  **echo** "文件不可写"
**fi**
**if** **[** -x $file **]**
**then**
  **echo** "文件可执行"
**else**
  **echo** "文件不可执行"
**fi**
**if** **[** -f $file **]**
**then**
  **echo** "文件为普通文件"
**else**
  **echo** "文件为特殊文件"
**fi**
**if** **[** -d $file **]**
**then**
  **echo** "文件是个目录"
**else**
  **echo** "文件不是个目录"
**fi**
**if** **[** -s $file **]**
**then**
  **echo** "文件不为空"
**else**
  **echo** "文件为空"
**fi**
**if** **[** -e $file **]**
**then**
  **echo** "文件存在"
**else**
  **echo** "文件不存在"
**fi**

执行脚本,输出结果如下所示:

```
文件可读
文件可写
文件可执行
文件为普通文件
文件不是个目录
文件不为空
文件存在
```

### Shell echo命令

Shell 的 echo 指令与 PHP 的 echo 指令类似,都是用于字符串的输出。命令格式:

```
echo string
```

您可以使用echo实现更复杂的输出格式控制。

#### 1.显示普通字符串:

```
echo "It is a test"
```

这里的双引号完全可以省略,以下命令与上面实例效果一致:

```
echo It is a test
```

#### 2.显示转义字符

```
echo "\"It is a test\""
```

结果将是:

```
"It is a test"
```

同样,双引号也可以省略

#### 3.显示变量

read 命令从标准输入中读取一行,并把输入行的每个字段的值指定给 shell 变量

```
#!/bin/sh
read name 
echo "$name It is a test"
```

以上代码保存为 test.sh,name 接收标准输入的变量,结果将是:

```
[root@www ~]# sh test.sh
OK                     #标准输入
OK It is a test        #输出
```

#### 4.显示换行

```
echo -e "OK! \n" # -e 开启转义
echo "It is a test"
```

输出结果:

```
OK!

It is a test
```

#### 5.显示不换行

```
#!/bin/sh
echo -e "OK! \c" # -e 开启转义 \c 不换行
echo "It is a test"
```

输出结果:

```
OK! It is a test
```

#### 6.显示结果定向至文件

```
echo "It is a test" > myfile
```

#### 7.原样输出字符串,不进行转义或取变量(用单引号)

```
echo '$name\"'
```

输出结果:

```
$name\"
```

#### 8.显示命令执行结果

```
echo `date`
```

**注意:** 这里使用的是反引号 **`**, 而不是单引号 **'**。

结果将显示当前日期

```
Thu Jul 24 10:08:46 CST 2014
```

### Shell printf 命令

上一章节我们学习了 Shell 的 echo 命令,本章节我们来学习 Shell 的另一个输出命令 printf。

printf 命令模仿 C 程序库(library)里的 printf() 程序。

printf 由 POSIX 标准所定义,因此使用 printf 的脚本比使用 echo 移植性好。

printf 使用引用文本或空格分隔的参数,外面可以在 printf 中使用格式化字符串,还可以制定字符串的宽度、左右对齐方式等。默认 printf 不会像 echo 自动添加换行符,我们可以手动添加 \n。

printf 命令的语法:

```
printf  format-string  [arguments...]
```

**参数说明:**

- **format-string:** 为格式控制字符串
- **arguments:** 为参数列表。

#### 实例

$ **echo** "Hello, Shell"
Hello, Shell
$ **printf** "Hello, Shell**\n**"
Hello, Shell
$

接下来,我来用一个脚本来体现 printf 的强大功能:

#### 实例

*#!/bin/bash*
*# author:菜鸟教程*
*# url:www.runoob.com*

**printf** "%-10s %-8s %-4s**\n**" 姓名 性别 体重kg  
**printf** "%-10s %-8s %-4.2f**\n**" 郭靖 男 66.1234
**printf** "%-10s %-8s %-4.2f**\n**" 杨过 男 48.6543
**printf** "%-10s %-8s %-4.2f**\n**" 郭芙 女 47.9876

执行脚本,输出结果如下所示:

```
姓名     性别   体重kg
郭靖     男      66.12
杨过     男      48.65
郭芙     女      47.99
```

**%s %c %d %f** 都是格式替代符,**%s** 输出一个字符串,**%d** 整型输出,**%c** 输出一个字符,**%f** 输出实数,以小数形式输出。

**%-10s** 指一个宽度为 10 个字符(**-** 表示左对齐,没有则表示右对齐),任何字符都会被显示在 10 个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。

**%-4.2f** 指格式化为小数,其中 **.2** 指保留2位小数。



#### 实例

*#!/bin/bash*
*# author:菜鸟教程*
*# url:www.runoob.com*

*# format-string为双引号*
**printf** "%d %s**\n**" 1 "abc"

*# 单引号与双引号效果一样*
**printf** '%d %s\n' 1 "abc"

*# 没有引号也可以输出*
**printf** **%**s abcdef

*# 格式只指定了一个参数,但多出的参数仍然会按照该格式输出,format-string 被重用*
**printf** **%**s abc def

**printf** "%s**\n**" abc def

**printf** "%s %s %s**\n**" a b c d e f g h i j

*# 如果没有 arguments,那么 %s 用NULL代替,%d 用 0 代替*
**printf** "%s and %d **\n**"

执行脚本,输出结果如下所示:

```
1 abc
1 abc
abcdefabcdefabc
def
a b c
d e f
g h i
j  
 and 0
```

------

#### printf 的转义序列

| 序列  | 说明                                                         |
| :---- | :----------------------------------------------------------- |
| \a    | 警告字符,通常为ASCII的BEL字符                               |
| \b    | 后退                                                         |
| \c    | 抑制(不显示)输出结果中任何结尾的换行字符(只在%b格式指示符控制下的参数字符串中有效),而且,任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符,都被忽略 |
| \f    | 换页(formfeed)                                             |
| \n    | 换行                                                         |
| \r    | 回车(Carriage return)                                      |
| \t    | 水平制表符                                                   |
| \v    | 垂直制表符                                                   |
| \\    | 一个字面上的反斜杠字符                                       |
| \ddd  | 表示1到3位数八进制值的字符。仅在格式字符串中有效             |
| \0ddd | 表示1到3位的八进制值字符                                     |

#### 实例

$ **printf** "a string, no processing:<%s>**\n**" "A**\n**B"
a string, no processing:**<**A\nB**>**

$ **printf** "a string, no processing:<%b>**\n**" "A**\n**B"
a string, no processing:**<**A b**>**

$ **printf** "www.runoob.com \a"
www.runoob.com $          *#不换行*

### Shell test 命令

Shell中的 test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。

------

#### 数值测试

| 参数 | 说明           |
| :--- | :------------- |
| -eq  | 等于则为真     |
| -ne  | 不等于则为真   |
| -gt  | 大于则为真     |
| -ge  | 大于等于则为真 |
| -lt  | 小于则为真     |
| -le  | 小于等于则为真 |

#### 实例

num1=100
num2=100
**if** **test** $**[**num1**]** -eq $**[**num2**]**
**then**
  **echo** '两个数相等!'
**else**
  **echo** '两个数不相等!'
**fi**

输出结果:

```
两个数相等!
```

代码中的 **[]** 执行基本的算数运算,如:

#### 实例

*#!/bin/bash*

a=5
b=6

result=$**[**a+b**]** *# 注意等号两边不能有空格*
**echo** "result 为: $result"

结果为:

```
result 为: 11
```

------

#### 字符串测试

| 参数      | 说明                     |
| :-------- | :----------------------- |
| =         | 等于则为真               |
| !=        | 不相等则为真             |
| -z 字符串 | 字符串的长度为零则为真   |
| -n 字符串 | 字符串的长度不为零则为真 |

#### 实例

num1="ru1noob"
num2="runoob"
**if** **test** $num1 = $num2
**then**
  **echo** '两个字符串相等!'
**else**
  **echo** '两个字符串不相等!'
**fi**

输出结果:

```
两个字符串不相等!
```

------

#### 文件测试

| 参数      | 说明                                 |
| :-------- | :----------------------------------- |
| -e 文件名 | 如果文件存在则为真                   |
| -r 文件名 | 如果文件存在且可读则为真             |
| -w 文件名 | 如果文件存在且可写则为真             |
| -x 文件名 | 如果文件存在且可执行则为真           |
| -s 文件名 | 如果文件存在且至少有一个字符则为真   |
| -d 文件名 | 如果文件存在且为目录则为真           |
| -f 文件名 | 如果文件存在且为普通文件则为真       |
| -c 文件名 | 如果文件存在且为字符型特殊文件则为真 |
| -b 文件名 | 如果文件存在且为块特殊文件则为真     |

#### 实例

**cd** **/**bin
**if** **test** -e .**/****bash**
**then**
  **echo** '文件已存在!'
**else**
  **echo** '文件不存在!'
**fi**

输出结果:

```
文件已存在!
```

另外,Shell 还提供了与( -a )、或( -o )、非( ! )三个逻辑操作符用于将测试条件连接起来,其优先级为: **!** 最高, **-a** 次之, **-o** 最低。例如:

#### 实例

**cd** **/**bin
**if** **test** -e .**/**notFile -o -e .**/****bash**
**then**
  **echo** '至少有一个文件存在!'
**else**
  **echo** '两个文件都不存在'
**fi**

输出结果:

```
至少有一个文件存在!
```

### Shell 流程控制

和 Java、PHP 等语言不一样,sh 的流程控制不可为空,如(以下为 PHP 流程控制写法):

#### 实例

结束循环。

#### 实例

**echo** '按下  退出'
**echo** -n '输入你最喜欢的网站名: '
**while** **read** FILM
**do**
  **echo** "是的!$FILM 是一个好网站"
**done**

运行脚本,输出类似下面:

```
按下  退出
输入你最喜欢的网站名:菜鸟教程
是的!菜鸟教程 是一个好网站
```

#### 无限循环

无限循环语法格式:

```
while :
do
    command
done
```

或者

```
while true
do
    command
done
```

或者

```
for (( ; ; ))
```



------

#### until 循环

until 循环执行一系列命令直至条件为 true 时停止。

until 循环与 while 循环在处理方式上刚好相反。

一般 while 循环优于 until 循环,但在某些时候—也只是极少数情况下,until 循环更加有用。

until 语法格式:

```
until condition
do
    command
done
```

condition 一般为条件表达式,如果返回值为 false,则继续执行循环体内的语句,否则跳出循环。

以下实例我们使用 until 命令来输出 0 ~ 9 的数字:

#### 实例

*#!/bin/bash*

a=0

**until** **[** **!** $a -lt 10 **]**
**do**
  **echo** $a
  a=**`****expr** $a + 1**`**
**done**

运行结果:

输出结果为:

```
0
1
2
3
4
5
6
7
8
9
```

------

#### case ... esac

**case ... esac** 为多选择语句,与其他语言中的 switch ... case 语句类似,是一种多分枝选择结构,每个 case 分支用右圆括号开始,用两个分号 **;;** 表示 break,即执行结束,跳出整个 case ... esac 语句,esac(就是 case 反过来)作为结束标记。

可以用 case 语句匹配一个值与一个模式,如果匹配成功,执行相匹配的命令。

**case ... esac** 语法格式如下:

```
case 值 in
模式1)
    command1
    command2
    ...
    commandN
    ;;
模式2)
    command1
    command2
    ...
    commandN
    ;;
esac
```

case 工作方式如上所示,取值后面必须为单词 **in**,每一模式必须以右括号结束。取值可以为变量或常数,匹配发现取值符合某一模式后,其间所有命令开始执行直至 **;;**。

取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。

下面的脚本提示输入 1 到 4,与每一种模式进行匹配:

#### 实例

**echo** '输入 1 到 4 之间的数字:'
**echo** '你输入的数字为:'
**read** aNum
**case** $aNum **in**
  1**)** **echo** '你选择了 1'
  **;;**
  2**)** **echo** '你选择了 2'
  **;;**
  3**)** **echo** '你选择了 3'
  **;;**
  4**)** **echo** '你选择了 4'
  **;;**
  *******)** **echo** '你没有输入 1 到 4 之间的数字'
  **;;**
**esac**

输入不同的内容,会有不同的结果,例如:

```
输入 1 到 4 之间的数字:
你输入的数字为:
3
你选择了 3
```

下面的脚本匹配字符串:

#### 实例

*#!/bin/sh*

site="runoob"

**case** "$site" **in**
  "runoob"**)** **echo** "菜鸟教程"
  **;;**
  "google"**)** **echo** "Google 搜索"
  **;;**
  "taobao"**)** **echo** "淘宝网"
  **;;**
**esac**

输出结果为:

```
菜鸟教程
```

------

#### 跳出循环

在循环过程中,有时候需要在未达到循环结束条件时强制跳出循环,Shell使用两个命令来实现该功能:break和continue。

#### break命令

break命令允许跳出所有循环(终止执行后面的所有循环)。

下面的例子中,脚本进入死循环直至用户输入数字大于5。要跳出这个循环,返回到shell提示符下,需要使用break命令。

##### 实例

*#!/bin/bash*
**while** :
**do**
  **echo** -n "输入 1 到 5 之间的数字:"
  **read** aNum
  **case** $aNum **in**
    1**|**2**|**3**|**4**|**5**)** **echo** "你输入的数字为 $aNum!"
    **;;**
    *******)** **echo** "你输入的数字不是 1 到 5 之间的! 游戏结束"
      **break**
    **;;**
  **esac**
**done**

执行以上代码,输出结果为:

```
输入 1 到 5 之间的数字:3
你输入的数字为 3!
输入 1 到 5 之间的数字:7
你输入的数字不是 1 到 5 之间的! 游戏结束
```

#### continue

continue命令与break命令类似,只有一点差别,它不会跳出所有循环,仅仅跳出当前循环。

对上面的例子进行修改:

#### 实例

*#!/bin/bash*
**while** :
**do**
  **echo** -n "输入 1 到 5 之间的数字: "
  **read** aNum
  **case** $aNum **in**
    1**|**2**|**3**|**4**|**5**)** **echo** "你输入的数字为 $aNum!"
    **;;**
    *******)** **echo** "你输入的数字不是 1 到 5 之间的!"
      **continue**
      **echo** "游戏结束"
    **;;**
  **esac**
**done**

运行代码发现,当输入大于5的数字时,该例中的循环不会结束,语句 **echo "游戏结束"** 永远不会被执行。

### Shell 函数

linux shell 可以用户定义函数,然后在shell脚本中可以随便调用。

shell中函数的定义格式如下:

```
[ function ] funname [()]

{

    action;

    [return int;]

}
```

说明:

- 1、可以带function fun() 定义,也可以直接fun() 定义,不带任何参数。
- 2、参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值。 return后跟数值n(0-255

下面的例子定义了一个函数并进行调用:

```
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

demoFun(){
    echo "这是我的第一个 shell 函数!"
}
echo "-----函数开始执行-----"
demoFun
echo "-----函数执行完毕-----"
```

输出结果:

```
-----函数开始执行-----
这是我的第一个 shell 函数!
-----函数执行完毕-----
```

下面定义一个带有return语句的函数:

```
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

funWithReturn(){
    echo "这个函数会对输入的两个数字进行相加运算..."
    echo "输入第一个数字: "
    read aNum
    echo "输入第二个数字: "
    read anotherNum
    echo "两个数字分别为 $aNum 和 $anotherNum !"
    return $(($aNum+$anotherNum))
}
funWithReturn
echo "输入的两个数字之和为 $? !"
```

输出类似下面:

```
这个函数会对输入的两个数字进行相加运算...
输入第一个数字: 
1
输入第二个数字: 
2
两个数字分别为 1 和 2 !
输入的两个数字之和为 3 !
```

函数返回值在调用该函数后通过 $? 来获得。

注意:所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。

------

#### 函数参数

在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数...

带参数的函数示例:

```
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

funWithParam(){
    echo "第一个参数为 $1 !"
    echo "第二个参数为 $2 !"
    echo "第十个参数为 $10 !"
    echo "第十个参数为 ${10} !"
    echo "第十一个参数为 ${11} !"
    echo "参数总数有 $# 个!"
    echo "作为一个字符串输出所有参数 $* !"
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73
```

输出结果:

```
第一个参数为 1 !
第二个参数为 2 !
第十个参数为 10 !
第十个参数为 34 !
第十一个参数为 73 !
参数总数有 11 个!
作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 34 73 !
```

注意,$10 不能获取第十个参数,获取第十个参数需要${10}。当n>=10时,需要使用${n}来获取参数。

另外,还有几个特殊字符用来处理参数:

| 参数处理 | 说明                                                         |
| :------- | :----------------------------------------------------------- |
| $#       | 传递到脚本或函数的参数个数                                   |
| $*       | 以一个单字符串显示所有向脚本传递的参数                       |
| $$       | 脚本运行的当前进程ID号                                       |
| $!       | 后台运行的最后一个进程的ID号                                 |
| $@       | 与$*相同,但是使用时加引号,并在引号中返回每个参数。         |
| $-       | 显示Shell使用的当前选项,与set命令功能相同。                 |
| $?       | 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。 |

### Shell 输入/输出重定向

大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。

重定向命令列表如下:

| 命令            | 说明                                               |
| :-------------- | :------------------------------------------------- |
| command > file  | 将输出重定向到 file。                              |
| command < file  | 将输入重定向到 file。                              |
| command >> file | 将输出以追加的方式重定向到 file。                  |
| n > file        | 将文件描述符为 n 的文件重定向到 file。             |
| n >> file       | 将文件描述符为 n 的文件以追加的方式重定向到 file。 |
| n >& m          | 将输出文件 m 和 n 合并。                           |
| n <& m | 将输入文件 和 n 合并。 << tag 将开始标记 和结束标记 之间的内容作为输入。> 需要注意的是文件描述符 0 通常是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。

------

#### 输出重定向

重定向一般通过在命令间插入特定的符号来实现。特别的,这些符号的语法如下所示:

```
command1 > file1
```

上面这个命令执行command1然后将输出的内容存入file1。

注意任何file1内的已经存在的内容将被新内容替代。如果要将新内容添加在文件末尾,请使用>>操作符。

#### 实例

执行下面的 who 命令,它将命令的完整的输出重定向在用户文件中(users):

```
$ who > users
```

执行后,并没有在终端输出信息,这是因为输出已被从默认的标准输出设备(终端)重定向到指定的文件。

你可以使用 cat 命令查看文件内容:

```
$ cat users
_mbsetupuser console  Oct 31 17:35 
tianqixin    console  Oct 31 17:35 
tianqixin    ttys000  Dec  1 11:33 
```

输出重定向会覆盖文件内容,请看下面的例子:

```
$ echo "菜鸟教程:www.runoob.com" > users
$ cat users
菜鸟教程:www.runoob.com
$
```

如果不希望文件内容被覆盖,可以使用 >> 追加到文件末尾,例如:

```
$ echo "菜鸟教程:www.runoob.com" >> users
$ cat users
菜鸟教程:www.runoob.com
菜鸟教程:www.runoob.com
$
```

------

#### 输入重定向

和输出重定向一样,Unix 命令也可以从文件获取输入,语法为:

```
command1 < file1
```

这样,本来需要从键盘获取输入的命令会转移到文件读取内容。

注意:输出重定向是大于号(>),输入重定向是小于号(<)。 2 #### 实例 接着以上实例,我们需要统计 users 文件的行数,执行以下命令: ``` $ wc -l 也可以将输入重定向到 文件: < 注意:上面两个例子的结果不同:第一个例子,会输出文件名;第二个不会,因为它仅仅知道从标准输入读取内容。 command1 infile> outfile
```

同时替换输入和输出,执行command1,从文件infile读取内容,然后将输出写入到outfile中。

#### 重定向深入讲解

一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:

- 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
- 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
- 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。

默认情况下,command > file 将 stdout 重定向到 file,command < file 将stdin 重定向到 file。

如果希望 stderr 重定向到 file,可以这样写:

```
$ command 2>file
```

如果希望 stderr 追加到 file 文件末尾,可以这样写:

```
$ command 2>>file
```

**2** 表示标准错误文件(stderr)。

如果希望将 stdout 和 stderr 合并后重定向到 file,可以这样写:

```
$ command > file 2>&1

或者

$ command >> file 2>&1
```

如果希望对 stdin 和 stdout 都重定向,可以这样写:

```
$ command < file1 >file2
```

command 命令将 stdin 重定向到 file1,将 stdout 重定向到 file2。

------

#### Here Document

Here Document 是 Shell 中的一种特殊的重定向方式,用来将输入重定向到一个交互式 Shell 脚本或程序。

它的基本的形式如下:

```
command << delimiter
    document
delimiter
```

它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command。

> 注意:
>
> - 结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。
> - 开始的delimiter前后的空格会被忽略掉。

#### 实例

在命令行中通过 **wc -l** 命令计算 Here Document 的行数:

```
$ wc -l << EOF
    欢迎来到
    菜鸟教程
    www.runoob.com
EOF
3          # 输出结果为 3 行
$
```

我们也可以将 Here Document 用在脚本中,例如:

```
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

cat << EOF
欢迎来到
菜鸟教程
www.runoob.com
EOF
```

执行以上脚本,输出结果:

```
欢迎来到
菜鸟教程
www.runoob.com
```

------

#### /dev/null 文件

如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:

```
$ command > /dev/null
```

/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。

如果希望屏蔽 stdout 和 stderr,可以这样写:

```
$ command > /dev/null 2>&1
```

> **注意:**0 是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。
>
> 这里的 **2** 和 **>** 之间不可以有空格,**2>** 是一体的时候才表示错误输出。

### Shell 文件包含

和其他语言一样,Shell 也可以包含外部脚本。这样可以很方便的封装一些公用的代码作为一个独立的文件。

Shell 文件包含的语法格式如下:

```
. filename   # 注意点号(.)和文件名中间有一空格

或

source filename
```

#### 实例

创建两个 shell 脚本文件。

test1.sh 代码如下:

```
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

url="http://www.runoob.com"
```

test2.sh 代码如下:

```
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

#使用 . 号来引用test1.sh 文件
. ./test1.sh

# 或者使用以下包含文件代码
# source ./test1.sh

echo "菜鸟教程官网地址:$url"
```

接下来,我们为 test2.sh 添加可执行权限并执行:

```
$ chmod +x test2.sh 
$ ./test2.sh 
菜鸟教程官网地址:http://www.runoob.com
```

> **注:**被包含的文件 test1.sh 不需要可执行权限。

### 在Idea中编辑shell脚本

{% asset_img image-20211224111619055.png image-20211224111619055 %}

{% asset_img image-20211224111715173.png image-20211224111715173 %}

{% asset_img image-20211224111744060.png image-20211224111744060 %}

{% asset_img image-20211224111916874.png image-20211224111916874 %}

{% asset_img image-20211224112414039.png image-20211224112414039 %}

{% asset_img image-20211224112123123.png image-20211224112123123 %}

{% asset_img image-20211224112159463.png image-20211224112159463 %}

{% asset_img image-20211224112526807.png image-20211224112526807 %}

{% asset_img image-20211224112648343.png image-20211224112648343 %}



在虚拟机上运行该文件

### 综合案例

1. 求1到10之间偶数和

   ```
   #!/bin/bash
   i=1
   s=0
   while (($i <= 1 2="=" 3 50 1099 1200 10)) do if (($i % 0)) then s="$((" $s + $i )) fi i="$((" done echo "the sum is:$s" ``` let命令可以直接执行基本的算术操作。当使用let时,变量名之前不需要再添加$,例如: let result="no1+no2" $result no2] 在[]中也可以使用$前缀,例如: 5] 也可以使用(()),但使用(())时,变量名之前需要加上$: no1 expr同样可以用于基本算术操作: 4` $no1 5) 以上这些方法只能用于整数运算,而不支持浮点数。 bc是一个用于数学运算的高级工具,这个精密计算器包含了大量的选项。我们可以借助它执行浮点数运算并应用一些高级函数: "4 * 0.56" | bc 2.24 no="54" "$no 1.5" bc` 81.0 ## docker卷——持久化的问题 容器的大部分力量源自它们能够尽可能多地封装运行时环境里的文件系统状态,这一点的确很有用处。 然而,有时候用户并不想把文件放到容器里。用户可能想要在容器之间共享或者单独管理一些大文件。一个经典的例子便是想要在容器里访问一个大型的集中式数据库,但是又希望其他的(也许是更传统的)客户端也能和新容器一样访问它。 解决方案便是卷,一种docker用来管理容器生命周期之外的文件的机制。尽管这有悖于容器“可以在任意地方部署”的原则(例如,用户将无法在不兼容数据库挂载的地方部署有数据库依赖的容器),但是在实际的docker使用中这仍然是一个很有用的功能。 ### 问题 想要在容器里访问宿主机上的文件。 解决方案 使用docker的volume标志,在容器里访问宿主机上的文件。图5-3演示了使用volume标志和宿主机上的文件系统交互的例子。 下面的命令展示了如何将宿主机上的 var db tables目录挂载到容器里的 data1目录,该命令在图5-3里启动容器时被执行: $ docker run -v tables: data1 -it debian bash {% asset_img image-20210326051559896.png image-20210326051559896 %} 图5-3 容器里的一个数据卷-v标志(--volume的简写)表示容器指定的一个外部卷。随后的参数以冒号分隔两个目录的形式给出了卷的挂载配置,告知docker将外部的 tables目录映射到容器里的 data1目录。外部目录和容器目录两者任一不存在的话均会被创建。 对已经存在的目录建立映射时要小心。即便镜像里已经存在需要映射的目录,该目录依旧会映射到宿主机上的对应目录。这意味着容器里映射目录的原本内容将会消失。如果用户试图映射一个关键目录,将会发生一些有趣的事情!例如,试试挂载一个空目录到 bin。 另外要注意的一点是,dockerfile里的卷被设定为不是持久化的。如果用户添加了一个卷,然后在一个dockerfile里对该目录做了一些更改,那么这些变化将不会被持久化到最终生成的镜像里。 最后还有一点,用户将会看到不少技巧里用到了-v docker.sock: docker.sock,技巧45就是其中的一个。这样做会把特殊的unix套接字文件公开到容器里,并且展示出此项技巧的一个重要功能——用户不必局限于所谓的“常规”文件,还可以应用更多不常见的文件系统层面的用例。但是,如果用户遇到设备节点的权限问题(举个例子),可能需要参考技巧93,了解一下--privileged标志的作用。 部署 springboot项目 1.创建springboot项目 ![img](https: upload-images.jianshu.io upload_images 15536448-022dcb5d56f43d70.png?imagemogr2 auto-orient strip|imageview2 w format webp) 创建springboot项目 ```css package com.eangulee.demo.controller; import org.springframework.stereotype.controller; org.springframework.web.bind.annotation.getmapping; org.springframework.web.bind.annotation.requestmapping; org.springframework.web.bind.annotation.responsebody; @controller public class hellocontroller { @requestmapping(" ") @responsebody string hello() return "hello, springboot with docker"; } 2.打包springboot项目为jar包 15536448-30b7e78666cc0094.png?imagemogr2 打包命令: clean 3. 编写dockerfile文件 ```ruby # image for file version 0.0.1 author: eangulee 基础镜像使用java from java:8 作者 maintainer 
# VOLUME 指定了临时文件目录为/tmp。
# 其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp
VOLUME /tmp 
# 将jar包添加到容器中并更名为app.jar
ADD demo-0.0.1-SNAPSHOT.jar app.jar 
# 运行jar包
RUN bash -c 'touch /app.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
```

**解释下这个配置文件:**

VOLUME 指定了临时文件目录为/tmp。其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp。该步骤是可选的,如果涉及到文件系统的应用就很有必要了。/tmp目录用来持久化到 Docker 数据文件夹,因为 Spring Boot 使用的内嵌 Tomcat 容器默认使用/tmp作为工作目录
 项目的 jar 文件作为 “app.jar” 添加到容器的
 ENTRYPOINT 执行项目 app.jar。为了缩短 Tomcat 启动时间,添加一个系统属性指向 “/dev/./urandom” 作为 Entropy Source

如果是第一次打包,它会自动下载java 8的镜像作为基础镜像,以后再制作镜像的时候就不会再下载了。

### 4. 部署文件

在服务器新建一个docker文件夹,将maven打包好的jar包和Dockerfile文件复制到服务器的docker文件夹下



![img](https:////upload-images.jianshu.io/upload_images/15536448-9af07900b7686a79.png?imageMogr2/auto-orient/strip|imageView2/2/w/360/format/webp)

docker文件夹

### 5. 制作镜像

执行下面命令, 看好,最后面有个"."点!



```undefined
docker build -t springbootdemo4docker .
```

-t 参数是指定此镜像的tag名



![img](https:////upload-images.jianshu.io/upload_images/15536448-d5087e016f87a13a.png?imageMogr2/auto-orient/strip|imageView2/2/w/729/format/webp)

制作完成后通过**docker images**命令查看我们制作的镜像

![img](https:////upload-images.jianshu.io/upload_images/15536448-4a8a43c82addd15e.png?imageMogr2/auto-orient/strip|imageView2/2/w/882/format/webp)



### 6.启动容器



```csharp
[root@localhost docker]# docker run -d -p 8080:8085 springbootdemo4docker
-d参数是让容器后台运行 
-p 是做端口映射,此时将服务器中的8080端口映射到容器中的8085(项目中端口配置的是8085)端口
```

### 7. 访问网站

直接浏览器访问: http://你的服务器ip地址:8080/

![img](https:////upload-images.jianshu.io/upload_images/15536448-a42bfc6b886d4ae2.png?imageMogr2/auto-orient/strip|imageView2/2/w/783/format/webp)

## Docker部署SpringBoot项目及mysql

最近写了一个小项目,正好比较闲,就想趁机会学习一下docker,把服务docker化。本以为会比较简单,结果没想到项目与容器内数据库连接的时候卡住了,具体看操作步骤吧。
安装docker

```
yum -y install docker-io
```

启动docker

```
service docker start
```

启动自带helloworld测试

```
docker run hello-world  
```

配置阿里云镜像加速
登录阿里云控制台,网址:https://cr.console.aliyun.com/?accounttraceid=6fb2e741-14b1-477e-a5a3-976f3a963051#/accelerator
进入如下界面,按照讲解配置镜像加速

5.安装java镜像,因为之后要通过java -jar命令运行项目jar包,所以下载java镜像;

```
docker pull java:8u111
```

6.创建mysql容器
   搜索mysql镜像

```
docker search mysql
```

安装mysql5.7镜像

```
docker pull mysql:5.7
```

使用镜像创建容器

```
docker run -p 3306:3306 --name mysql -v $PWD/conf:/etc/mysql/conf.d -v $PWD/logs:/logs -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

docker run -p 3306:3306 --name mysql -v /root/mysql/conf:/etc/mysql/conf.d -v /root/mysql/logs:/logs -v /root/mysql/data:/opt/mysql/data -e MYSQL_ROOT_PASSWORD=root -d 192.168.1.190:5000/centos/mysql-57-centos7
```

7.部署springboot项目jar包
将项目打包成可执行的jar包,数据库配置可以直接写localhost
先建docker文件夹
把jar包上传到服务器放到docker文件夹去,同时创建文件Dockerfile,内容如下:
开始构建镜像,注意一定要使用host网络连接方式,否则项目会连接不上mysql,导致项目起不来。

```
docker run    --net=host  --name=news  -d -p 8081:8081 news
```

目前为止,项目就成功跑来了,可以去浏览器访问一下看看是否成功。

小编一开始没有采用host的方式去启动项目容器,结果一直连接不上mysql,这是因为docker会自动给每个容器分配ip,但是项目里配置的mysql地址是localhost,在容器内连接localhost是连接容器本身而不是宿主机的localhost,所以一直找不到mysql。所以采用host的网络方式将容器与宿主机共用一个Network Namespace,这样容器内localhost就是宿主机的localhost了。具体请参考以下两篇文章:点击打开链接 点击打开链接



## 在Centos上安装docker compose

官网安装说明:https://docs.docker.com/compose/install/

1. 运行以下命令以下载Docker Compose的当前稳定版本:

   ```
   sudo curl -L "https://github.com/docker/compose/releases/download/1.29.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
   ```

2. 将可执行权限应用于二进制文件:

   ```
   sudo chmod +x /usr/local/bin/docker-compose
   ```

3. 测试安装

   ```
   docker-compose --version
   ```

## 阿里云镜像加速器

1. 访问阿里云官网:https://www.aliyun.com/

2. 使用 支付宝 或 钉钉 登录

3. 访问镜像加速器

   {% asset_img image-20210410061812531.png image-20210410061812531 %}

4. 粘贴加速器地址到 docker desktop 的设置中

   {% asset_img image-20210410061910390.png image-20210410061910390 %}

5. 保存并重启

# 防火墙操作

1. 查看状态:systemctl status firewalld
2. 关闭防火墙:systemctl stop firewalld

# 启动mysql容器

 docker run --name mysql-read -p 3316:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

# 安装vim

 apt-get update

apt-get install vim

# 重启mysql服务

service mysql restart

# 解压gz文件

gzip -dv *

# dhclient释放获取IP

dhclient -r //release ip 释放IP

dhclient //获取IP

# 开放及查看端口

1、开放端口

**firewall-cmd --zone=public --add-port=5672/tcp --permanent**  # 开放5672端口

**firewall-cmd --zone=public --remove-port=5672/tcp --permanent** #关闭5672端口

**firewall-cmd --reload**  # 配置立即生效

 

2、查看防火墙所有开放的端口

**firewall-cmd --zone=public --list-ports** 

3.、关闭防火墙

如果要开放的端口太多,嫌麻烦,可以关闭防火墙,安全性自行评估

**systemctl stop firewalld.service**

 

4、查看防火墙状态

 **firewall-cmd --state**

 

5、查看监听的端口

**netstat -lnpt**

{% asset_img 1336432-20190302110949754-1765820036.png img %}

*PS:centos7默认没有 netstat 命令,需要安装 net-tools 工具,yum install -y net-tools*

 

 

6、检查端口被哪个进程占用

**netstat -lnpt |grep 5672**

{% asset_img 1336432-20190302104128381-1210567174.png img %}

 

7、查看进程的详细信息

**ps 6832**

{% asset_img 1336432-20190302104342651-779103690.png img %}

 

8、中止进程

**kill -9 6832**

# Docker的启动与停止

```text
systemctl命令是系统服务管理器指令
启动docker:
systemctl start docker
停止docker:
systemctl stop docker
重启docker:
systemctl restart docker
查看docker状态:
systemctl status docker
开机启动:
systemctl enable docker
查看docker概要信息
docker info
查看docker帮助文档
docker ‐‐help
```

# 修改主机名

//永久性的修改主机名称,重启后能保持修改后的。
hostnamectl set-hostname xxx	

//删除hostname
hostnamectl set-hostname ""
hostnamectl set-hostname "" --static
hostnamectl set-hostname "" --pretty

# 重启linux

reboot -f

# 在命令行模式和图形模式切换

在图形界面时
1.按ctrl+alt+f3(任何时候都可以不仅限以下画面)
2.在终端输入init 3

在命令行模式时
1.按ctrl+alt+f1
2.登录用户后输入init 5

## 查看CentOS版本

```
cat  /etc/redhat-release
```

## 阿里云CentOS镜像

```
https://mirrors.aliyun.com/centos/7/isos/x86_64/
```

# 华信服务器说明

```
http://192.168.1.190:8080/
```

## 查看docker服务器内容

```
 curl 192.168.1.190:5000/v2/_catalog
 
 {"repositories":["centos/mysql-57-centos7","nginx","redis","tomcat"]}
```

## 查看镜像版本号

```
 curl 192.168.1.190:5000/v2/redis/tags/list
```

## 添加/etc/docker/daemon.json文件

```
{
       "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"],
       "insecure-registries":["192.168.1.190:5000"]
}
```

## 重启docker服务

```
systemctl restart docker
```



## 下载tomcat的docker镜像测试

```
docker pull 192.168.1.190:5000/tomcat:8
```

下载其他的镜像的时候,不需要添加最后的标签名,因为服务器上他们的默认标签名是:latest

# centos7 rpm 离线安装 docker

```
https://www.jianshu.com/p/764ec08196e0
```

## Docker下载包

```
https://download.docker.com/linux/centos/
```

## 更新yum源

```
1.备份原有yum源

cd /etc/yum.repos.d/
mkdir backup
mv ./*.repo ./backup/ 
2.输入如下命令

REPO_URL=http://192.168.1.190:5001/repository/dhee-group-yum/

cat > /etc/yum.repos.d/nexus.repo << EOF
[base]
name=Nexus
baseurl=$REPO_URL\$releasever/os/\$basearch/
enabled=1
gpgcheck=0
[extras]
name=Nexus
baseurl=$REPO_URL\$releasever/extras/\$basearch/
enabled=1
gpgcheck=0
[centosplus]
name=Nexus
baseurl=$REPO_URL\$releasever/centosplus/\$basearch/
enabled=1
gpgcheck=0
EOF
3.更新本地yum源

yum update
```

## 安装顺序

安装依赖包:按照下列顺序安装

```
-rw-r--r--. 1 root root  78256 12月 22 14:50 audit-libs-python-2.8.5-4.el7.x86_64.rpm
-rw-r--r--. 1 root root 302068 12月 22 14:50 checkpolicy-2.5-8.el7.x86_64.rpm
-rw-r--r--. 1 root root  67720 12月 22 14:51 libcgroup-0.41-21.el7.x86_64.rpm
-rw-r--r--. 1 root root  57460 12月 22 14:51 libseccomp-2.3.1-4.el7.x86_64.rpm
-rw-r--r--. 1 root root 115284 12月 22 14:51 libsemanage-python-2.5-14.el7.x86_64.rpm
-rw-r--r--. 1 root root  32880 12月 22 14:52 python-IPy-0.75-6.el7.noarch.rpm
-rw-r--r--. 1 root root 635184 12月 22 14:52 setools-libs-3.3.8-4.el7.x86_64.rpm
-rw-r--r--. 1 root root   468316 12月 22 14:52 policycoreutils-python-2.5-34.el7.x86_64.rpm
```

按照顺序安装docker

1. container-selinux-2.119.2-1.911c772.el7_8.noarch.rpm
2. containerd.io-1.4.9-3.1.el7.x86_64.rpm
3. docker-ce-cli-19.03.9-3.el7.x86_64.rpm
4. docker-ce-19.03.9-3.el7.x86_64.rpm

重启docker服务器

```

```



```
192.168.204.26
```

# 华信Docker离线安装

1. 更新Yum源

   ```
   补充:
   手工删除/etc/yum.repos.d/目录中除了 nexus.repo 之外的其他repo文件
   
   yum clean all
   yum makecache
   ```

   

2. 安装Docker

3. 更改docker源配置

# 华信安装redis

一、安装gcc依赖

由于 redis 是用 C 语言开发,安装之前必先确认是否安装 gcc 环境(gcc -v),如果没有安装,执行以下命令进行安装

 **[root@localhost local]# yum install -y gcc** 

 

二、下载并解压安装包

**[root@localhost local]# wget http://download.redis.io/releases/redis-5.0.3.tar.gz**(此步骤不做,从网上下载redis最新版)

redis最新版下载地址:https://redis.io/download

{% asset_img image-20211223110812306.png image-20211223110812306 %}

**[root@localhost local]# tar -zxvf redis-5.0.3.tar.gz**

 

三、cd切换到redis解压目录下,执行编译

**[root@localhost local]# cd redis-5.0.3**

**[root@localhost redis-5.0.3]# make**

 

四、安装并指定安装目录

**[root@localhost redis-5.0.3]# make install PREFIX=/usr/local/redis**

 

五、启动服务

5.1前台启动

**[root@localhost redis-5.0.3]# cd /usr/local/redis/bin/**

**[root@localhost bin]# ./redis-server**

 

5.2后台启动

从 redis 的源码目录中复制 redis.conf 到 redis 的安装目录

**[root@localhost bin]# cp /usr/local/redis-5.0.3/redis.conf /usr/local/redis/bin/**

 

修改 redis.conf 文件,把 daemonize no 改为 daemonize yes

**[root@localhost bin]# vi redis.conf**

{% asset_img 1336432-20190302212509880-1874470634.png img %}

后台启动

**[root@localhost bin]# ./redis-server redis.conf**

{% asset_img 1336432-20190302212804992-1094141996.png img %}

 

六、设置开机启动

添加开机启动服务

**[root@localhost bin]# vi /etc/systemd/system/redis.service**

复制粘贴以下内容:

[{% asset_img copycode.gif)](javascript:void(0); 复制代码 %}

[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/redis/bin/redis-server /usr/local/redis/bin/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target

[{% asset_img copycode.gif)](javascript:void(0); 复制代码 %}

注意:ExecStart配置成自己的路径 

 

设置开机启动

**[root@localhost bin]# systemctl daemon-reload**

**[root@localhost bin]# systemctl start redis.service**

**[root@localhost bin]# systemctl enable redis.service**

 

创建 redis 命令软链接

**[root@localhost ~]# ln -s /usr/local/redis/bin/redis-cli /usr/bin/redis**

测试 redis

{% asset_img 1336432-20190302221347104-518199130.png img %}

 

服务操作命令

systemctl start redis.service  #启动redis服务

systemctl stop redis.service  #停止redis服务

systemctl restart redis.service  #重新启动服务

systemctl status redis.service  #查看服务当前状态

systemctl enable redis.service  #设置开机自启动

systemctl disable redis.service  #停止开机自启动

 

代码改变一切!

# 华信安装无法独立安装mysql

rpm包缺少依赖

# 华信安装nginx

1. 下载:https://nginx.org/en/download.html

   ```
   nginx-1.21.4.tar.gz
   ```

2. 解压

   依然是直接命令:

   ```
   tar -zxvf nginx-1.21.4.tar.gz
   cd nginx-1.21.4
   ```

3. 安装依赖

   ```
   yum -y install pcre-devel
   yum -y install openssl openssl-devel
   ```

4. 配置

   - 其实在 nginx-1.21.4 版本中你就不需要去配置相关东西,默认就可以了。当然,如果你要自己配置目录也是可以的。
     使用默认配置

   ```
   ./configure
   ```

   - 自定义配置(不推荐)

   ```
   ./configure \
   --prefix=/usr/local/nginx \
   --conf-path=/usr/local/nginx/conf/nginx.conf \
   --pid-path=/usr/local/nginx/conf/nginx.pid \
   --lock-path=/var/lock/nginx.lock \
   --error-log-path=/var/log/nginx/error.log \
   --http-log-path=/var/log/nginx/access.log \
   --with-http_gzip_static_module \
   --http-client-body-temp-path=/var/temp/nginx/client \
   --http-proxy-temp-path=/var/temp/nginx/proxy \
   --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
   --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
   --http-scgi-temp-path=/var/temp/nginx/scgi
   ```

   > 注:将临时文件目录指定为/var/temp/nginx,需要在/var下创建temp及nginx目录

4. 编译安装

   ```
   make
   
   make install PREFIX=/usr/local/nginx
   ```

5. 查找安装路径:

   ```
   whereis nginx
   ```

6. 启动、停止nginx

   ```
   cd /usr/local/nginx/sbin/
   ./nginx 
   ./nginx -s stop
   ./nginx -s quit
   ./nginx -s reload
   ```

```
启动时报80端口被占用:
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)

```

` 解决办法:1、安装net-tool 包:`yum install net-tools

```
 
```

> `./nginx -s quit`:此方式停止步骤是待nginx进程处理任务完毕进行停止。
> `./nginx -s stop`:此方式相当于先查出nginx进程id再使用kill命令强制杀掉进程。

查询nginx进程:

```
ps aux|grep nginx
```

重启 nginx

1.先停止再启动(推荐):
对 nginx 进行重启相当于先停止再启动,即先执行停止命令再执行启动命令。如下:

```
./nginx -s quit
./nginx
```

2.重新加载配置文件:
当 ngin x的配置文件 nginx.conf 修改后,要想让配置生效需要重启 nginx,使用`-s reload`不用先停止 ngin x再启动 nginx 即可将配置信息在 nginx 中生效,如下:
./nginx -s reload

启动成功后,在浏览器可以看到这样的页面:

{% asset_img 160905180451093.png nginx-welcome.png %}

# 华信安装jdk8

JDK安装
	步骤:	


	1)查看当前Linux系统是否已经安装java
		输入 rpm -qa | grep java
	2)卸载两个openJDK
	输入rpm -e --nodeps 要卸载的软件名
	3)上传jdk到linux
	4)安装jdk运行需要的插件yum install glibc.i686(特殊情况下可选做,一般还是要安装的)
	5)解压jdk到/usr/local下 tar –xvf jdk-7u71-linux-i586.tar.gz –C /usr/local
	6)配置jdk环境变量,打开/etc/profile配置文件,将下面配置拷贝进去
		#set java environment
		JAVA_HOME=/usr/local/jdk1.7.0_71
		CLASSPATH=.:$JAVA_HOME/lib.tools.jar
		PATH=$JAVA_HOME/bin:$PATH
		export JAVA_HOME CLASSPATH PATH 
	7)重新加载/etc/profile配置文件 source /etc/profile
	8)使用命令测试,是否安装成功
		java -version

# 华信安装tomcat9

```
Tomcat安装
	步骤:
	1)上传Tomcat到linux上
	2)解压Tomcat到/usr/local下
		tar -xvf apache-tomcat-7.0.57.tar.gz -C /usr/local/
	3)开放Linux的对外访问的端口8080
	/sbin/iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
	
	//规则防火墙规则(centos7中不好用)
	/etc/rc.d/init.d/iptables save
	4)启动关闭Tomcat
	进入tomcat的bin下启动:./startup.sh
	进入tomcat的bin下关闭:./shutdown.sh
	5)测试:
		http://linux服务器ip:8080
		显示tomcat主页,验证成功!
```

# 华信使用Nginx

```
5.配置虚拟主机
	配置虚拟主机
		就是在一台服务器启动多个网站。
		如何区分不同的网站:
		a、域名不同
		b、端口不同
6.通过端口区分不同虚拟机
	1)修改Nginx的配置文件:(注意修改内容,如下,注意结束大括号的位置)
		/usr/local/nginx/conf/nginx.conf,在最下面copy一个server,修改其端口号:
		
		server {
			listen       81;
			server_name  localhost;

			#charset koi8-r;

			#access_log  logs/host.access.log  main;

			location / {
				root   html-81;
				index  index.html index.htm;
			}
		}
		保存
	2)复制html文件夹,命名为html-81
		[root@CentOs nginx]# cp -r html html-81
	3)修改html-81文件夹下的index.html文件,以区分不同的端口网站,如,改成:
			

Welcome to nginx-81!

编辑文件:[root@CentOs html-81]# vim index.html 输入i:然后可以修改内容,改完后按esc键,再按“:”,再按“wq”(保存后退出) 4)刷新nginx服务:[root@CentOs sbin]# ./nginx -s reload 5)在浏览器中测试该81端口:http://192.168.146.128:81/ 7.域名 域名就是网站。 www.baidu.com www.taobao.com www.jd.com Tcp/ip Dns服务器:把域名解析为ip地址。保存的就是域名和ip的映射关系。 一级域名: Baidu.com Taobao.com Jd.com 二级域名: www.baidu.com Image.baidu.com Item.baidu.com 三级域名: 1.Image.baidu.com Aaa.image.baidu.com 一个域名对应一个ip地址,一个ip地址可以被多个域名绑定。 本地测试可以修改hosts文件。 修改window的hosts文件:(C:\Windows\System32\drivers\etc),默认内容都被注释掉了 可以配置域名和ip的映射关系,如果hosts文件中配置了域名和ip的对应关系,不需要走dns服务器。 8.使用switchhosts修改主机域名解析(配置后,这里的域名就不走dns了) 1)打开switchhosts软件,添加一个新的本地方案(使用窗口底部左面的“+”按钮) 2)设置本地方案名称 3)编辑本地方案内:如 192.168.146.128 www.test.com 192.168.146.128 www.test2.com 192.168.146.128 www.test3.com 4)切换host(使用底部右边“对号”按钮来切换,有时系统会拦截该切换行为,弹出对话框,要允许该操作才可以) 5)在浏览器中测试,分别输入上面设置的域名,如:www.test2.com,看能不能访问nginx网站 9.nginx域名配置 1)修改nginx的配置文件:/usr/local/nginx/conf/nginx.conf,改为: server { listen 80; server_name www.test.com; #charset koi8-r; #access_log logs/host.access.log main; location / { root html-test; index index.html index.htm; } } server { listen 80; server_name www.test2.com; #charset koi8-r; #access_log logs/host.access.log main; location / { root html-test2; index index.html index.htm; } } 目的:一个ip地址,可以为多个域名提供80端口的服务映射 2)在linux上复制出html-test和html-test2目录: [root@CentOs nginx]# cp -r html html-test [root@CentOs nginx]# cp -r html html-test2 3)修改html-test和html-test2目录中index.html的内容为: nginx-test! 和 nginx-test2! 2)刷新nginx服务器:[root@CentOs sbin]# ./nginx -s reload 3)在浏览器中测试: http://www.test.com =>nginx-test! http://www.test2.com =>nginx-test2! 10.nginx反向代理 反向代理服务器决定哪台服务器提供服务。 反向代理服务器不提供服务,就是请求的转发。 正向代理,也就是传说中的代理,他的工作原理就像一个跳板, 简单的说, 我是一个用户,我访问不了某网站,但是我能访问一个代理服务器 这个代理服务器呢,他能访问那个我不能访问的网站 于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容 代理服务器去取回来,然后返回给我 从网站的角度,只在代理服务器来取内容的时候有一次记录 有时候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站 结论就是 正向代理 是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。 正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。正向代理还可以使用缓冲特性减少网络使用率。 反向代理的典型用途是将 防火墙后面的服务器提供给Internet用户访问。 反向代理还可以为后端的多台服务器提供负载平衡,或为后端较慢的服务器提供缓冲服务。 反向代理还可以启用高级URL策略和管理技术,从而使处于不同web服务器系统的web页面同时存在于同一个URL空间下。 11.nginx实现反向代理(假设该公司只有一个公网ip,但有两个网站,分别放在了两台服务器上) 1)在本机的hosts文件中添加:(不能带端口号) 192.168.146.128 www.sina.com.cn 192.168.146.128 www.sohu.com 2)上传tomcat 安装包到linux的root下 3)解压缩tomcat安装包:tar zxf apache-tomcat-7.0.57.tar.gz 4)复制一份,命名为:cp apache-tomcat-7.0.57 tomcat-sina -r 5)复制一份,命名为:cp apache-tomcat-7.0.57 tomcat-sohu -r 6)编辑sohu tomcat端口号:vim tomcat-sohu/conf/server.xml(使用/port查找) i) 改为: ii) 改为: iii) 改为: 以Tomcat7.0为例, 在安装目录下. conf/server.xml 中可以配置三个端口号, 如果使用多个tomcat 是需要配置这三个. 该Connector 用于监听请求. protocol: HTTP/1.1 协议 ,用于监听浏览器发送的请求. 设置成80 后可以直接使用http://localhost 访问 AJP/1.3 协议 , 用于监听其他服务器转发过来的请求. connectionTimeout: 连接超时时间 redirectPort: 如果发送的是https 请求. 就将请求转发到8443 端口. 使用8009 接受其他服务器转发过来的请求. tomcat 监听的关闭端口. 7)分别启动两个tomcat: tomcat-sina/bin/startup.sh tomcat-sohu/bin/startup.sh 8)在浏览器中测试: http://192.168.146.128:8080/ http://192.168.146.128:8081/ 9)修改tomcat中欢迎页内容,表示区别 i)vim tomcat-sina/webapps/ROOT/index.jsp ii)使用命令:/h1,定位到h1标签,修改内容为:

${pageContext.servletContext.serverInfo}-sina

iii)vim tomcat-sohu/webapps/ROOT/index.jsp iv)使用命令:/h1,定位到h1标签,修改内容为:

${pageContext.servletContext.serverInfo}-sohu

10)在浏览器中重新测试: http://192.168.146.128:8080/ http://192.168.146.128:8081/ 11)修改nginx的配置文件:/usr/local/nginx/conf/nginx.conf,添加如下内容: upstream sina{ server 192.168.146.128:8080; } server { listen 80; server_name www.sina.com.cn; #charset koi8-r; #access_log logs/host.access.log main; location / { proxy_pass http://sina; index index.html index.htm; } } upstream sohu{ server 192.168.146.128:8081; } server { listen 80; server_name www.sohu.com; #charset koi8-r; #access_log logs/host.access.log main; location / { proxy_pass http://sohu; index index.html index.htm; } } 12)刷新nginx服务器:[root@CentOs sbin]# ./nginx -s reload 13)在浏览器中测试: http://www.sina.com.cn http://www.sohu.com 12)nginx负载均衡 如果一个服务由多个服务器提供,需要把负载分配到不同的服务器处理,需要负载均衡。 upstream sina { server 192.168.25.148:8081; server 192.168.25.148:8082; } 1)复制一份tomcat,命名为:cp apache-tomcat-7.0.57 tomcat-sina2 -r 2)编辑sohu tomcat端口号:vim tomcat-sina2/conf/server.xml(使用/port查找) i) 改为: ii) 改为: iii) 改为: 3)启动tomcat: tomcat-sina2/bin/startup.sh 4)修改tomcat中欢迎页内容,表示区别 i)vim tomcat-sina2/webapps/ROOT/index.jsp ii)使用命令:/h1,定位到h1标签,修改内容为:

${pageContext.servletContext.serverInfo}-sina2

5)在浏览器中重新测试: http://192.168.146.128:8082/ 6)修改 upstream sina{ server 192.168.146.128:8080; } 改为: upstream sina{ server 192.168.146.128:8080; server 192.168.146.128:8082; } 7)刷新nginx服务器:[root@CentOs sbin]# ./nginx -s reload 8)在浏览器中测试:(反复刷新,看是否在两台服务器间负载均衡,别再eclipse中的浏览器测试,最好使用谷歌浏览器) http://www.sina.com.cn 9)可以根据服务器的实际情况调整服务器权重。权重越高分配的请求越多,权重越低,请求越少。默认是都是1 i)修改权重 upstream sina{ server 192.168.146.128:8080; server 192.168.146.128:8082 weight=2; } ii)刷新nginx服务器:[root@CentOs sbin]# ./nginx -s reload iii)在浏览器中测试:(反复刷新,看是否在两台服务器间负载均衡,别再eclipse中的浏览器测试,最好使用谷歌浏览器) http://www.sina.com.cn ``` # Docker的卷 ## 问题 想要在容器里访问宿主机上的文件。 ## 解决方案 使用Docker的volume标志,在容器里访问宿主机上的文件。图5-3演示了使用volume标志和宿主机上的文件系统交互的例子。 下面的命令展示了如何将宿主机上的/var/db/tables目录挂载到容器里的/var/data1目录,该命令在图5-3里启动容器时被执行: ``` docker run -v /var/db/tables:/var/data1 -it debian bash ``` {% asset_img image-20211227133001386.png image-20211227133001386 %} -v标志(--volume的简写)表示容器指定的一个外部卷。随后的参数以冒号分隔两个目录的形式给出了卷的挂载配置,告知Docker将外部的/var/db/tables目录映射到容器里的/var/data1目录。外部目录和容器目录两者任一不存在的话均会被创建。 # Docker以守护进程运行 ``` //与docker run一起使用的-d标志将以守护进程方式运行容器。 //-i标志则赋予容器与Telnet会话交互的能力 docker run -d -i debian bash docker exec -it 容器id ``` # Dockerfile命令 ## **FROM** 功能为指定基础镜像,并且必须是第一条指令。 如果不以任何镜像为基础,那么写法为:FROM scratch。 同时意味着接下来所写的指令将作为镜像的第一层开始 语法: ``` FROM FROM : FROM : ``` 三种写法,其中 是可选项,如果没有选择,那么默认值为latest ## RUN 功能为运行指定的命令 RUN命令有两种格式 ``` 1. RUN 2. RUN ["executable", "param1", "param2"] ``` 第一种后边直接跟shell命令 - 在linux操作系统上默认 /bin/sh -c - 在windows操作系统上默认 cmd /S /C 第二种是类似于函数调用。 可将executable理解成为可执行文件,后面就是两个参数。 两种写法比对: - ``` RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME ``` - ``` RUN ["/bin/bash", "-c", "echo hello"] ``` 注意:多行命令不要写多个RUN,原因是Dockerfile中每一个指令都会建立一层. 多少个RUN就构建了多少层镜像,会造成镜像的臃肿、多层,不仅仅增加了构件部署的时间,还容易出错。 RUN书写时的换行符是\ ## CMD 功能为容器启动时要运行的命令 语法有三种写法 ``` 1. CMD ["executable","param1","param2"] 2. CMD ["param1","param2"] 3. CMD command param1 param2 ``` 第三种比较好理解了,就时shell这种执行方式和写法 第一种和第二种其实都是可执行文件加上参数的形式 举例说明两种写法: - ``` CMD [ "sh", "-c", "echo $HOME" ``` - ``` CMD [ "echo", "$HOME" ] ``` 补充细节:这里边包括参数的一定要用双引号,就是",不能是单引号。千万不能写成单引号。 原因是参数传递后,docker解析的是一个JSON array RUN & CMD 不要把RUN和CMD搞混了。 RUN是构件容器时就运行的命令以及提交运行结果 CMD是容器启动时执行的命令,在构件时并不运行,构件时紧紧指定了这个命令到底是个什么样子 ## LABEL 功能是为镜像指定标签 语法: ``` LABEL = = = ... ``` 一个Dockerfile种可以有多个LABEL,如下: ``` LABEL "com.example.vendor"="ACME Incorporated" LABEL com.example.label-with-value="foo" LABEL version="1.0" LABEL description="This text illustrates \ that label-values can span multiple lines." ``` 但是并不建议这样写,最好就写成一行,如太长需要换行的话则使用\符号 如下: ``` LABEL multi.label1="value1" \ multi.label2="value2" \ other="value3" ``` 说明:LABEL会继承基础镜像种的LABEL,如遇到key相同,则值覆盖 ## MAINTAINER 指定作者 语法: ``` MAINTAINER ``` ## EXPOSE 功能为暴漏容器运行时的监听端口给外部 但是EXPOSE并不会使容器访问主机的端口 如果想使得容器与主机的端口有映射关系,必须在容器启动的时候加上 -P参数 ## ENV 功能为设置环境变量 语法有两种 ``` 1. ENV 2. ENV = ... ``` 两者的区别就是第一种是一次设置一个,第二种是一次设置多个 ## **ADD** 一个复制命令,把文件复制到景象中。 如果把虚拟机与容器想象成两台linux服务器的话,那么这个命令就类似于scp,只是scp需要加用户名和密码的权限验证,而ADD不用。 语法如下: ``` 1. ADD ... 2. ADD ["",... ""] ``` 路径的填写可以是容器内的绝对路径,也可以是相对于工作目录的相对路径 可以是一个本地文件或者是一个本地压缩文件,还可以是一个url 如果把写成一个url,那么ADD就类似于wget命令 如以下写法都是可以的: - ``` ADD test relativeDir/ ``` - ``` ADD test /relativeDir ``` - ``` ADD http://example.com/foobar / ``` 尽量不要把写成一个文件夹,如果是一个文件夹了,复制整个目录的内容,包括文件系统元数据 ## COPY 看这个名字就知道,又是一个复制命令 语法如下: ``` 1. COPY ... 2. COPY ["",... ""] ``` 与ADD的区别 COPY的只能是本地文件,其他用法一致 ## ENTRYPOINT 功能是启动时的默认命令 语法如下: ``` 1. ENTRYPOINT ["executable", "param1", "param2"] 2. ENTRYPOINT command param1 param2 ``` 如果从上到下看到这里的话,那么你应该对这两种语法很熟悉啦。 第二种就是写shell 第一种就是可执行文件加参数 与CMD比较说明(这俩命令太像了,而且还可以配合使用): \1. 相同点: - 只能写一条,如果写了多条,那么只有最后一条生效 - 容器启动时才运行,运行时机相同 \2. 不同点: - ENTRYPOINT不会被运行的command覆盖,而CMD则会被覆盖 - 如果我们在Dockerfile种同时写了ENTRYPOINT和CMD,并且CMD指令不是一个完整的可执行命令,那么CMD指定的内容将会作为ENTRYPOINT的参数 如下: ``` FROM ubuntu ENTRYPOINT ["top", "-b"] CMD ["-c"] ``` - 如果我们在Dockerfile种同时写了ENTRYPOINT和CMD,并且CMD是一个完整的指令,那么它们两个会互相覆盖,谁在最后谁生效 如下: ``` FROM ubuntu ENTRYPOINT ["top", "-b"] CMD ls -al ``` 那么将执行ls -al ,top -b不会执行。 Docker官方使用一张表格来展示了ENTRYPOINT 和CMD不同组合的执行情况 (下方表格来自docker官网) {% asset_img 0.jpeg img %} ## VOLUME 可实现挂载功能,可以将内地文件夹或者其他容器种得文件夹挂在到这个容器种 语法为: ``` VOLUME ["/data"] ``` 说明: ["/data"]可以是一个JsonArray ,也可以是多个值。所以如下几种写法都是正确的 ``` VOLUME ["/var/log/"] VOLUME /var/log VOLUME /var/log /var/db ``` 一般的使用场景为需要持久化存储数据时 容器使用的是AUFS,这种文件系统不能持久化数据,当容器关闭后,所有的更改都会丢失。 所以当数据需要持久化时用这个命令。 ## USER 设置启动容器的用户,可以是用户名或UID,所以,只有下面的两种写法是正确的 - ``` USER daemo ``` - ``` USER UID ``` 注意:如果设置了容器以daemon用户去运行,那么RUN, CMD 和 ENTRYPOINT 都会以这个用户去运行 ## WORKDIR 语法: ``` WORKDIR /path/to/workdir ``` 设置工作目录,对RUN,CMD,ENTRYPOINT,COPY,ADD生效。如果不存在则会创建,也可以设置多次。 如: ``` WORKDIR /a WORKDIR b WORKDIR c RUN pwd ``` pwd执行的结果是/a/b/c WORKDIR也可以解析环境变量 如: ``` ENV DIRPATH /path WORKDIR $DIRPATH/$DIRNAME RUN pwd ``` pwd的执行结果是/path/$DIRNAME ## ARG 语法: ``` ARG [=] ``` 设置变量命令,ARG命令定义了一个变量,在docker build创建镜像的时候,使用 --build-arg =来指定参数 如果用户在build镜像时指定了一个参数没有定义在Dockerfile种,那么将有一个Warning 提示如下: ``` [Warning] One or more build-args [foo] were not consumed. ``` 我们可以定义一个或多个参数,如下: ``` FROM busybox ARG user1 ARG buildno ... ``` 也可以给参数一个默认值: ``` FROM busybox ARG user1=someuser ARG buildno=1 ... ``` 如果我们给了ARG定义的参数默认值,那么当build镜像时没有指定参数值,将会使用这个默认值 ## ONBUILD 语法: ``` ONBUILD [INSTRUCTION] ``` 这个命令只对当前镜像的子镜像生效。 比如当前镜像为A,在Dockerfile种添加: ``` ONBUILD RUN ls -al ``` 这个 ls -al 命令不会在A镜像构建或启动的时候执行 此时有一个镜像B是基于A镜像构建的,那么这个ls -al 命令会在B镜像构建的时候被执行。 ## STOPSIGNAL 语法: ``` STOPSIGNAL signal ``` STOPSIGNAL命令是的作用是当容器推出时给系统发送什么样的指令 HEALTHCHECK 容器健康状况检查命令 语法有两种: ``` 1. HEALTHCHECK [OPTIONS] CMD command 2. HEALTHCHECK NONE ``` 第一个的功能是在容器内部运行一个命令来检查容器的健康状况 第二个的功能是在基础镜像中取消健康检查命令 [OPTIONS]的选项支持以下三中选项: ***--interval=DURATION 两次检查默认的时间间隔为30秒*** ***--timeout=DURATION 健康检查命令运行超时时长,默认30秒*** ***--retries=N 当连续失败指定次数后,则容器被认为是不健康的,状态为unhealthy,默认次数是3*** 注意: HEALTHCHECK命令只能出现一次,如果出现了多次,只有最后一个生效。 CMD后边的命令的返回值决定了本次健康检查是否成功,具体的返回值如下: ***0: success - 表示容器是健康的*** ***1: unhealthy - 表示容器已经不能工作了*** ***2: reserved - 保留值*** 例子: ``` HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost/ || exit 1 ``` 健康检查命令是:curl -f http://localhost/ || exit 1 两次检查的间隔时间是5秒 命令超时时间为3秒 # docker命令详解 Dockerfile中包括FROM、MAINTAINER、RUN、CMD、EXPOSE、ENV、ADD、COPY、ENTRYPOINT、VOLUME、USER、WORKDIR、ONBUILD等13个指令。下面一一讲解。 ## FROM 所谓定制镜像,那么就一定是以一个镜像为基础,在其上进行修改定制。就像我们之前运行了一个Nginx的容器,在其上面修改一样,基础容器是必需指定的。而`FROM`就是指定基础镜像,因此在DockerFile中,`FROM`是必备指定,并且必需是第一条指令! 除了指定现有的基础镜像以外,DockerFile还存在一个特殊的镜像`srcatch`,这个镜像是一个虚拟的概念,并不实际存在,它表示一个空白的镜像: ```bash FROM scratch ... ``` 如果你以`scratch`作为基础镜像,意味着你将不使用任何镜像为基础,接下来你所写的指令将作为第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见。如`swarm`、`coreos/etcd`。对Linux下静态编译的程序来说,并不需要其他操作提供其运行时支持,所需的一切库都在可执行文件里了,因此使用`scratch`作为基础,可以使镜像的体积更加小巧。 格式为FROM image或FROM image:tag,且在同一个Dockerfile中创建多个镜像时,可以使用多个FROM指令。 ## RUN `RUN`指令是用来执行命令行命令的,由于命令行的强大功能,`RUN`指令是定制镜像时最常用的指令之一。其格式有两种: - **shell格式**:就像在命令行中输入的Shell脚本命令一样,比如之前的: ```bash echo '

Hello Docker!

' > /usr/share/nginx/html/index.html ``` - **exec格式**:像是函数调用的格式,例如: ```bash apt-get update mkdir -p /usr/src/redis ``` DockerFile的每一个指令都会新构建一层,`RUN`命令也不例外。每一个`RUN`行为,都会新建立一层,然后在其上执行命令,执行完毕后,提交这一层的修改,构成新的镜像! UnionFS是有最大层数限制的,比如AUFS,曾经是最大不能超过42层,现在是最大不能超过127层。所以,对于一些编译、软件的安装、更新等操作,无需分成好几层来操作,这样会使得镜像非常臃肿,拥有非常多的层,不仅仅增加了构建部署的时间,也很容易出错!!例如,上面的`exec`格式的命令可以写作一层: ```bash RUN buildDeps=apt-get update && mkdir -p /usr/src/redis && apt-get purge -y --auto-remove $buildDeps ``` 这里仅仅使用了一个`RUN`指令,所以只会新建一层!对于一些编译、安装以及软件的更新等操作,没有必要分为很多层来操作,只需要一层就可以了!在此,我们可以使用`&&`符号将多个命令分割开,使其先后执行。此时,一个`RUN`指令有可能会变得非常长,为了使DockerFile的可阅读性和代码更加美观,我们可以使用`\`进行换行操作。另外,我们还可以使用`#`进行行首的注释。 观察刚刚编写的`RUN`指令,我们会发现在指令的结尾处添加了清理工作的命令,删除了为了编译构建的软件,清理了所有下载、展开的文件,并且还清理`apt`缓存文件。我们之前说过,镜像是多层存储,每一层存储的东西不会在下一层删除,会一直跟随着镜像。因此在镜像构建时,一定要确保每一层只添加真正需要的东西,任何无关的东西都应该被清理掉。 格式为RUN command或 RUN ["EXECUTABLE","PARAM1","PARAM2".....],前者在shell终端中运行命令,/bin/sh -c command,例如:/bin/sh -c "echo hello";后者使用exec执行,指定其他运行终端使用RUN["/bin/bash","-c","echo hello"] {% asset_img 270324-20180407222151795-458304224.png img %} {% asset_img 270324-20180407222206219-1004812737.png img %} 每条RUN指令将当前的镜像基础上执行指令,并提交为新的镜像,命令较长的时候可以使用\来换行。 ## COPY `COPY`指令将从上下文目录中的指定路径下的文件或文件夹复制到新的一层的镜像内的指定路径之下,格式为: ```bash COPY <源路径> ... <目标路径> ``` 原路径可以是多个,甚至是通配符,其通配规则只需要满足GO语言的`filepath.Math`规则即可,如下: ```bash COPY ./test1.py ./test2.py /test/ COPY ./t*.py /test/ COPY ./test?.py /test/ ``` 目标路径是容器内的绝对路径,也可以是工作目录下的相对路径,工作目录可以使用`WORKDIR`指令进行指定。目标路径不需要事先创建,Docker会自动创建所需的文件目录。使用`COPY`指令会将源路径的文件的所有元数据,比如读、写、指定全选、时间变更等。如果源路径时一个目录,那么会将整个目录复制到容器中,包括文件系统元数据。 ## ADD `ADD`指令和`COPY`的格式和性质基本一致,只不过是在`COPY`的基础上增加了一些功能。例如`ADD`指定中,源路径可以是一个远程URL,Docker引擎会自动帮我们将远程URL的文件下载下来到目标路径下,例如: ```bash ADD http://192.168.0.89:5000/test.py /test/ ``` 我们使用`docker build`进行构建镜像,然后使用`docker run`创建并启动容器,会发现在根目录下的test文件夹下有了`test.py`文件。如果源路径是本地的一个tar压缩文件时,`ADD`指定在复制到目录路径下会自动将其进行解压,如下: ```bash ADD docker2.tar /test/ ``` 压缩格式为`gzip`、`bzip2`以及`xz`的情况下,`ADD`指令都会将其解压缩! 非常值得注意的是,目标路径为一个URL时,会将其自动下载到目标路径下,但是其权限被自动设置成了`600`,如果这并不是你想要的权限,那么你还需要额外增加一层`RUN`命令进行更改,另外,如果下载的是一个压缩包,同样你还需要额外增加一层`RUN`命令进行解压缩。所以,在这种情况下,你还不如指定只用一层`RUN`,使用`curl`或者`wget`工具进行下载,并更改权限,然后进行解压缩,最后清理无用文件! 当你的源路径为压缩文件并且不想让Docker引擎将其自动解压缩,这个时候就不可以使用`ADD`命令,你可以使用`COPY`命令进行完成! 其实`ADD`命令并不实用,并不推荐使用!!! ## CMD 支持三种格式: CMD ["executable","param1","param2"],使用exec执行,这是推荐的方式。 CMD command param1 param2 在/bin/sh中执行。 CMD ["param1","param2"] 提供给ENTERYPOINT的默认参数。 CMD用于指定容器启动时执行的命令,每个Dockerfile只能有一个CMD命令,多个CMD命令只执行最后一个。若容器启动时指定了运行的命令,则会覆盖掉CMD中指定的命令。 `CMD`指令与`RUN`指令相似,也具有两种格式: - **shell格式**:**CMD <命令>** - **exec格式**:**CMD [“可执行文件”, “参数1”, “参数2”, …]** 之前介绍容器的时候就说过,Docker不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,就需要指定运行的程序及参数。`CMD`就是指定默认的容器主进程的启动命令的。 在运行时可以设置`CMD`指令来代替镜像设置中的命令,例如Ubuntu默认的`CMD`是`/bin/bash`,当我们使用命令`docker run -it ubuntu`创建并启动一个容器会直接进入`bash`。我们也可以在运行时指定运行别的命令,比如`docker run -it ubuntu cat /etc/os-release`,这就用`cat /etc/os-release`命令代替了默认的`/bin/bash`命令,输出了系统版本信息。比如,我想在启动容器的时候,在控制台中输出`Hello Docker!`,我们可以在Dockerfile中这样写,如下: ```bash FROM ubuntu CMD echo "Hello Docker!" ``` 接下来,我们构建一个镜像`ubuntu:v1.0`,接下来,我们以此镜像为基础创建并启动一个容器,如下: ```bash docker run -it ubuntu:v1.0 ``` 这样,就会在控制台中输出`Hello Docker!`的信息。 值得注意的是,如果使用shell格式,那么实际的命令会被包装成为`sh -c`的参数的形式进行执行。上面的`CMD`指令,在实际执行中会变成: ```bash CMD ["sh", "-c", "echo", "Hello Docker!"] ``` 因为这种特性,一些命令在加上`sh -c`之后,有可能会发生意想不到的错误,因此在Dockerfile中使用`RUN`指令时,更加推荐使用`exec`格式!最后需要牢记,使用`docker run`命令指定要执行的命令可以覆盖`RUN`指令,如果我们的`docker run`中指定了我们将要执行的命令,并且在Dockerfile中也指定了CMD命令,那么最终只会执行`docker run`命令中指定的命令。比如有这样一个Dockerfile: ```bash FROM ubuntu CMD ["echo", "Hello Docker!"] ``` 我们将其构建成成镜像`ubuntu:v1.1`,下面,我们以此镜像为基础创建并启动一个容器,如下: ```bash docker run -it ubuntu:v1.1 cat /etc/os-release ``` 那么容器只会执行`cat /etc/os-release`命令,也就是说在控制台只会输出系统版本信息,并不会输出Hello Docker!信息 ## ENTRYPOINT 格式有两种: ENTRYPOINT ["executable","param1","param2"] ENTRYPOINT command param1,param2 会在shell中执行。 用于配置容器启动后执行的命令,这些命令不能被docker run提供的参数覆盖。和CMD一样,每个Dockerfile中只能有一个ENTRYPOINT,当有多个时最后一个生效。 `ENTRYPOINT`指令和`CMD`指令目的一样,都是指定容器运行程序及参数,并且与`CMD`一样拥有两种格式的写法: - **shell格式**:**ENTRYPOINT <命令>** - **exec格式**:**ENTRYPOINT [“可执行文件”, “参数1”, “参数2”, …]** 与`CMD`指令一样,`ENTRYPOINT`也更加推荐使用`exec`格式,`ENTRYPOINT`在`docker run`命令中同样也可以进行指定,只不过比`CMD`指令来的繁琐一些,需要指定`--entrypoint`参数。同样,在`docker run`命令中指定了`--entrypoint`参数的话,会覆盖Dockerfile中`ENTRYPOINT`上的指令。 当指定了`ENTRYPOINT`指令时,`CMD`指令里的命令性质将会发生改变!`CMD`指令中的内容将会以参数形式传递给`ENTRYPOINT`指令中的命令,如下: ```bash FROM ubuntu ENTRYPOINT ["rm", "docker2"] CMD ["-rf"] ``` 其实,它真正执行的命令将会是: ```bash rm docker2 -rf ``` 从例子中可以看出,`ENTRYPOINT`指令和`CMD`指令非常的相似,也很容易将其搞混,就比如上面的例子,就可以完全使用一条`CMD`指令`CMD ["rm", "docker2", "-rf"]`来完成。这两个指令到底有什么区别,为什么要同时保留这两条指令呢? 我们可以使用`ENTRYPOINT`指令和`CMD`指令相结合,使得在创建并启动时要执行的命令更加灵活!有如下Dockerfile: ```bash FROM ubuntu RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* ENTRYPOINT ["curl", "-s", "http://ip.cn"] ``` 此时,我们将其构建成镜像`ubuntu:v1.2`,下面我们创建并启动容器: ```bash docker run -it ubuntu:v1.2 ``` 将会在控制台输出我们相应的公网IP信息!此时,如果我们还需要获取HTTP头信息时,我们可以这样: ```bash docker run -it ubuntu:v1.2 -i ``` 此时,将会在控制台中将公网IP信息以及HTTP头信息全部输出!我们知道,`docker run`命令中紧跟在镜像后面的是`CMD`指令命令,运行时会替换默认的`CMD`指令。因为我们在Dockerfile中指定了`ENTRYPOINT`指令,根据`ENTRYPOINT`指令的特性知道,当指定了`ENTRYPOINT`指令,`CMD`指令的内容将会以参数的形式传递给`ENTRYPOINT`,所以在容器中最终执行的命令是`curl -s -i http://ip.cn`,`-i`参数被传递到`ENTRYPOINT`中,所以最终在控制台中会输出HTTP头信息!!! ## ENV `ENV`指令用于设置环境变量,格式有两种: - **ENV** - **ENV = = …** 这个指令非常简单,就是用于设置环境变量而已,无论是接下来的指令,还是在容器中运行的程序,都可以使用这里定义的环境变量。例如: ```bash FROM ubuntu:16.04 ENV MODE=test RUN apt-get update && apt-get install -y curl && curl http://192.168.0.89:5000/$MODE && rm -rf /var/lib/apt/lists/* ``` 如果你要设置多个环境变量,为了美观,你可以使用`\`来进行换行。多个环境变量的隔开,使用空格进行隔开的,如果某个环境变量的值是由一组英文单词构成,那么你可以将其使用`""`进行圈起来。如下: ```bash FROM ubuntu:16.04 RUN MODE=test DESCRITPION="ios 12" \ TITLE="iphone" ``` 接下来,将这个Dockerfile构建成镜像,然后以此镜像为基础创建并启动一个容器,在容器中,我们调用这个环境变量,仍然是有用的!!! 值得注意的是,如果你想通过`CMD`或者`ENTRYPOINT`指令的exec格式来打印环境,就像下面这样: ```bash CMD ["echo", $MODE] CMD ["echo", "$MODE"] ``` 这样都是不能正确输出环境变量的值的,你可以改成exec格式来执行shell命令,如下: ```bash CMD ["sh", "-c", "echo $MODE"] ``` 如此,就能正确输出环境变量的值了! ## ARG 构建参数`ARG`和`ENV`指令一样,都是设置环境变量。与之不同的是,`ARG`设置的环境变量只是在镜像构建时所设置的,在将来容器运行时是不会存在这些环境变量的。但是不要因此就用`ARG`来保存密码之类的信息,因为通过`docker history`还是能够看得到的。`ARG`指令与`ENV`指令的使用类似,如下: ```bash FROM ubuntu:16.04 ARG app="python-pip" RUN apt-get update && apt-get install -y $app && rm -rf /var/lib/apt/lists/* ``` `ARG`构建参数可以通过`docker run`命令中的`--build-arg`参数来进行覆盖 ## VOLUME `VOLUME`指令用于构建镜像时定义匿名卷,其格式有两种: - **VOLUME <路径>** - **VOLUME [“<路径1>”, “<路径2>”, …]** 之前我们说过,容器存储层应该保持无状态化,容器运行时应尽量保持容器内不发生任何写入操作,对于需要保存动态数据的应用,其数据文件应该将其保存在数据卷中(VOLUME) 定义一个匿名卷: ```bash FROM ubuntu:16.04 VOLUME /data ``` 定义多个匿名卷: ```bash FROM ubuntu:16.04 VOLUME ["/data", "/command"] ``` 这里的`/data`和`/command`目录在容器运行时会自动挂载为匿名卷,任何向`/data`和`/command`目录中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化!容器匿名卷目录指定可以通过`docker run`命令中指定`-v`参数来进行覆盖 ## EXPOSE 格式为 EXPOSE port [port2,port3,...],例如EXPOSE 80这条指令告诉Docker服务器暴露80端口,供容器外部连接使用。 在启动容器的使用使用-P,Docker会自动分配一个端口和转发指定的端口,使用-p可以具体指定使用哪个本地的端口来映射对外开放的端口。 `EXPOSE`指令是声明运行时容器服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在Dockerfile中这样声明有两个好处:一个是帮助镜像使用者更好的理解这个镜像服务的守护端口,另一个作用则是在运行时使用随机端口映射时,也就是`docker run -p`命令时,会自动随机映射`EXPOSE`端口。 要将`EXPOSE`和在运行时使用`-p <宿主>:<容器端口>`区分开来,`-p`是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而`EXPOSE`仅仅是声明端口使用什么端口而已,并不会自动在宿主进行端口映射。 ## WORKDIR 格式: WORKDIR /path 为后续的RUN CMD ENTRYPOINT指定配置工作目录,可以使用多个WORKDIR指令,若后续指令用得是相对路径,则会基于之前的命令指定路径。 使用`WORKDIR`指令来制定工作目录(或者称为当前目录),以后各层操作的当前目录就是为指定的目录,如果该目录不存在,`WORKDIR`会自动帮你创建目录,如下: ```bash FROM ubuntu:16.04 WORKDIR /data/test RUN mkdir docker && echo "test" > demo.txt ``` 当我们使用`docker build`构建此镜像,并使用`docker run`命令进行创建和启动容器之后,会发现目录被自动切换到了`/data/test`,并且在当前目录下有一个文件夹`docker`,在`docker`下有一个文件`domo.txt`并且有相应的内容。我们还可以为特定的指令指定不同的工作目录,如下: ```bash FROM ubuntu:16.04 WORKDIR /data/test RUN mkdir docker WORKDIR /data/test/docker RUN echo "test" > demo.txt ``` 这样,Dockerfile中两次`RUN`指令的操作都在不同的目录下进行,最终容器会切换到最后一次`WORKDIR`指令下的目录。 `WORKDIR`指令可以通过`docker run`命令中的`-w`参数来进行覆盖 ## USER ``` 格式为:USER username指定容器运行时的用户名或UID,后续的RUN也会使用指定的用户。要临时使用管理员权限可以使用sudo。在USER命令之前可以使用RUN命令创建需要的用户。例如:RUN groupadd -r docker && useradd -r -g docker docker ``` `USER`指令用于将会用以什么样的用户去运行,例如: ```bash FROM ubuntu:16.04 USER docker ``` 基于该镜像启动的容器会以`docker`用户的身份来运行,我们可以指定用户名或者UID,组名或者GID,或者两者的结合,如下: ```bash FROM ubuntu:16.04 USER user USER user:group USER uid USER uid:gid USER user:gid USER uid:group ``` `USER`指令可以在`docker run`命令中的`-u`参数进行覆盖 ## HEALTHCHECK `HEALTHECHECK`指令是告诉Docker该如何判断容器的状态是否正常,这是1.12引入的新指令,其格式有两种: - **HEALTHCHECK [options] CMD <命令>**:检查容器健康状态的命令 - **HEALTHCHECK NONE**:如果基础镜像有健康检查指令,这一行将会屏蔽掉其健康检查指令 HEALTHECHECK支持下列选项: - **–interval=<间隔>**:两次检查的时间间隔,默认为30s - **–timeout=<时长>**:健康检查命令运行超时时间,如果超过这个时间,本次健康检查将会判定为失败,默认为30s - **–retries=<次数>**:当连续失败指定次数之后,则将容器状态视为`unhealthy`,默认为3次 在没有`HEALTHCHECK`指令之前,Docker引擎只可以通过容器内主进程是否退出来判断容器状态是否异常。很多情况下这没有问题,但是如果程序进入了死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法继续提供服务了。在1.12之前,Docker引擎不会检测到容器的这种状态,从而不会重新调度,导致可能容器已经无法提供服务了却仍然还在接收用户的请求。 假设我们有个镜像是最简单的Web服务,我们希望增加健康检查来判断Web服务是否在正常工作,我们可以用`curl`来帮助判断,其`Dockerfile`的`HEALTHCHECK`可以这么写: ```bash 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 ``` 接下来,我们将该Dockerfile编译构建成一个镜像,并以此镜像为基础创建并启动一个容器。此时,我们使用`docker container ls`命令来查看容器的状态,如下: ```bash root@ubuntu:~/docker# docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 036b91eea00d nginx:v1.2 "nginx -g 'daemon of…" 7 seconds ago Up 6 seconds (healthy) 0.0.0.0:80->80/tcp web ``` 我们再`STATUS`这一列中可以看到,状态未`healthy`。如果我们快速的多次执行`docker container ls`的话,会发现`STATUS`状态是由`health: starting`最后变为`healthy`,当然如果容器未在正常工作,最后的状态将会变为`unhealthy` 这里,我们设置了每5s检查一次,如果检查时间超过3s没有响应就视为失败。`||`符号左边的命令执行结果为假,右边的命令才会执行! 为了帮助排除故障,健康检查命令的输出会被存储于健康状态里,我们可以使用`docker inspect`命令来进行查看: ```bash root@ubuntu:~/docker# docker inspect --format '{{json .State.Health}}' web | python3 -m json.tool { "Status": "healthy", "FailingStreak": 0, "Log": [ { "Start": "2018-07-17T21:15:05.900643297+08:00", "End": "2018-07-17T21:15:05.968989028+08:00", "ExitCode": 0, "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n" } ] }

CMDNETRYPOINT一样,HEALTHCHECK指令只可以出现一次,如果有多个HEALTHCHECK指令,那么只有最后一个才会生效!!!

ONBUILD

格式ONBUILD [INSTRUCTION]该配置指定当所创建的镜像作为其他新建镜像的基础镜像时所执行的指令。例如下面的Dockerfile创建了镜像A:ONBUILD ADD . /appONBUILD RUN python app.py

``

ONBUILD`是一个特殊的指令,它后面跟着的是其他指令,比如`COPY`、`RUN`等,而这些命令在当前镜像被构建时,并不会被执行。只有以当前镜像为基础镜像去构建下一级镜像时,才会被执行。格式为:`ONBUILD <其他指令>

Dockerfile中的其他指令都是为了构建当前镜像准备的,只有ONBUILD指令是为了帮助别人定制而准备的。例如:

from ubuntu:16.04
WORKDIR /data
ONBUILD RUN mkdir test

此时,我们以此Dockerfile进行构建镜像ubuntu:test,并以此镜像为基础创建并启动一个容器,进入容器后,容器会自动切换到WORKDIR指令下的目录,此时我们使用ls命令会发现在工作目录下,并未创建test文件夹,如下:

root@ubuntu:~/docker# docker run -it ubuntu:test
root@3a8f912fd23b:/data# ls
root@3a8f912fd23b:/data#

此时,我们再创建一个Dockerfile,只需一个FROM指令即可,使其继承刚刚我们构建的ubuntu:test镜像,如下:

FROM ubuntu:test

我们再以此Dockerfile构建镜像ubuntu:test_onbuild,并以此镜像为基础创建并启动一个容器,进入容器后,容器会自动切换到WORKDIR指令下的目录,此时我们使用ls命令会发现在工作目录下,已经创建好了一个名为test的文件夹,如下:

root@ubuntu:~/docker# docker run -it ubuntu:test_onbuild
root@5394e605b6ea:/data# ls
test

LABEL

LABEL`指令可以为镜像指定标签,其格式为:`LABEL <key1>=<value1> <key2>=<value2> ...

LABEL后面是键值对,多个键值对以空格进行隔开,如果value中包含空格,请使用""将value进行圈起来,如下:

FROM ubuntu:16.04
LABEL name=test
LABEL description="a container is used to test"

我们知道,DockerFile的每一个指令都会新构建一层,所以,上面的LABEL我们可以写成一条指令,用空格进行隔开,如下:

FROM ubuntu:16.04
LABEL name=test description="a container is used to test"

为了美观,我们还可以使用\符号进行换行操作。

要查看镜像的标签,我们可以使用docker inspect命令,如下:

root@ubuntu:~# docker inspect --format '&#123;&#123;json .Config.Labels&#125;&#125;' test | python3 -m json.tool 
&#123;
    "description": "a container is used to test",
    "name": "test"
&#125;

其中“test”为容器名称!

值得注意的是,这里的标签并非是我们一开始将镜像名称中的<仓库>:<标签>,这两者是不一样的!这里标签,类似于签条,注解之类的意思

MAINTAINER

MAINTAINER`指令用于指定生成镜像的作者名称,其格式为:`MAINTAINER <name>

MAINTAINER指令已经被弃用,可以使用LABEL指令进行替代,如下:

LABEL maintainer='Stephen Chow'

MAINTAINER指令在一些老的Dockerfile中仍然可以看到,所以还是需要了解一下的!

docker build
创建好Dockerfile之后,通过docker build命令来创建镜像,该命令首先会上传Dockerfile文件给Docker服务器端,服务器端将逐行执行Dockerfile中定义的指令。
通常建议放置Dockerfile的目录为空目录。另外可以在目录下创建.dockerignore文件,让Docker忽略路径下的文件和目录,这一点与Git中的配置很相似。

通过 -t 指定镜像的标签信息,例如:docker build -t regenzm/first_image . ##”.”指定的是Dockerfile所在的路径

Docker history 查看docker镜像构建过程 还原dockerfile

介绍:

docker history --help
Usage:  docker history [OPTIONS] IMAGE
Show the history of an image
Options:
      --format string   Pretty-print images using a Go template
      --help            Print usage
  -H, --human           Print sizes and dates in human readable format (default true)
      --no-trunc        Don't truncate output
  -q, --quiet           Only show numeric IDs

示例:

docker history kubeguide/tomcat-app:v1
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
a29e200a18e9        2 years ago         /bin/sh -c #(nop) ADD dir:c5c3bddef49cbc9f...   992kB               
<missing>           2 years ago         /bin/sh -c #(nop) MAINTAINER bestme <bestm...   0B                  
<missing>           2 years ago         /bin/sh -c #(nop) CMD ["catalina.sh" "run"]     0B                  
<missing>           2 years ago         /bin/sh -c #(nop) EXPOSE 8080/tcp               0B                  
<missing>           2 years ago         /bin/sh -c set -e  && nativeLines="$(catal...   0B                  
<missing>           2 years ago         /bin/sh -c set -x   && curl -fSL "$TOMCAT_...   16.6MB              
<missing>           2 years ago         /bin/sh -c #(nop) ENV TOMCAT_TGZ_URL=https...   0B                  
<missing>           2 years ago         /bin/sh -c #(nop) ENV TOMCAT_VERSION=8.0.35     0B                  
<missing>           2 years ago         /bin/sh -c #(nop) ENV TOMCAT_MAJOR=8            0B                  
<missing>           2 years ago         /bin/sh -c set -ex  && for key in   05AB33...   114kB               
<missing>           2 years ago         /bin/sh -c apt-get update && apt-get insta...   7.18MB              
<missing>           2 years ago         /bin/sh -c &#123;   echo 'deb http://httpredir....   172B                
<missing>           2 years ago         /bin/sh -c #(nop) ENV OPENSSL_VERSION=1.0....   0B                  
<missing>           2 years ago         /bin/sh -c #(nop) WORKDIR /usr/local/tomcat     0B                  
<missing>           2 years ago         /bin/sh -c mkdir -p "$CATALINA_HOME"            0B                  
<missing>           2 years ago         /bin/sh -c #(nop) ENV PATH=/usr/local/tomc...   0B                  
<missing>           2 years ago         /bin/sh -c #(nop) ENV CATALINA_HOME=/usr/l...   0B                  
<missing>           2 years ago         /bin/sh -c set -x  && apt-get update  && a...   163MB               
<missing>           2 years ago         /bin/sh -c #(nop) ENV JAVA_DEBIAN_VERSION=...   0B                  
<missing>           2 years ago         /bin/sh -c #(nop) ENV JAVA_VERSION=7u101        0B                  
<missing>           2 years ago         /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/j...   0B                  
<missing>           2 years ago         /bin/sh -c &#123;   echo '#!/bin/sh';   echo 's...   87B                 
<missing>           2 years ago         /bin/sh -c #(nop) ENV LANG=C.UTF-8              0B                  
<missing>           2 years ago         /bin/sh -c apt-get update && apt-get insta...   1.17MB              
<missing>           2 years ago         /bin/sh -c apt-get update && apt-get insta...   44.3MB              
<missing>           2 years ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0B                  
<missing>           2 years ago         /bin/sh -c #(nop) ADD file:5d8521419ad6cfb...   125MB

如果要让CREATED BY 列完整显示,可以加上 –no-trunc 参数。直接在shell中看会比较乱,可以输出到文件查看,就比较直观了

docker history --no-trunc 镜像:tag

docker学习路线指引

在上一节课 docker学习00-容器技术产生的背景 中,我们讨论了容器技术产生的背景,和容器所解决的痛点问题,可见,容器技术的产生,可以显著地提高生产力,方便开发和运维工作,甚至推动了整个IT架构的演进。也让DEVOPS成为了可能!

那么在容器技术领域,有着许多的技术实现,比如LXC容器、OpenVZ,CoreOS rkt(备注: coreos已经被redhat收购),Containerd 、docker等等,这些都可以作为容器运行。

因为容器被越来越重视,使用越来越广泛,因此,在行业内,已经形成了容器技术的一些标准,比如OCI (开放容器计划),CRI(容器运行时接口),现在我们只需要知道,只要符合这些标准的容器技术,就可以相互替代,就如同满足USB3.0 的数据线,或者是type-c的数据线可以共用一样。

为什么选择docker?在众多的容器实现中,docker无疑是出类拔萃的,它甚至是行业标准的推进者,尽管容器编排老大k8s说在新版本中默认编排工具放弃使用docker,但终端用户完全无需担心,因为,docker是符合OCI标准的,用户使用docker进行的镜像构建,打包的应用,完全可以在k8s中的容器中运行,并且,docker对终端用户来说,使用起来,真的很方便!

好了,上面是一些背景知识,现在,接下来就可以开始docker的学习了。学习docker之前,我们要先了解要学习哪些内容,这样,才不至于,只见树木不见森林!

docker学习中,将学习哪些内容呢?下面是学习路线:

docker初探

带你初步认识docker,体验docker的魅力

  • docker安装:这一章节,将会介绍,docker的安装方式
  • docker开胃菜:这一节,会通过几个例子,来体验下docker的具体使用方法,以及其带来的便捷;

docker基础

带你一步步走进docker的世界,和docker交朋友

  • docker架构及底层技术:通过docker初探,我们对docker有了初步的感性认识,这一节将详细介绍docker的软件设计架构及依赖的底层技术,帮助大家更好的深入了解docker;
  • docker镜像:从这节开始,将介绍几个重要的docker对象,包括 docker镜像image,docker 容器container,docker镜像仓库 registry,docker存储,docker网络等; 这节课重点介绍 docker镜像;
  • docker 镜像仓库:介绍docker镜像仓库registry的内容;
  • docker容器:详细介绍docker容器的使用;
  • docker镜像多阶段构建:介绍docker镜像的新特性,多阶段构建相关内容;
  • 网络基础回顾:这节会带你回顾下网络相关的基础知识,方便后续能够更好的掌握docker网络相关知识;

docker网络

带你了解docker通讯的原理

  • docker单机网络:这节介绍docker在单台主机上运行时的网络模式;
  • docker跨主机网络:介绍docker在不同主机间的通讯原理,采用的网络模式等;

docker存储

带你掌握docker存储驱动和数据持久化机制

  • docker容器存储:这节课介绍docker容器的存储驱动类型,注意,这节课介绍的是docker运行时,数据在docker内的存储实现,而非在docker外部的持久化存储;
  • 持久化存储和数据共享:这节课将介绍如何实现docker容器数据的持久化和不同docker容器间的数据共享等;

docker容器编排

带你大规模玩转docker容器,让docker成为提高生产力的“利剑”

  • 单机编排工具之docker compose:从这节课开始讲介绍容器的编排工具,就是说,如何大规模地使用这些docker容器; 这节课将重点介绍单机编排工具之 docker compose的语法,用法等;
  • 大规模编排之k8s:这节课介绍docker大规模编排工具k8s,会带你初步认识k8s,并介绍k8s是如何实现大规模,集群化容器编排的,了解k8s是如何提高生产力的;

补充内容

主要是针对比较重要的内容,并且需要前面的基础知识做铺垫,所以,在这里给予补充,后面比较重要的东西,都会在这里进行补充。

  • docker镜像仓库开源工具之 Harbor:主要介绍 docker开源镜像仓库工具 harbor的部署,搭建、配置和使用等

Docker练习

部署SSM项目到docker容器中

  1. 基于 tomcat9镜像,使用Dockerfile创建项目镜像

    • 搭建静态网站

      //Dockerfile
      FROM tomcat:9.0-jdk8-corretto
      RUN mkdir $CATALINA_HOME/webapps/ROOT
      ADD index.html $CATALINA_HOME/webapps/ROOT/index.html
      CMD ["catalina.sh","run"]
      
      //运行镜像
      docker run --name tomcat -d -p 80:8080 镜像id
      
      //测试
      http://主机id
      
      
      
    • 搭建动态网站

      //压缩文件
      tar -zcvf app.tar.gz *
      
      //Dockerfile
      FROM tomcat:9.0-jdk8-corretto
      RUN mkdir $CATALINA_HOME/webapps/ROOT
      ADD app.tar.gz $CATALINA_HOME/webapps/ROOT/
      CMD ["catalina.sh","run"]
      
      //运行镜像
      docker run --name tomcat -d -p 80:8080 镜像id
      
      //测试
      http://主机id
      
  2. 基于mysql5.7镜像,使用Dockerfile创建项目的数据库镜像

    • 启动mysql服务器

      //Dockerfile
      FROM mysql:5.7
      WORKDIR /docker-entrypoint-initdb.d
      ENV LANG=C.UTF-8
      ADD init.sql .
      
      //运行mysql镜像
      //-t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
      //-d: 后台运行容器,并返回容器ID;
      //-i: 以交互模式运行容器,通常与 -t 同时使用;
      //-e username="ritchie": 设置环境变量;
      //--name="nginx-lb": 为容器指定一个名称;
      //-p: 指定端口映射,格式为:主机(宿主)端口:容器端口
      docker run -e MYSQL_ROOT_PASSWORD=root --name mysql -it -d -p 5001:3306  镜像Id
      
    • init.sql文件(注意文件类型编码为:utf8,回车换行符为:linux)

      CREATE DATABASE `mydb` /*!40100 DEFAULT CHARACTER SET utf8 */;
      
      use mydb;
      
      CREATE TABLE `dept` (
        `deptno` int(11) NOT NULL,
        `dname` varchar(14) DEFAULT NULL,
        `loc` varchar(13) DEFAULT NULL,
        PRIMARY KEY (`deptno`) USING BTREE
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      INSERT INTO `dept` VALUES (10, 'ACCOUNTING', 'NEW YORK');
      INSERT INTO `dept` VALUES (20, 'RESEARCH', 'DALLAS');
      INSERT INTO `dept` VALUES (30, 'SALES', 'CHICAGO');
      INSERT INTO `dept` VALUES (40, 'OPERATIONS', 'BOSTON');
      INSERT INTO `dept` VALUES (50, 'hr2', 'sy3');
      
    1. 把mysql容器连接到tomcat容器

      # 先删除mysql容器
      docker rm -f mysql容器id
      
      #重新创建mysql容器,并连接到tomcat
      # --net=container:tomcat 把新容器连接到tomcat容器的同一网络(共享网络空间)
       docker run -e MYSQL_ROOT_PASSWORD=root --name mysql -it -d --net=container:tomcat mysql镜像Id
       
       #测试
       http://主机id/dept/getAll.action
      

查看镜像详细信息

通过 docker inspect 命令,我们可以获取镜像的详细信息,其中,包括创建者,各层的数字摘要等

docker inspect 镜像Id

查看镜像历史

通过 docker history 命令,可以列出各个层(layer)的创建信息

docker history

容器运行后休眠

# 容器运行后一直休眠 infinity(永远)
docker run -d 镜像Id sleep infinity

#在容器中直接执行命令
docker exec 容器id cat /etc/hosts

图形化管理Docker

Portainer,这是一款由Docker核心贡献者之一开发的工具。Portainer的前身是DockerUI

docker run -d -p 9000:9000 \
 -v /var/run/docker.sock:/var/run/docker.sock \
 portainer/portainer -H unix:///var/run/docker.sock
 
 或
 
  docker run -d --restart=always -p 9000:9000 \
 -v /var/run/docker.sock:/var/run/docker.sock \
 -v portainer_data:/data \
 -v /public:/public \
 --name=portainer portainer/portainer
 
 运行参数信息:
-d:容器在后台运行;
-p 80:9000 :宿主机80端口映射容器中的9000端口,前一个80是宿主机端口,后一个9000是docker内部应用端口
-v /var/run/docker.sock:/var/run/docker.sock  :把宿主机的Docker守护进程(Docker daemon)默认监听的Unix域套接字挂载到容器中;
-v portainer_data:/data :把宿主机portainer_data数据卷挂载到容器/data目录下,以”:”分隔
–name=portainer:指定运行portainer容器的名称为portainer,以后再次运行时只需通过docker run portainer来运行了
--restart=always: 实现容器在宿主机开机时自启动
--privileged:映射文件最高权限,可以进行读写操作

执行上述命令将会在后台启动一个portainer容器。如果现在访问 http://localhost:9000 ,可以在看板上看到机器上运行的Docker的简要信息。

解决错误:WARNING: IPv4 forwarding is disabled. Networking will not work.

解决方法:

第一步:找到文件 :vim /usr/lib/sysctl.d/00-system.conf

添加代码:net.ipv4.ip_forward=1

第二步:重启network服务和docker服务:

systemctl restart network && systemctl restart docker

第三步:重启建容器

汉化

可以下载汉化包之后解压,并将解压后的public文件夹上传到centos系统的根目录下

然后执行以下命令:(如果已部署,需要将之前的容器删除)

docker run -d --restart=always -p 9000:9000 \
>  -v /var/run/docker.sock:/var/run/docker.sock \
>  -v portainer_data:/data \
>  -v /public:/public \
>  --name=portainer portainer/portainer

汉化后效果图:

镜像导出与导入

#导出
docker save -o debian.tar 镜像Id

#导入
docker load < debian.tar

#导入后没有tag,打标签
docker tag 镜像id  debian:latest

Docker Compose

安装

  1. 安装 pip

     yum install python-pip
    
  2. 4343

    yum install python3-pip
    
  3. 44

    pip3 install docker-compose
    
  4. 测试

    docker-compose -v
    结果
    docker-compose version 1.29.2, build unknown
    
  5. 配置 wordpress

    version: "3.9"
        
    services:
      db:
        image: mysql:5.7
        volumes:
          - db_data:/var/lib/mysql
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: somewordpress
          MYSQL_DATABASE: wordpress
          MYSQL_USER: wordpress
          MYSQL_PASSWORD: wordpress
        
      wordpress:
        depends_on:
          - db
        image: wordpress:latest
        volumes:
          - wordpress_data:/var/www/html
        ports:
          - "8000:80"
        restart: always
        environment:
          WORDPRESS_DB_HOST: db
          WORDPRESS_DB_USER: wordpress
          WORDPRESS_DB_PASSWORD: wordpress
          WORDPRESS_DB_NAME: wordpress
    volumes:
      db_data: {}
      wordpress_data: {}
    
  6. 安装 wordpress

    docker-compose up -d
    
  7. 测试:

    http://ip:8000
    

文章作者: FFFfrance
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 FFFfrance !