在使用Docker时,我们常常需要修改容器中的文件,并且希望在容器重启后这些修改能够得到保留。
0.简介
使用Docker时有一个需要注意的问题:当你修改了容器中的文件后,重启容器后这些修改将会被重置,深入研究这个问题。
Docker使用镜像来创建容器,镜像是包含了应用程序和其依赖的文件系统快照。当你运行一个容器时,Docker会基于镜像创建一个可运行的实例。这个实例拥有自己的文件系统,并且可以对其进行修改。然而,这些修改只会存在于容器的生命周期内,一旦容器被重启,所有修改将会丢失。
流程:
启动容器—进入容器—修改文件—退出容器—停止容器—重新启动容器
0.1 Docker操作简述
0.1.1 镜像的操作
1、拉取镜像的网址
http://hub.daocloud.io/
2、查看镜像
docker images
3、拉取镜像到本地
docker pull 镜像路径
4、删除镜像
docker rm 镜像的唯一标识
5、将本地的镜像导出
dokcer save -o 镜像路径 镜像ID
6、加载镜像
docker load -i 镜像名称
7、修改镜像名称和版本号
docker tag 镜像唯一标识 镜像新名称:版本号
0.1.2 容器的操作
容器的唯一标识就是容器的ID
容器就是运行起来的镜像
1、运行容器
#简单命令
docker run 镜像的唯一标识|镜像名称[:版本号]
#常用命令
docker run -d -p 宿主机端口:容器端口 --name 容器名称 镜像的唯一标识|镜像名称[:tag]
#-d 代表后台运行容器
#-p宿主机端口:容器端口 是为了映射当前liunx的端口号和容器的端口号
#--name 容器名称 指定容器的名称
2、查看正在运行的容器
docker ps [-qa]
#-a 查看全部的容器
#-q 只查看容器的ID
#-qa 查看全部的唯一标识(容器ID)
3、查看容器日志
1、docker logs -f 容器ID
2、docker logs -f -t 容器ID
3、docker logs -ft 容器ID
#看容器日志的最后几行
#-f 可以滚动查看容器日志的最后几行
#ctrl+c退出查看日志
4、docker logs -ft --tail n 容器ID
4、进入到容器内部
方式一:
#进入容器后,打开一个新的终端(常用)
1、docker exec -it 容器ID bash
2、docker exec -it 容器ID /bin/bash
方式二:
#进入容器正在执行的终端,不会启动新的进程!
docker attach 容器ID
5、退出容器
输入exit后回车
6、删除容器
#删除容器前,需要先停止容器
docker stop 容器ID #停止容器
dcoker rm 容器ID #删除容器
docker rm $(docker -ps -qa) #删除全部容器
7、数据卷
7.1什么是数据卷?
将宿主机中的一个目录映射到容器中的一个目录.
操作宿主机的目录,容器中的目录也会跟着改变
7.2创建数据卷
docker volume create 数据卷名称
#创建数据卷之后,会默认存放在一个目录下
# /var/bin/docker/volumes/数据卷名称/_data
7.3查看数据卷信息
docker volume inspect 数据卷名称
7.4查看全部数据卷
docker volume ls
7.5删除数据卷
docker volume rm 数据名称
7.6应用数据卷
#当你映射数据卷时,如果数据卷不存在,Docker会自动创建
#在运行容器的时候
docker run -v 数据卷名称:容器内部的路径 镜像ID
#没有设置宿主机中数据卷的路径,会自动创建路径
或者:
docker run -v 路径:容器内部的路径 镜像ID
8、查看docker容器的挂载目录
docker inspect 容器名字或者容器ID | grep Mounts -A 20
9、查看容器内部的目录文件(例子:查看/tmp目录下文件)
docker exec 容器ID ls /tmp
#linux重新启动docker 服务
systemctl restart docker.service
#设置docker服务随着开机自动启动
systemctl enable docker
#设置MySQL在docker中自动启动
docker update 容器名字 --restart=always
10、查看容器中的进程信息
docker top 容器ID
11、查看容器的信息
docker inspect 容器ID
step1:启动一个Docker容器
假设我们要启动一个基于Ubuntu镜像的容器,使用以下命令启动容器
docker run -it --name mycontainer ubuntu:latest
以上命令将以交互式的方式启动一个基于最新版本Ubuntu镜像的容器,并且容器的名称为mycontainer。
step2:进入容器
进入容器的目的是为了修改文件。使用以下命令进入容器:
docker exec -it mycontainer /bin/bash
以上命令将以交互式的方式进入名为mycontainer的容器,并且启动一个bash终端。
step3:修改文件
在容器中,我们可以通过任意文本编辑器修改文件。这里假设我们要修改的文件路径为/app/config.ini。使用以下命令打开文件:
vi /app/config.ini
在打开的文件中进行必要的修改,并保存退出。
step4:退出容器
完成文件的修改后,我们需要退出容器。使用以下命令退出容器:exit
step5:停止容器
在重新启动容器前,我们需要停止当前正在运行的容器。
docker stop mycontainer
以上命令将停止名为mycontainer的容器。
step6:重新启动容器
我们可以重新启动容器,并且修改的文件将得到保留,使用以下命令重新启动容器:
docker start mycontainer
以上命令将重新启动名为mycontainer的容器。
0.1.3 镜像容器查询
查看镜像的命令通常有:images、tag和inspect子命令
1.使用images命令列出本地镜像
使用docker images 命令可以列出本地主机上已有镜像的基本信息。
- REPOSITORY:表示来自于哪个仓库。
- TAG:表示镜像的标签信息,标签只是标记,并不能标识镜像内容。
- IMAGE ID:镜像ID,镜像的唯一标识,如果两个镜像ID相同,则说明它们实际上指向了同一个镜像,只是具有不同标签名而已。
- CREATED:表示镜像最后的更新时间。
- VIRTUAL SIZE:表示镜像大小,好的镜像往往体积会较小。
2.使用tag命令为镜像添加标签
为了方便在后续工作中使用特定镜像,还可以使用docker tag命令为本地镜像任意添加新的标签
3.使用inspect命令查看镜像详细信息
使用inspect命令可以获取镜像的详细信息,包括制作者、适应架构、各层的数字摘要等
4.使用history命令查看镜像历史
既然镜像文件由多个层组成,那么怎么才知道各层的内容具体是什么呢?这时候可以使用history子命令,该命令将列出各层的创建信息。
1.Docker修改数据后如何优雅保存退出
1.1 卷映射方式
使用Docker数据卷(Volume)功能
数据卷是一个特殊的目录,可以绕过容器的文件系统,将数据存储在主机上。这样,即使容器被重启,数据卷中的文件仍然会被保留下来。
如,创建一个简单的Flask应用,通过数据卷来保存应用的日志文件。
step1:创建一个Dockerfile来构建我们的镜像。在Dockerfile中,我们将指定应用程序的依赖和启动命令,并将日志文件的路径设置为一个数据卷。
FROM python:3.9
WORKDIR /app
COPY requirements.txt /app
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py /app
VOLUME /app/logs
CMD ["python", "app.py"]
通过VOLUME /app/logs将/app/logs设置为一个数据卷。这样,在容器运行时,所有写入到/app/logs目录的文件都将被保存在主机上。
step2:接下来构建镜像并运行容器
$ docker build -t myapp .
$ docker run -d -p 5000:5000 -v /path/to/logs:/app/logs myapp
上面的命令中,我们使用-v /path/to/logs:/app/logs将主机上的/path/to/logs目录映射到容器的/app/logs目录。这样,所有写入到/app/logs目录的文件都将被保存在主机上的/path/to/logs目录中。
step3:现在可以修改容器中的文件并重启容器来验证是否保存了修改。我们可以在容器中的app.py文件中添加一行日志输出:
import logging
logging.basicConfig(filename='logs/app.log', level=logging.INFO)
logging.info('Hello, Docker!')
然后可以重启容器:
$ docker restart <container_id>
重启后,我们可以查看主机上的/path/to/logs/app.log文件,应该能够看到刚才添加的日志信息。
总结一下,通过使用数据卷,我们可以解决Docker中修改文件后重启会被重置的问题。数据卷能够将容器中的文件保存在主机上,确保容器重启后文件的修改不会丢失。这对于保存应用程序的配置文件、日志文件等非常有用。
1.2 commit提交
我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff 命令看到具体的改动。
当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
commit:提交容器到镜像,实现容器持久化;
export:导出容器和镜像,实现容器内容持久化;
save:导出镜像文件,实现镜像内容持久化。
Docker通过 docker commit命令来提交容器成为一个新的副本。也就是从容器中创建一个新的镜像。
在docker 中通过 docker commit --help命令我们可以看到docker commit的描述以及它的一些可选项。
docker commit -m=“提交的描述信息” -a=“作者” 容器id 目标镜像名:[TAG]
docker commit 命令用于将容器的当前状态保存为一个新的 Docker 镜像。
docker commit 命令通常用于创建镜像来保存容器的状态,以便在将来可以重用或分发该镜像。
1.2.1 Docker镜像加载原理
Docker镜像基于UnionFS构建,采用分层结构,通过读写层与只读层的组合实现轻量级和高效的资源复用。镜像加载时,从基镜像开始层层加载,每个层代表一个文件系统,修改只存在于最上层的可写层。这种方式节约存储,便于快速部署,升级便捷,并允许多个容器共享基础镜像。UnionFS的优势包括空间节省、快速启动、内存优化、方便升级和独立修改。
Docker镜像为什么是一种轻量级,并且包含了运行某一个软件需要的一些代码、运行环境、配置文件等等,让你将所有的应用以及环境直接打包为镜像就可以直接运行。都是依赖于Docker中的UnionFs(联合文件系统)
UnionFs(联合文件系统)
unionfs是一种为Linux,FreeBSD和NetBSD操作系统设计的把其他文件系统联合到一个联合挂载点的文件系统服务。它使用branch把不同文件系统的文件和目录“透明地”覆盖,形成一个单一一致的文件系统。这些branches或者是read-only或者是read-write的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为unionfs用到了一个重要的资管管理技术叫写时复制。
写时复制(copy-on-write,下文简称CoW),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源;这个资源可以被新旧实例共享。创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增减小部分的开销。
———摘抄至《自己动手写Docker》
Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像,可以制作各种具体的应用镜像。
- 镜像加载原理
docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统UnionFS。
典型的Linux启动到运行需要两个FS - bootfs + rootfs
boots(boot file system)主要包含 bootloader和 Kernel, bootloader主要是引导加 kernel,
Linux刚启动时会加bootfs文件系统,在 Docker镜像的最底层是 boots。这一层与我们典型的
Linux/Unix系统是一样的,包含boot加載器和内核。当boot加载完成之后整个内核就都在内存中了,此时
内存的使用权已由 bootfs转交给内核,此时系统也会卸载bootfs。
rootfs(root file system),在 bootfs之上。包含的就是典型 Linux系统中
的/dev,/proc,/bin,/etc等标准目录和文件。 rootfs就是各种不同的操作系统发行版,比如 Ubuntu,
Centos等等。
典型的Linux在启动后,首先将 rootfs 置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite” 供用户使用。在docker中,起初也是将 rootfs 以readonly方式加载并检查,然而接下来利用 union mount 的将一个 readwrite 文件系统挂载在 readonly 的rootfs之上,并且允许再次将下层的 file system设定为readonly 并且向上叠加, 这样一组readonly和一个writeable的结构构成一个container的运行目录, 每一个被称作一个Layer
每一个对readonly层文件/目录的修改都只会存在于上层的writeable层中。这样由于不存在竞争, 多个container可以共享readonly的layer。
所以docker将readonly的层称作 “image” - 对于container而言整个rootfs都是read-write的,但事实上所有的修改都写入最上层的writeable层中,
上层的image依赖下层的image,因此docker中把下层的image称作父image,没有父image的image称作base image
想要从一个image启动一个container,docker会先加载其父image直到base image,用户的进程运行在writeable的layer中。所有parent image中的数据信息以及ID、网络和lxc管理的资源限制等具体container的配置,构成一个docker概念上的container
UnionFs(联合文件系统)的好处
1.节省存储空间 - 多个container可以共享base image存储
2.快速部署 - 如果要部署多个container,base image可以避免多次拷贝
3.内存更省 - 因为多个container共享base image, 以及OS的disk缓存机制,多个container中的进程命中缓存内容的几率大大增加
4.升级更方便 - 相比于 copy-on-write 类型的FS,base-image也是可以挂载为可writeable的,可以通过更新base image而一次性更新其之上的container
5.允许在不更改base-image的同时修改其目录中的文件 - 所有写操作都发生在最上层的writeable层中,这样可以大大增加base image能共享的文件内容。
参考资料
《自己动手写Docker》
【狂神说Java】Docker最新超详细版教程通俗易懂
Docker百度百科
1.2.2 commit提交举例
主要的思路是我们启动一个启动一个镜像然后对这个进行进行一些操作增删改查之类的,然后我们将修改之后的镜像通过commit命令生成一个新的镜像。
docker run -it -p 8080:8080 tomcat #启动tomcat
docker exec -it 9a7c6967e1f0 /bin/bash #进入到容器内部
修改之后的容器使用commit命令进行提交
docker commit -a=“David” -m=“add webapps app” 9a7c6967e1f0 tomcat02:1.0
这儿其实很好的体现了docker的分层原理,我们可以这样理解,最初的镜像是一层,我们的修改又是一层。然后通过commit命令将这两层打包成了一层。之后我们又可以在这一层上进行操作。
1.2.3 commit语法
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
OPTIONS说明:
-a :提交的镜像作者。
-c :使用 Dockerfile 指令来创建镜像。
-m :提交时的说明文字。
-p :提交镜像前暂停容器(默认为 true)
- 将容器保存为新镜像:
docker commit my_container my_new_image
将名为 my_container 的容器保存为一个名为 my_new_image 的新镜像。
- 指定标签:
docker commit my_container my_new_image:latest
将容器保存为带有 latest 标签的镜像
- 添加作者信息和提交信息:
docker commit -a “John Doe” -m “Added new features” my_container my_new_image
将容器保存为新镜像,并添加作者信息和提交信息。
- 在不暂停容器的情况下提交镜像:
docker commit --pause=false my_container my_new_image
在不暂停容器的情况下,将其保存为新镜像。
1.2.4 举例
- 启动一个容器:
docker run -d -it --name my_container ubuntu bash
- 进行一些更改:
docker exec my_container apt-get update
docker exec my_container apt-get install -y nginx
- 提交容器为新镜像:
docker commit -a “Your Name” -m “Installed nginx” my_container my_new_image
- 查看新镜像:
docker images
1.2.5 常用场景
- 保存工作进度: 在开发或测试过程中,将容器的当前状态保存为镜像,以便稍后可以恢复。
- 创建基础镜像: 为特定应用程序或环境配置创建自定义基础镜像。
- 分发配置: 将特定配置或应用程序状态保存为镜像,以便分发给其他团队成员或在不同环境中使用。
docker commit 命令是一个强大的工具,允许用户将容器的当前状态保存为新的 Docker 镜像。通过使用该命令,用户可以创建自定义镜像,以便在将来重用或分发。添加适当的作者和提交信息,有助于跟踪镜像的历史和变化。
2.Docker Images同名覆盖问题
在Docker中,当我们尝试构建一个与现有镜像同名的镜像时,新镜像会覆盖旧镜像
- 使用不同的镜像名称:一种简单的方法是为每个版本或应用程序使用不同的镜像名称。这样,即使新版本构建成功,也不会覆盖旧版本。
- 使用标签(Tags)来标记不同版本:Docker允许我们为镜像添加标签,以便标识不同的版本。我们可以为每个版本分配一个唯一的标签,并在构建新版本时使用该标签。这样,即使新版本构建成功,也不会覆盖具有相同名称但不同标签的旧版本。
- 清理不再需要的旧镜像:如果我们认为不再需要旧版本的镜像,可以使用Docker命令将其删除。例如,可以使用docker rmi <IMAGE_ID>命令删除特定ID的镜像。
- 使用多阶段构建:多阶段构建允许我们在单个Dockerfile中创建多个镜像。这样,我们可以为应用程序创建一个基础镜像,并为每个版本创建一个单独的镜像。基础镜像可以包含应用程序所需的所有依赖项和设置,而新版本可以基于基础镜像进行构建,同时添加任何必要的更改或修复。
请注意,使用不同的镜像名称或标签可能会导致管理变得更加复杂。因此,在选择适当的策略时,请根据您的具体需求和项目规模进行权衡。另外,始终确保在使用Docker时遵循最佳实践和安全准则,以防止潜在的安全风险和问题。