当前位置: 首页 > 科技观察

使用Docker镜像作为桌面系统

时间:2023-03-15 01:47:19 科技观察

博主一直喜欢思考如何管理安装在自己电脑上的桌面系统。这篇文章算是前作中的主力军,可以加载到虚拟机中,可以随时打包带走。Linux就是这样一个强大的后续探索。近年来,Docker在全球掀起了一股容器化浪潮,因为它提供了一种非常便捷的方式来创建和运行应用程序容器。容器通过打包软件及其运行所需的环境使人们免于依赖。虽然Docker设计的初衷不是一个操作系统容器,更不是一个直接运行在裸机上的操作系统,但是docker这个强大的工具也会给我们管理操作系统带来很大的方便。为什么使用Docker镜像作为桌面系统?这要从普通桌面系统的不便说起。通常我们都有不止一台计算机,我们希望这些计算机保持一致。这里所说的“一致性”,打个比方,就是我在一台电脑上编辑了一半的文件,不需要复制到另一台电脑上,直接打开电脑就可以编辑了。如果文件只是纯文本文件或MicrosoftWord文档,实现这种一致性很容易:只需将文件放在Dropbox等云同步驱动器上即可。但是,对于专业用户来说,这种一致性的维护并不是简单的扔进Dropbox那么简单:比如你最近忙一个项目,需要好几种编程语言,然后在里面装了一堆库电脑。一堆工具软件,有图形界面和命令行。在工作过程中,您可能会不断安装新工具,或者决定放弃您之前计划使用的库或工具。要让你的工作在你的几台电脑上工作,你必须始终保持不同机器环境的一致性:安装在一台机器上的工具必须在所有机器上重新安装。在一台机器上升级的库需要在所有机器上升级。如果稍有不同,一个脚本/程序可能在一台机器上运行良好,但在另一台机器上运行不佳。运行问题。不熟悉docker哲学的读者可以点此了解docker。Docker的使用非常简单:我们编写一个Dockerfile,在Dockerfile中写入相应的命令,来安装和配置我们想要的库和工具。不熟悉docker的读者可以看看下面复制的Dockerfile示例,以了解Dockerfile是什么样子的:/us.archive.ubuntu.com/ubuntu/preciseuniverse">>/etc/apt/sources.listRUNapt-getupdateRUNapt-getinstall-ynodejs#RUNapt-getinstall-ynodejs=0.6.12~dfsg1-1ubuntu1RUNmkdir/var/wwwADDapp.js/var/www/app.jsCMD["/usr/bin/node","/var/www/app.js"]使用Dockerfile,只需要一条dockerbuild命令就可以创建一个docker镜像。同时,Docker公司提供了一个名为DockerHub的服务,可以免费托管公共镜像。使用dockerpush直接将镜像上传到DockerHub即可。在不同的电脑上,只需要dockerpull就可以从DockerHub中获取最新版本的镜像。DockerHub还支持自动构建。通过将DockerHub账号与GitHub账号关联,DockerHub可以在GitHub上的Dockerfile发生变化时自动重新生成镜像。文章开头提到的一致性维护,docker其实是在给我们提供答案:我们构建一个docker镜像,让这个镜像包含我们项目需要的一切。这样我们就可以使用dockerrun为所有的开发、测试、部署等任务开启一个容器,然后在容器中执行所有的工作。当我们决定修改运行环境时,比如引入新的库,我们应该在Dockerfile中进行相应的修改,重新生成镜像,然后在不同的机器上使用dockerpull更新。这种使用理念,通过一个中心化的仓库,非常优雅的解决了不同机器上环境一致的问题。美中不足的是,并不是所有的程序都能跑在容器里,也不是所有的程序都方便跑在容器里。如果使用图形界面的程序,或者一些系统级的程序,在容器中使用这些程序会很麻烦,有的甚至可能根本无法使用。所以很自然地想到,如果我们每次开机都可以直接挂载一个docker生成的镜像作为根目录,我们就可以让这个镜像直接在裸机上(而不是在容器中)运行,来做我们的日常桌面系统。这种做法,除了保持一致性的便利之外,还有一些其他的好处:整个系统都存储在云端,本地的内容只是云端的一个缓存,不需要定期更新系统。备份。你的系统是如何从头开始一步步配置到你想要的,一切都清楚地显示在Dockerfile中。Dockerfile是您的完美笔记。不用担心长期使用系统后残留的一些垃圾文件,或者某些系统中某些程序的数据损坏,因为我们每次启动都是一个全新的系统。要安装新机器,您不需要从头开始安装操作系统。您只需要从DockerHub中拉取镜像并使用它。安装系统的过程变得非常方便。系统更新的过程其实就是从最新的软件仓库重新安装,根据Dockerfile生成docker镜像的过程。不会出现某些更新遇到文件冲突或者依赖无法处理需要人为干预才能完成的问题。dockerstoragedriver的工作原理Docker的storagedriver的工作原理官方已经介绍过了,这里只是简单的介绍一下。Docker使用层的概念。当docker构建镜像时,它会逐行执行我们的Dockerfile中的每一行。每执行一行,docker都会创建一个新的层来存储新的内容。当我们执行dockerpull或dockerpush时,docker实际上传下载的是这些层之间的增量。每当执行dockerrun时,docker会将这些下载的层组合在一起,形成一个完整的镜像,然后创建一个新的读写层,运行过程中的所有写入都会写入读写层。图像本身保持只读状态,无法更改。“层”的概念具体实现取决于docker目录(通常是目录/var/lib/docker)所在的文件系统。具体实现在docker中称为graphdriver,docker自带的graphdrivers包括aufs、overlay、btrfs、zfs、devicemapper等,这些graphdrivers大多采用了copy-on-write技术,使得进程合并各个层不需要再复制一份数据,真正的复制发生在写入的时候。由于笔者使用的是btrfs,本文以btrfs为例,介绍如何将系统启动到docker镜像。Btrfs是一个写时复制系统。由于dockerimages是一层层堆叠的,docker使用btrfs时,每堆叠一层,docker都会创建一个原始层的快照,然后把新层的内容写入到快照中。然后docker在从镜像创建容器的时候会对镜像的顶层做一个快照,并将这个快照作为容器的读写层。开始进入docker镜像了解docker存储驱动的工作原理,还需要了解linux的启动过程才能达到我们的目的。Linux在启动的时候一般会让launcher加载一个内存盘initramfs到内核,然后内核完成简单的前期初始化后,将内存盘的内容解压到根目录/,然后启动init内存盘中的程序(一般是/init),这个init程序会进行进一步的初始化(比如加载文件系统的驱动,fscking文件系统等),这一步初始化完成后,init程序会根据内核选项中的root和rootflags等内容挂载真实根目录,然后通过switch_root程序启动真实根目录下的init程序。这个init程序会完成初始的初始化工作,比如挂载fstab,加载图形界面等等。很多发行版都提供了制作initramfs的工具,比如archlinux的mkinitcpio。这些工具通常是模块化的,允许用户自己添加钩子。让系统启动进入docker镜像所需的知识已经完成。思路也很明确:通过给initramfs加一个hook,让initramfs中的init在挂载root作为读写层之前,先从docker本地缓存中的镜??像创建一个快照,然后将这个读写层作为真正的root去山。具体操作上,在bootmanager中写入启动项的内核选项时,root写入的是/var/lib/docker所在的分区,rootflags中至少要有一个subvol=XXXXX,其中XXXXX就是我们要创建的读文件写层的位置。那么最重要的就是写一个hook。这个hook的作用是:找到想要的dockerimage对应的btrfssubvolume,为这个subvolume创建一个snapshot,命名为XXXXX(内核选项中的名字保持一致)。在这种情况下,Linux将控制权交给initramfs中的init程序后,init程序会先从docker缓存中的subvolume创建一个XXXXX快照,然后以root身份挂载XXXXX快照,并执行以下操作。如果读者和作者一样使用ArchLinux,那么作者已经完成了所有的工作,读者可以直接使用。作者的源码位于GitHub:https://github.com/zasdfgbnm/mkinitcpio-docker-hooks,读者也可以直接从AUR中搜索mkinitcpio-docker-hooks安装作者的hook。下面介绍一下这个钩子的使用方法。mkinitcpio-docker-hooks的使用mkinitcpio-docker-hooks的使用大致分为以下几个步骤:确保你的/var/lib/docker位于一个btrfs分区准备一个适合在裸机上启动的docker镜像和然后运行在这个镜像中安装配置mkinitcpio-docker-hooks,准备内核和initramfs,准备顶层内容,设置bootmanager,准备docker镜像。很多docker镜像,为了减小镜像的体积,没有自带只能在裸机上使用的软件包(如dhcpcd),所以读者可能需要在Dockerfile中手动安装这些软件包。对于ArchLinux,只需要安装base组。由于接下来会安装mkinitcpio-docker-hooks,所以建议使用内置yaourt的镜像。我用的是自己的archlinux-yaourt镜像zasdfgbnm/archlinux-yaourt。所以Dockerfile的开头是这样的:以FROMzasdfgbnm/archlinux-yaourtUSERrootRUNpacman-Syu--noconfirmbase为例,这里不安装除base以外的其他软件。请读者根据需要自行安装其他软件。安装和配置mkinitcpio-docker-hooksmkinitcpio-docker-hooks安装在docker内部,而不是当前运行在裸机上的系统。这个软件包之所以要安装在docker镜像中很重要,因为linux内核不提供ABI稳定性,所以内核模块和内核的版本必须严格对应,否则无法加载模块。为了保持这种一致性,我们采取的措施是在docker中安装mkinitcpio-docker-hooks,在docker中生成initramfs,启动时从image中的kernel启动,这样kernel和initramfs中的modules可以得到保证,和开机进入镜像后的/lib/modules是一致的。安装过程在Dockerfile中是这样写的:RUNsudo-uuseryaourt-S--noconfirmmkinitcpio-docker-hooksmkinitcpio-docker-hooks安装完成后,需要进行配置。配置文件在/etc/docker-btrfs.json中。初始内容如下:{"docker_image":"archlinux/base","docker_tag":"latest"}我们需要做的就是将这两个变量的值替换为我们想要的值。比如这里我打算把我的docker镜像的名字叫做“sample_image”。同时,我们还需要在/etc/mkinitcpio.conf中添加docker-btrfshook。综上所述,在Dockerfile中可以使用如下命令进行配置:RUNsed-i's/archlinux\/base/sample_image/g'/etc/docker-btrfs.jsonRUNperl-i-p-e's/(?<=^HOOKS=\()(.*)(?=\))/$1docker-btrfs/g'/etc/mkinitcpio.conf我们现在有一个完整的Dockerfile示例:FROMzasdfgbnm/archlinux-yaourtUSERrootRUNpacman-Syu--noconfirmbaseRUNsudo-uuseryaourt-S--noconfirmmkinitcpio-docker-hooksRUNsed-i's/archlinux\/base/sample_image/g'/etc/docker-btrfs.jsonRUNperl-i-p-e's/(?<=^HOOKS=\()(.*)(?=\))/$1docker-btrfs/g'/etc/mkinitcpio.conf只需要通过dockerbuild构建自己的镜像即可。-t样本图像。准备好内核和initramfs映像后,下一步就是准备内核并构建initramfs。注意这一步要在你打算用来启动docker镜像的机器上进行,因为mkinitcpio会根据机器自动将对应的内核模块放到initramfs中。如果是在另一台机器上做的,可能有一些Driver没有自动加载到initramfs中。如前所述,这一步是在docker容器中完成的。首先运行容器,打开一个shell:dockerrun-v$(pwd):/workspace-w/workspace-itsample_imagebash然后在容器中执行如下命令:mkinitcpio-plinuxcp/boot/*.exit就可以在里面看到当前目录下准备了initramfs-linux-fallback.img、initramfs-linux.img和vmlinuz-linux。准备顶层的内容。顶层是mkinitcpio-docker-hooks引入的新概念。它指的是某个驱动器中的目录。该目录会在启动时创建读写层之后,真正的root挂载之前创建。,通过busybox的cp-a命令将整个文件复制到读写层。为什么我们需要顶层?因为我们需要在多台机器上启动同一个镜像,而往往会根据需要在不同的机器上配置不同的配置文件,比如/etc/fstab和/etc/X11/xorg.conf。另外,免费的DockerHub账号上的镜像都是公开的,镜像中不适合存放/etc/passwd、/etc/shadow等私有文件。准备顶层的内容其实就是找一个文件夹,将需要配置的文件按照与根目录的相对路径分别存放在这个文件夹中。例如,如果你想为某台机器单独配置/etc/fstab,那么你应该在顶层目录下添加文件etc/fstab。这里推荐的具体操作流程是:先通过dockerrun-v$(pwd):/workspace-w/workspace-itsample_imagebash进入容器中的shell,然后在里面做各种配置,比如useradd...,完成后,将新生成的配置文件复制到顶层文件夹,设置启动管理器。设置好顶层之后,我们基本上就可以算是万事俱备了。我们只需要简单的设置启动管理器就可以启动我们的系统了。这里我们以refind为例。这里假设我们所有的东西都放在一个标签为“linux”的btrfs分区中,docker目录(也就是你的系统启动后会挂载到/var/lib/docker的目录)就存放在这个在分区根目录下一个名为“docker”的子卷中,kernel、initramfs和toplayer都位于分区根目录下的“boot_docker”文件夹中,以及我们要的读写层的名称创建名为“docker_rwlayer”,那么refind.conf中对应菜单项的代码如下:LABEL=linuxrootflags=subvol=docker_rwlayerrwdocker_path=dockertoplay=LABEL=linuxtoplayer_path=boot_docker"}在kernel选项中,我们通过root指定docker目录所在的分区,rootflags中的subvol指定读写的位置te层,docker_path指定docker目录在root中的相对位置;通过toplayer来指定顶层目录所在的分区,toplayerflags用于指定顶层所在分区的挂载选项,toplayer_path用于指定顶层目录在toplayer分区中的相对位置.一切就绪,重启并享受!