入門)
1. 從“黑盒”到“白盒”為什么Commit定制是Docker入門的必經(jīng)之路很多剛接觸Docker的朋友在學(xué)會了docker pull拉取鏡像、docker run啟動容器后面對的第一個進(jìn)階困惑往往是我該怎么做一個自己的鏡像網(wǎng)上的教程一上來就講Dockerfile各種FROM、RUN、COPY指令看得人頭大感覺門檻一下就上去了。其實Docker官方提供了一條更符合人類直覺的學(xué)習(xí)路徑那就是基于docker commit命令來定制鏡像。你可以把它理解為給一個現(xiàn)有的、運行中的系統(tǒng)“拍快照”。想象一下這個場景你從官方倉庫拉了一個純凈的Ubuntu鏡像運行起來后你就像登錄進(jìn)了一臺全新的虛擬機(jī)。在這臺“虛擬機(jī)”里你安裝了Nginx修改了配置文件部署了自己的網(wǎng)站代碼調(diào)整了系統(tǒng)參數(shù)。一頓操作之后你希望把當(dāng)前這個“完美狀態(tài)”保存下來以后直接就能用而不是每次都重復(fù)這一系列安裝配置步驟。docker commit干的就是這個事——它將一個容器的當(dāng)前文件系統(tǒng)變更連同它的運行歷史打包成一個新的、可復(fù)用的鏡像。雖然業(yè)界公認(rèn)的最佳實踐是使用聲明式的Dockerfile來構(gòu)建鏡像因為可追溯、可重復(fù)但commit方式作為一種命令式的構(gòu)建方法對于新手理解Docker鏡像的“層疊”本質(zhì)和容器狀態(tài)管理有著不可替代的教學(xué)意義。它能讓你直觀地感受到“鏡像即凍結(jié)的容器狀態(tài)”這一核心概念。今天我們就來手把手操作一遍把這個“黑盒”過程徹底變成“白盒”。2. 實戰(zhàn)演練從零開始Commit一個Nginx定制鏡像我們通過一個完整的例子來演示如何將一個基礎(chǔ)的Ubuntu容器定制成一個包含我們特定網(wǎng)頁的Nginx服務(wù)器鏡像。2.1 環(huán)境準(zhǔn)備與基礎(chǔ)容器啟動首先我們需要一個起點。這里我們選擇最常用的ubuntu:22.04作為基礎(chǔ)鏡像。# 1. 拉取基礎(chǔ)鏡像如果本地沒有 docker pull ubuntu:22.04 # 2. 以交互模式運行一個容器并給它起個名字方便后續(xù)操作 docker run -it --name my_ubuntu_container ubuntu:22.04 /bin/bash執(zhí)行上面的命令后你的終端會直接進(jìn)入到這個新容器的bash shell中。注意這時你看到的命令行提示符可能會變成類似roota1b2c3d4e5f6:/#的樣子這表明你已經(jīng)在容器內(nèi)部了。這個a1b2c3d4e5f6就是容器的短ID。注意-it是兩個參數(shù)-i保持標(biāo)準(zhǔn)輸入打開和-t分配一個偽終端。它們通常一起使用讓你可以像使用普通Linux終端一樣與容器交互。--name參數(shù)為容器指定一個易讀的名稱否則Docker會隨機(jī)分配一個不便于后續(xù)管理。2.2 在容器內(nèi)部進(jìn)行定制化操作現(xiàn)在我們在這個“純凈”的Ubuntu系統(tǒng)里開始我們的改造工作。請在容器內(nèi)的shell中依次執(zhí)行以下命令# 1. 更新軟件包列表Ubuntu的標(biāo)準(zhǔn)操作 apt-get update # 2. 安裝Nginx服務(wù)器和用于編輯文件的vim或nano apt-get install -y nginx vim # 3. 安裝完成后Nginx服務(wù)默認(rèn)不會自動啟動。我們先啟動它看看效果。 service nginx start # 4. 創(chuàng)建一個我們自己的網(wǎng)頁覆蓋Nginx的默認(rèn)首頁。 # 首先進(jìn)入Nginx默認(rèn)的網(wǎng)站根目錄 cd /var/www/html # 5. 備份原來的默認(rèn)首頁可選但是個好習(xí)慣 mv index.nginx-debian.html index.nginx-debian.html.bak # 6. 使用vim創(chuàng)建我們自己的首頁 vim index.html在vim中按i進(jìn)入插入模式輸入以下簡單的HTML內(nèi)容!DOCTYPE html html head titleMy Custom Docker Nginx/title /head body h1Hello from my committed Docker Image!/h1 pThis page is served from a custom image built via docker commit./p /body /html輸入完畢后按ESC鍵退出插入模式然后輸入:wq并按回車保存文件并退出vim。此時如果你在容器內(nèi)部使用curl localhost命令應(yīng)該能看到剛剛創(chuàng)建的HTML內(nèi)容。我們的定制化操作就完成了核心是更新系統(tǒng)、安裝軟件、修改配置、添加自定義文件。2.3 提交容器狀態(tài)生成新鏡像現(xiàn)在我們想要保存這個容器的當(dāng)前狀態(tài)。不要關(guān)閉或退出當(dāng)前容器的bash終端。你需要打開一個新的本地終端窗口或者使用終端的分屏功能。在新的終端中執(zhí)行以下命令# 查看當(dāng)前正在運行的容器確認(rèn)我們的容器ID或名稱 docker ps # 使用 docker commit 命令提交容器。格式docker commit [容器名/ID] [新鏡像名:標(biāo)簽] docker commit my_ubuntu_container my_custom_nginx:v1命令解析my_ubuntu_container這是我們之前通過--name指定的容器名稱。你也可以使用docker ps查看到的容器ID。my_custom_nginx:v1這是我們要創(chuàng)建的新鏡像的名稱和標(biāo)簽。名稱可以自定義標(biāo)簽v1常用于表示版本。執(zhí)行成功后終端會輸出新創(chuàng)建鏡像的長ID如sha256:xxxx...。你可以用docker images命令查看列表中應(yīng)該會出現(xiàn)一個名為my_custom_nginx、標(biāo)簽為v1的鏡像。2.4 驗證與運行定制鏡像提交完成后原來的容器my_ubuntu_container任務(wù)就完成了。我們可以在原容器終端里輸入exit退出并停止它。然后用我們剛做好的新鏡像來啟動一個全新的容器。# 1. 基于新鏡像運行一個容器并將容器的80端口映射到主機(jī)的8080端口 docker run -d -p 8080:80 --name my_nginx_test my_custom_nginx:v1 nginx -g daemon off;命令解析-d讓容器在后臺運行。-p 8080:80端口映射。將主機(jī)你的電腦的8080端口映射到容器的80端口Nginx默認(rèn)端口。--name my_nginx_test為新容器命名。nginx -g daemon off;這是容器的啟動命令。它以前臺模式啟動Nginx服務(wù)。這是運行Nginx等服務(wù)的常見做法因為Docker容器需要有一個前臺進(jìn)程才能保持運行?,F(xiàn)在打開你的瀏覽器訪問http://localhost:8080。你應(yīng)該能看到之前編寫的“Hello from my committed Docker Image!”頁面。這說明我們的定制鏡像完全成功了3. 深入原理Commit到底做了什么表面上docker commit只是保存了狀態(tài)但其背后體現(xiàn)了Docker鏡像的核心設(shè)計思想——聯(lián)合文件系統(tǒng)Union File System和層Layer。3.1 鏡像的層疊結(jié)構(gòu)與Commit的實質(zhì)Docker鏡像并非一個完整的、單一的文件包。它是由一系列只讀層Layer疊加起來的每一層代表文件系統(tǒng)的一次更改比如添加一個文件、安裝一個軟件包。當(dāng)你運行一個容器時Docker會在這些只讀層之上添加一個薄薄的可寫層容器層。所有在容器內(nèi)進(jìn)行的文件創(chuàng)建、修改、刪除都發(fā)生在這個可寫層。docker commit命令所做的正是將當(dāng)前容器的這個可寫層固化成一個新的、只讀的鏡像層并將這個新層疊加到原有鏡像層之上從而形成一個新的鏡像。你可以用docker history my_custom_nginx:v1命令來驗證這一點。這個命令會顯示構(gòu)建該鏡像的每一層歷史記錄你會看到最上面一層就是我們剛剛的提交操作以及它大致的尺寸。3.2 Commit與Dockerfile構(gòu)建的本質(zhì)區(qū)別理解了這個就能明白為什么commit方式雖然直觀但在生產(chǎn)環(huán)境中不被推薦可重復(fù)性Reproducibility差commit構(gòu)建的鏡像是一個“黑箱”。別人甚至未來的你自己無法確切知道鏡像里到底包含了哪些操作是apt-get install了三個包還是三十個修改了哪幾個配置文件。而Dockerfile是一個文本文件清晰地記錄了每一步操作構(gòu)建過程完全透明、可重復(fù)。鏡像臃腫commit會包含操作過程中產(chǎn)生的所有中間文件、緩存如apt-get的緩存/var/cache/apt/archives/、歷史記錄等。這會導(dǎo)致鏡像體積無謂地增大。Dockerfile則可以通過精心設(shè)計在一個RUN指令中串聯(lián)多條命令并及時清理緩存從而構(gòu)建出更精簡的鏡像。無法自動化與版本管理Dockerfile可以和代碼一起放入版本控制系統(tǒng)如Git任何更改都有記錄并且可以集成到CI/CD流水線中自動構(gòu)建。commit是手動操作難以融入自動化流程。所以commit是絕佳的學(xué)習(xí)工具和調(diào)試工具。當(dāng)你不知道如何用Dockerfile實現(xiàn)某個復(fù)雜環(huán)境配置時可以先用commit方式手動搭出來再用docker history或docker diff命令反推步驟最后寫成Dockerfile。這才是它的正確打開方式。4. Commit命令的進(jìn)階參數(shù)與實用技巧docker commit命令還有一些有用的參數(shù)可以幫助我們創(chuàng)建更符合需求的鏡像。4.1 使用-m和-a添加元數(shù)據(jù)就像Git提交一樣我們可以為這次鏡像提交添加注釋和作者信息這對于后期維護(hù)非常重要。docker commit -m Initial version with Nginx and custom homepage -a Your Name my_ubuntu_container my_custom_nginx:v1.1-m “…”提交信息說明這次定制的主要內(nèi)容。-a “…”作者信息。添加了元數(shù)據(jù)后使用docker inspect my_custom_nginx:v1.1命令在輸出的JSON信息中你可以找到Comment和Author字段里面就是我們剛才填寫的信息。4.2 使用–change或-c應(yīng)用Dockerfile指令這是commit命令一個非常強(qiáng)大但容易被忽略的功能。它允許你在提交的同時直接對鏡像應(yīng)用一些Dockerfile指令。例如我們想在提交時就指定新鏡像的默認(rèn)啟動命令docker commit --changeCMD [nginx, -g, daemon off;] my_ubuntu_container my_custom_nginx:with-cmd這樣生成的my_custom_nginx:with-cmd鏡像在運行時就不需要再在docker run后面指定啟動命令了直接docker run -d -p 8080:80 my_custom_nginx:with-cmd即可。--change支持的指令包括CMD,ENTRYPOINT,ENV,EXPOSE,USER,WORKDIR,VOLUME等。這相當(dāng)于在提交的瞬間為鏡像的頂層附加了一個微型的Dockerfile指令層。4.3 排查與調(diào)試docker diff的妙用如果你對一個正在運行的容器做了很多修改記不清到底改了哪些文件可以使用docker diff命令。它列出容器層可寫層相對于其基礎(chǔ)鏡像的所有變化。# 在提交前查看容器my_ubuntu_container的文件系統(tǒng)變化 docker diff my_ubuntu_container輸出通常由三種字符開頭A新增的文件AddedD刪除的文件DeletedC修改的文件Changed這個命令是反推Dockerfile步驟的利器。通過查看變化列表你可以清晰地知道安裝軟件創(chuàng)建了哪些目錄、修改了哪些配置從而更準(zhǔn)確地編寫Dockerfile中的COPY或RUN指令。5. 從Commit到Dockerfile最佳實踐遷移指南通過commit掌握了鏡像定制的感性認(rèn)識后我們的最終目標(biāo)是要將其轉(zhuǎn)化為一個可維護(hù)的Dockerfile。以上面的Nginx定制為例我們來還原并優(yōu)化出一個標(biāo)準(zhǔn)的Dockerfile。5.1 反推操作步驟編寫初始Dockerfile回顧我們在容器內(nèi)的操作apt-get updateapt-get install -y nginx vim進(jìn)入/var/www/html目錄創(chuàng)建自定義的index.html文件對應(yīng)的Dockerfile初版如下# 基于Ubuntu 22.04 FROM ubuntu:22.04 # 執(zhí)行系統(tǒng)更新和軟件安裝 RUN apt-get update apt-get install -y nginx # 設(shè)置工作目錄不一定必要但好習(xí)慣 WORKDIR /var/www/html # 將我們本地的網(wǎng)頁文件復(fù)制到鏡像中 COPY ./my-index.html /var/www/html/index.html # 聲明容器運行時暴露的端口 EXPOSE 80 # 設(shè)置容器啟動時執(zhí)行的命令 CMD [nginx, -g, daemon off;]在同一目錄下創(chuàng)建一個my-index.html文件內(nèi)容就是我們之前寫的HTML。5.2 優(yōu)化Dockerfile縮小體積與提升構(gòu)建效率初版Dockerfile有兩個明顯問題1. 沒有清理apt緩存鏡像會很大2. 使用了相對臃腫的Ubuntu作為基礎(chǔ)鏡像。優(yōu)化后如下# 使用更精簡的官方Nginx鏡像作為基礎(chǔ)它本身基于Debian FROM nginx:alpine # 直接覆蓋默認(rèn)的首頁文件 COPY ./my-index.html /usr/share/nginx/html/index.html # 基于alpine的Nginx鏡像已經(jīng)暴露了80端口并設(shè)置了正確的CMD所以這里可以省略EXPOSE和CMD。 # 但如果需要自定義可以顯式寫出 # EXPOSE 80 # CMD [nginx, -g, daemon off;]這個優(yōu)化版的優(yōu)勢極其明顯體積ubuntu:22.04鏡像約70MB安裝Nginx后可能超過100MB。而nginx:alpine鏡像只有約20MB。效率省去了apt-get update install的漫長過程構(gòu)建速度飛快。安全與維護(hù)使用官方維護(hù)的鏡像減少了系統(tǒng)層面的依賴和潛在安全漏洞。5.3 構(gòu)建并驗證優(yōu)化后的鏡像使用優(yōu)化后的Dockerfile進(jìn)行構(gòu)建和運行# 構(gòu)建鏡像注意最后有一個點表示當(dāng)前上下文目錄 docker build -t my_nginx_dockerfile:latest . # 運行容器 docker run -d -p 8081:80 --name nginx_from_dockerfile my_nginx_dockerfile:latest # 訪問驗證 curl http://localhost:8081你會發(fā)現(xiàn)效果和之前用commit制作的鏡像完全一樣但整個過程清晰、可重復(fù)、鏡像更小。這就是Dockerfile的價值所在。6. 常見問題與避坑指南在實際操作docker commit時新手很容易遇到以下幾個坑6.1 提交后容器內(nèi)的服務(wù)沒有自動啟動這是最常見的問題。很多人以為在容器里用service nginx start啟動了服務(wù)提交后的鏡像就會自動運行它。這是錯誤的。commit只保存文件系統(tǒng)的狀態(tài)即安裝了Nginx配置文件也改了但不保存容器的運行狀態(tài)進(jìn)程列表、內(nèi)存狀態(tài)等。鏡像的默認(rèn)啟動行為由兩個指令決定CMD和ENTRYPOINT。如果你從官方ubuntu鏡像commit它的默認(rèn)CMD是bash。所以你直接運行新鏡像它只會啟動一個bash shellNginx服務(wù)并不會啟動。解決方案在docker run時直接指定啟動命令如我們之前做的docker run ... nginx -g daemon off;。在commit時使用--change參數(shù)修改默認(rèn)CMD如上文4.2節(jié)所示。更好的方式是后續(xù)使用Dockerfile來構(gòu)建在文件中明確指定CMD。6.2 提交的鏡像包含了敏感數(shù)據(jù)或臨時文件在容器內(nèi)操作時可能會無意中留下密碼文件、緩存、日志等。commit會把這些全部打包進(jìn)去存在安全風(fēng)險和導(dǎo)致鏡像臃腫。排查與解決提交前檢查使用docker diff 容器名仔細(xì)查看即將被提交的變更列表。對于不需要的文件可以在容器內(nèi)手動刪除后再提交。使用.dockerignore理念雖然commit沒有類似機(jī)制但要有意識地在操作結(jié)束時清理臨時文件例如運行apt-get clean來清除安裝包緩存。終極方案還是使用Dockerfile在RUN指令中鏈?zhǔn)讲僮鞑⒓磿r清理例如RUN apt-get update apt-get install -y nginx apt-get clean rm -rf /var/lib/apt/lists/*。6.3 鏡像的層級過多且混亂頻繁地對同一個容器進(jìn)行修改并commit會產(chǎn)生多個迭代的鏡像版本。每個commit都會產(chǎn)生一個新層導(dǎo)致鏡像歷史冗長、關(guān)系復(fù)雜。管理建議為鏡像使用有意義的標(biāo)簽如myapp:dev-20231027而不是每次都打latest標(biāo)簽。定期使用docker image prune清理未被使用的中間鏡像或懸虛鏡像dangling images即沒有標(biāo)簽的鏡像層。明確commit的定位它是用于創(chuàng)建“一次性”基礎(chǔ)模板或用于調(diào)試的過渡手段而非正式的版本管理工具。定稿后應(yīng)及時轉(zhuǎn)化為Dockerfile。走過commit定制的整個流程你才能真正體會到Docker鏡像“層”的概念不再是抽象的術(shù)語。它就像做蛋糕每一層commit就是往上抹一層奶油或水果。雖然最終大家都會用食譜Dockerfile來高效、標(biāo)準(zhǔn)地做蛋糕但親手抹一遍奶油才能深刻理解每一層對最終成品的影響。下次當(dāng)你遇到一個復(fù)雜環(huán)境不知如何用Dockerfile描述時不妨先run一個基礎(chǔ)容器進(jìn)去手動把它調(diào)通然后commit一下再用docker history和docker diff看看你究竟做了什么——這往往是解開難題最快的一把鑰匙。