Docker與Kubernetes系列(二): Docker的基本用法
這段時間工作中用到了Docker以及Kubernetes(簡稱K8S),現在整理下我學習Docker以及K8S過程中看的一些比較好的資料,方便自己回顧,也希望能給容器小白一些幫助。給自己定一個小目標,二月底之前完成。
這是本系列的第二篇文章, 將介紹Docker的一些基本用法。
整理自: https://www.gitbook.com/book/yeasy/docker_practice/details
一、獲取、執行、列出映象
從 Docker Registry 獲取映象的命令是 docker pull。其命令格式為:
docker pull [選項] [Docker Registry地址]<倉庫名>:<標籤> 具體的選項可以通過 docker pull --help 命令看到,這裡我們說一下映象名稱的格式。
Docker Registry地址:地址的格式一般是 <域名/IP>[:埠號]。預設地址是 Docker Hub。 倉庫名:如之前所說,這裡的倉庫名是兩段式名稱,既 <使用者名稱>/<軟體名>。對於 Docker Hub,如果不給出使用者名稱,則預設為 library,也就是官方映象。
要想列出已經下載下來的映象,可以使用 docker images 命令。
有了映象後,我們就可以以這個映象為基礎啟動一個容器來執行。以上面的 ubuntu:14.04 為例,如果我們打算啟動裡面的 bash 並且進行互動式操作的話,可以執行下面的命令。
$ docker run -it --rm ubuntu:14.04 bash docker run 就是執行容器的命令,我們這裡簡要的說明一下上面用到的引數。
-it:這是兩個引數,一個是 -i:互動式操作,一個是 -t 終端。我們這裡打算進入 bash 執行一些命令並檢視返回結果,因此我們需要互動式終端。 --rm:這個引數是說容器退出後隨之將其刪除。預設情況下,為了排障需求,退出的容器並不會立即刪除,除非手動 docker rm。我們這裡只是隨便執行個命令,看看結果,不需要排障和保留結果,因此使用 --rm 可以避免浪費空間。 ubuntu:14.04:這是指用 ubuntu:14.04 映象為基礎來啟動容器。 bash:放在映象名後的是命令,這裡我們希望有個互動式 Shell,因此用的是 bash。
二、使用Dockerfile定製映象
Dockerfile 是一個文字檔案,其內包含了一條條的指令(Instruction),每一條指令構建一層,因此每一條指令的內容,就是描述該層應當如何構建。
From指令
所謂定製映象,那一定是以一個映象為基礎,在其上進行定製。就像我們之前運行了一個 nginx 映象的容器,再進行修改一樣,基礎映象是必須指定的。而 FROM 就是指定基礎映象,因此一個 Dockerfile 中 FROM 是必備的指令,並且必須是第一條指令。
除了選擇現有映象為基礎映象外,Docker 還存在一個特殊的映象,名為 scratch。這個映象是虛擬的概念,並不實際存在,它表示一個空白的映象。
FROM scratch ... 如果你以 scratch 為基礎映象的話,意味著你不以任何映象為基礎,接下來所寫的指令將作為映象第一層開始存在。
不以任何系統為基礎,直接將可執行檔案複製進映象的做法並不罕見,比如 swarm、coreos/etcd。對於 Linux 下靜態編譯的程式來說,並不需要有作業系統提供執行時支援,所需的一切庫都已經在可執行檔案裡了,因此直接 FROM scratch 會讓映象體積更加小巧。
Run指令
RUN 指令是用來執行命令列命令的。由於命令列的強大能力,RUN 指令在定製映象時是最常用的指令之一。其格式有兩種:
shell 格式:RUN <命令>,就像直接在命令列中輸入的命令一樣。 exec 格式:RUN ["可執行檔案", "引數1", "引數2"],這更像是函式呼叫中的格式。
RUN buildDeps='gcc libc6-dev make' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" 僅僅使用一個 RUN 指令,並使用 && 將各個所需命令串聯起來。
映象構建上下文(Context)
在build的時候,只需要再Dockerfile所處目錄下執行以下命令即可:
docker build [選項] <上下文路徑/URL/-> 那麼什麼是上下文呢?
首先我們要理解 docker build 的工作原理。Docker 在執行時分為 Docker 引擎(也就是服務端守護程序)和客戶端工具。Docker 的引擎提供了一組 REST API,被稱為 Docker Remote API,而如 docker 命令這樣的客戶端工具,則是通過這組 API 與 Docker 引擎互動,從而完成各種功能。因此,雖然表面上我們好像是在本機執行各種 docker 功能,但實際上,一切都是使用的遠端呼叫形式在服務端(Docker 引擎)完成。也因為這種 C/S 設計,讓我們操作遠端伺服器的 Docker 引擎變得輕而易舉。
當我們進行映象構建的時候,並非所有定製都會通過 RUN 指令完成,經常會需要將一些本地檔案複製進映象,比如通過 COPY 指令、ADD 指令等。而 docker build 命令構建映象,其實並非在本地構建,而是在服務端,也就是 Docker 引擎中構建的。那麼在這種客戶端/服務端的架構中,如何才能讓服務端獲得本地檔案呢?
這就引入了上下文的概念。當構建的時候,使用者會指定構建映象上下文的路徑,docker build 命令得知這個路徑後,會將路徑下的所有內容打包,然後上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下文包後,展開就會獲得構建映象所需的一切檔案。
如果在 Dockerfile 中這麼寫:
COPY ./package.json /app/ 這並不是要複製執行 docker build 命令所在的目錄下的 package.json,也不是複製 Dockerfile 所在目錄下的 package.json,而是複製 上下文(context) 目錄下的 package.json。
因此,COPY 這類指令中的原始檔的路徑都是相對路徑。這也是初學者經常會問的為什麼 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法工作的原因,因為這些路徑已經超出了上下文的範圍,Docker 引擎無法獲得這些位置的檔案。如果真的需要那些檔案,應該將它們複製到上下文目錄中去。
COPY 複製檔案
格式:
COPY <源路徑>... <目標路徑> COPY ["<源路徑1>",... "<目標路徑>"] 和 RUN 指令一樣,也有兩種格式,一種類似於命令列,一種類似於函式呼叫。
COPY 指令將從構建上下文目錄中 <源路徑> 的檔案/目錄複製到新的一層的映象內的 <目標路徑> 位置。比如:
COPY package.json /usr/src/app/
ADD 更高階的複製檔案
ADD 指令和 COPY 的格式和性質基本一致。但是在 COPY 基礎上增加了一些功能。
比如 <源路徑> 可以是一個 URL,這種情況下,Docker 引擎會試圖去下載這個連結的檔案放到 <目標路徑> 去。下載後的檔案許可權自動設定為 600,如果這並不是想要的許可權,那麼還需要增加額外的一層 RUN進行許可權調整,另外,如果下載的是個壓縮包,需要解壓縮,也一樣還需要額外的一層 RUN 指令進行解壓縮。所以不如直接使用 RUN 指令,然後使用 wget 或者 curl 工具下載,處理許可權、解壓縮、然後清理無用檔案更合理。因此,這個功能其實並不實用,而且不推薦使用。
如果 <源路徑> 為一個 tar 壓縮檔案的話,壓縮格式為 gzip, bzip2 以及 xz 的情況下,ADD 指令將會自動解壓縮這個壓縮檔案到 <目標路徑> 去。
在某些情況下,這個自動解壓縮的功能非常有用,比如官方映象 ubuntu 中:
FROM scratch ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ... Docker 官方的最佳實踐文件中要求,儘可能的使用 COPY,因為 COPY 的語義很明確,就是複製檔案而已,而 ADD 則包含了更復雜的功能,其行為也不一定很清晰。最適合使用 ADD 的場合,就是所提及的需要自動解壓縮的場合。
CMD 容器啟動命令
CMD 指令的格式和 RUN 相似,也是兩種格式:
shell 格式:CMD <命令> exec 格式:CMD ["可執行檔案", "引數1", "引數2"...] 引數列表格式:CMD ["引數1", "引數2"...]。在指定了 ENTRYPOINT 指令後,用 CMD 指定具體的引數。 之前介紹容器的時候曾經說過,Docker 不是虛擬機器,容器就是程序。既然是程序,那麼在啟動容器的時候,需要指定所執行的程式及引數。CMD 指令就是用於指定預設的容器主程序的啟動命令的。
提到 CMD 就不得不提容器中應用在前臺執行和後臺執行的問題。這是初學者常出現的一個混淆。
Docker 不是虛擬機器,容器中的應用都應該以前臺執行,而不是像虛擬機器、物理機裡面那樣,用 upstart/systemd 去啟動後臺服務,容器內沒有後臺服務的概念。
一些初學者將 CMD 寫為:
CMD service nginx start 然後發現容器執行後就立即退出了。甚至在容器內去使用 systemctl 命令結果卻發現根本執行不了。這就是因為沒有搞明白前臺、後臺的概念,沒有區分容器和虛擬機器的差異,依舊在以傳統虛擬機器的角度去理解容器。
對於容器而言,其啟動程式就是容器應用程序,容器就是為了主程序而存在的,主程序退出,容器就失去了存在的意義,從而退出,其它輔助程序不是它需要關心的東西。
而使用 service nginx start 命令,則是希望 upstart 來以後臺守護程序形式啟動 nginx 服務。而剛才說了 CMD service nginx start 會被理解為 CMD [ "sh", "-c", "service nginx start"],因此主程序實際上是 sh。那麼當 service nginx start 命令結束後,sh 也就結束了,sh 作為主程序退出了,自然就會令容器退出。
正確的做法是直接執行 nginx 可執行檔案,並且要求以前臺形式執行。比如:
CMD ["nginx", "-g", "daemon off;"]
ENV 設定環境變數
格式有兩種:
ENV <key> <value> ENV <key1>=<value1> <key2>=<value2>... 這個指令很簡單,就是設定環境變數而已,無論是後面的其它指令,如 RUN,還是執行時的應用,都可以直接使用使用這裡定義的環境變數。
ENV VERSION=1.0 DEBUG=on \ NAME="Happy Feet" NTRYPOINT 入口點
ENTRYPOINT 的格式和 RUN 指令格式一樣,分為 exec 格式和 shell 格式。
ENTRYPOINT 的目的和 CMD 一樣,都是在指定容器啟動程式及引數。ENTRYPOINT 在執行時也可以替代,不過比 CMD 要略顯繁瑣,需要通過 docker run 的引數 --entrypoint 來指定。
當指定了 ENTRYPOINT 後,CMD 的含義就發生了改變,不再是直接的執行其命令,而是將 CMD 的內容作為引數傳給 ENTRYPOINT 指令,換句話說實際執行時,將變為:
<ENTRYPOINT> "<CMD>" 那麼有了 CMD 後,為什麼還要有 ENTRYPOINT 呢?這種 <ENTRYPOINT> "<CMD>" 有什麼好處麼?讓我們來看幾個場景。
場景一:讓映象變成像命令一樣使用
假設我們需要一個得知自己當前公網 IP 的映象,那麼可以先用 CMD 來實現:
FROM ubuntu:16.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* CMD [ "curl", "-s", "http://ip.cn" ] 假如我們使用 docker build -t myip . 來構建映象的話,如果我們需要查詢當前公網 IP,只需要執行:
$ docker run myip 當前 IP:61.148.226.66 來自:北京市 聯通 嗯,這麼看起來好像可以直接把映象當做命令使用了,不過命令總有引數,如果我們希望加引數呢?比如從上面的 CMD 中可以看到實質的命令是 curl,那麼如果我們希望顯示 HTTP 頭資訊,就需要加上 -i 引數。那麼我們可以直接加 -i 引數給 docker run myip 麼?
$ docker run myip -i docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n". 我們可以看到可執行檔案找不到的報錯,executable file not found。之前我們說過,跟在映象名後面的是 command,執行時會替換 CMD 的預設值。因此這裡的 -i 替換了遠了的 CMD,而不是新增在原來的 curl -s http://ip.cn 後面。而 -i 根本不是命令,所以自然找不到。
那麼如果我們希望加入 -i 這引數,我們就必須重新完整的輸入這個命令:
$ docker run myip curl -s http://ip.cn -i 這顯然不是很好的解決方案,而使用 ENTRYPOINT 就可以解決這個問題。現在我們重新用 ENTRYPOINT 來實現這個映象:
FROM ubuntu:16.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* ENTRYPOINT [ "curl", "-s", "http://ip.cn" ] 這次我們再來嘗試直接使用 docker run myip -i:
$ docker run myip 當前 IP:61.148.226.66 來自:北京市 聯通
$ docker run myip -i HTTP/1.1 200 OK Server: nginx/1.8.0 Date: Tue, 22 Nov 2016 05:12:40 GMT Content-Type: text/html; charset=UTF-8 Vary: Accept-Encoding X-Powered-By: PHP/5.6.24-1~dotdeb+7.1 X-Cache: MISS from cache-2 X-Cache-Lookup: MISS from cache-2:80 X-Cache: MISS from proxy-2_6 Transfer-Encoding: chunked Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006 Connection: keep-alive
當前 IP:61.148.226.66 來自:北京市 聯通 可以看到,這次成功了。這是因為當存在 ENTRYPOINT 後,CMD 的內容將會作為引數傳給 ENTRYPOINT,而這裡 -i 就是新的 CMD,因此會作為引數傳給 curl,從而達到了我們預期的效果。
場景二:應用執行前的準備工作
啟動容器就是啟動主程序,但有些時候,啟動主程序前,需要一些準備工作。
比如 mysql 類的資料庫,可能需要一些資料庫配置、初始化的工作,這些工作要在最終的 mysql 伺服器執行之前解決。
此外,可能希望避免使用 root 使用者去啟動服務,從而提高安全性,而在啟動服務前還需要以 root 身份執行一些必要的準備工作,最後切換到服務使用者身份啟動服務。或者除了服務外,其它命令依舊可以使用 root 身份執行,方便除錯等。
這些準備工作是和容器 CMD 無關的,無論 CMD 為什麼,都需要事先進行一個預處理的工作。這種情況下,可以寫一個指令碼,然後放入 ENTRYPOINT 中去執行,而這個指令碼會將接到的引數(也就是 <CMD>)作為命令,在指令碼最後執行。比如官方映象 redis 中就是這麼做的:
FROM alpine:3.4 ... RUN addgroup -S redis && adduser -S -G redis redis ... ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379 CMD [ "redis-server" ] 可以看到其中為了 redis 服務建立了 redis 使用者,並在最後指定了 ENTRYPOINT 為 docker-entrypoint.sh 指令碼。
#!/bin/sh ... # allow the container to be started with `--user` if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then chown -R redis . exec su-exec redis "$0" "[email protected]" fi
exec "[email protected]" 該指令碼的內容就是根據 CMD 的內容來判斷,如果是 redis-server 的話,則切換到 redis 使用者身份啟動伺服器,否則依舊使用 root 身份執行。比如:
$ docker run -it redis id uid=0(root) gid=0(root) groups=0(root)
VOLUME 定義匿名卷
格式為:
VOLUME ["<路徑1>", "<路徑2>"...] VOLUME <路徑> 之前我們說過,容器執行時應該儘量保持容器儲存層不發生寫操作,對於資料庫類需要儲存動態資料的應用,其資料庫檔案應該保存於卷(volume)中,後面的章節我們會進一步介紹 Docker 卷的概念。為了防止執行時使用者忘記將動態檔案所儲存目錄掛載為卷,在 Dockerfile 中,我們可以事先指定某些目錄掛載為匿名卷,這樣在執行時如果使用者不指定掛載,其應用也可以正常執行,不會向容器儲存層寫入大量資料。
VOLUME /data 這裡的 /data 目錄就會在執行時自動掛載為匿名卷,任何向 /data 中寫入的資訊都不會記錄進容器儲存層,從而保證了容器儲存層的無狀態化。當然,執行時可以覆蓋這個掛載設定。比如:
docker run -d -v mydata:/data xxxx 在這行命令中,就使用了 mydata 這個命名卷掛載到了 /data 這個位置,替代了 Dockerfile 中定義的匿名卷的掛載配置。
EXPOSE 宣告埠
格式為 EXPOSE <埠1> [<埠2>...]。
EXPOSE 指令是宣告執行時容器提供服務埠,這只是一個宣告,在執行時並不會因為這個宣告應用就會開啟這個埠的服務。在 Dockerfile 中寫入這樣的宣告有兩個好處,一個是幫助映象使用者理解這個映象服務的守護埠,以方便配置對映;另一個用處則是在執行時使用隨機埠對映時,也就是 docker run -P時,會自動隨機對映 EXPOSE 的埠。
要將 EXPOSE 和在執行時使用 -p <宿主埠>:<容器埠> 區分開來。-p,是對映宿主埠和容器埠,換句話說,就是將容器的對應埠服務公開給外界訪問,而 EXPOSE 僅僅是宣告容器打算使用什麼埠而已,並不會自動在宿主進行埠對映。
WORKDIR 指定工作目錄
格式為 WORKDIR <工作目錄路徑>。
使用 WORKDIR 指令可以來指定工作目錄(或者稱為當前目錄),以後各層的當前目錄就被改為指定的目錄,該目錄需要已經存在,WORKDIR 並不會幫你建立目錄。
之前提到一些初學者常犯的錯誤是把 Dockerfile 等同於 Shell 指令碼來書寫,這種錯誤的理解還可能會導致出現下面這樣的錯誤:
RUN cd /app RUN echo "hello" > world.txt 如果將這個 Dockerfile 進行構建映象執行後,會發現找不到 /app/world.txt 檔案,或者其內容不是 hello。原因其實很簡單,在 Shell 中,連續兩行是同一個程序執行環境,因此前一個命令修改的記憶體狀態,會直接影響後一個命令;而在 Dockerfile 中,這兩行 RUN 命令的執行環境根本不同,是兩個完全不同的容器。這就是對 Dokerfile 構建分層儲存的概念不瞭解所導致的錯誤。
之前說過每一個 RUN 都是啟動一個容器、執行命令、然後提交儲存層檔案變更。第一層 RUN cd /app 的執行僅僅是當前程序的工作目錄變更,一個記憶體上的變化而已,其結果不會造成任何檔案變更。而到第二層的時候,啟動的是一個全新的容器,跟第一層的容器更完全沒關係,自然不可能繼承前一層構建過程中的記憶體變化。
因此如果需要改變以後各層的工作目錄的位置,那麼應該使用 WORKDIR 指令。
三、操作容器
容器是 Docker 又一核心概念。
簡單的說,容器是獨立執行的一個或一組應用,以及它們的執行態環境。對應的,虛擬機器可以理解為模擬執行的一整套作業系統(提供了執行態環境和其他系統環境)和跑在上面的應用。
下面將具體介紹如何來管理一個容器,包括建立、啟動和停止等。
啟動容器
啟動容器有兩種方式,一種是基於映象新建一個容器並啟動,另外一個是將在終止狀態(stopped)的容器重新啟動。
因為 Docker 的容器實在太輕量級了,很多時候使用者都是隨時刪除和新建立容器。
新建並啟動
所需要的命令主要為 docker run。
例如,下面的命令輸出一個 “Hello World”,之後終止容器。
$ sudo docker run ubuntu:14.04 /bin/echo 'Hello world' Hello world 這跟在本地直接執行 /bin/echo 'hello world' 幾乎感覺不出任何區別。
下面的命令則啟動一個 bash 終端,允許使用者進行互動。
$ sudo docker run -t -i ubuntu:14.04 /bin/bash [email protected]:/# 其中,-t 選項讓Docker分配一個偽終端(pseudo-tty)並繫結到容器的標準輸入上, -i 則讓容器的標準輸入保持開啟。
當利用 docker run 來建立容器時,Docker 在後臺執行的標準操作包括:
檢查本地是否存在指定的映象,不存在就從公有倉庫下載 利用映象建立並啟動一個容器 分配一個檔案系統,並在只讀的映象層外面掛載一層可讀寫層 從宿主主機配置的網橋介面中橋接一個虛擬介面到容器中去 從地址池配置一個 ip 地址給容器 執行使用者指定的應用程式 執行完畢後容器被終止 啟動已終止容器
可以利用 docker start 命令,直接將一個已經終止的容器啟動執行。
容器的核心為所執行的應用程式,所需要的資源都是應用程式執行所必需的。除此之外,並沒有其它的資源。可以在偽終端中利用 ps 或 top 來檢視程序資訊。
[email protected]:/# ps PID TTY TIME CMD 1 ? 00:00:00 bash 11 ? 00:00:00 ps 可見,容器中僅運行了指定的 bash 應用。這種特點使得 Docker 對資源的利用率極高,是貨真價實的輕量級虛擬化。
後臺(background)執行
更多的時候,需要讓 Docker在後臺執行而不是直接把執行命令的結果輸出在當前宿主機下。此時,可以通過新增 -d 引數來實現。
終止容器
可以使用 docker stop 來終止一個執行中的容器。
此外,當Docker容器中指定的應用終結時,容器也自動終止。 例如對於上一章節中只啟動了一個終端的容器,使用者通過 exit 命令或 Ctrl+d 來退出終端時,所建立的容器立刻終止。
終止狀態的容器可以用 docker ps -a 命令看到。例如
sudo docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ba267838cc1b ubuntu:14.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton 98e5efa7d997 training/webapp:latest "python app.py" About an hour ago Exited (0) 34 minutes ago backstabbing_pike
進入容器
在使用 -d 引數時,容器啟動後會進入後臺。 某些時候需要進入容器進行操作,有很多種方法,包括使用 docker attach 命令或 nsenter 工具等。
attach 命令
docker attach 是Docker自帶的命令。下面示例如何使用該命令。
$ sudo docker run -idt ubuntu 243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550 $ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia $sudo docker attach nostalgic_hypatia [email protected]:/# 但是使用 attach 命令有時候並不方便。當多個視窗同時 attach 到同一個容器的時候,所有視窗都會同步顯示。當某個視窗因命令阻塞時,其他視窗也無法執行操作了。
刪除容器
可以使用 docker rm 來刪除一個處於終止狀態的容器。 例如
$sudo docker rm trusting_newton trusting_newton 如果要刪除一個執行中的容器,可以新增 -f 引數。Docker 會發送 SIGKILL 訊號給容器。
清理所有處於終止狀態的容器
用 docker ps -a 命令可以檢視所有已經建立的包括終止狀態的容器,如果數量太多要一個個刪除可能會很麻煩,用 docker rm $(docker ps -a -q) 可以全部清理掉。
*注意:這個命令其實會試圖刪除所有的包括還在執行中的容器,不過就像上面提過的 docker rm 預設並不會刪除執行中的容器 --------------------- 作者:沈鴻斌 來源:CSDN 原文:https://blog.csdn.net/u012422829/article/details/54958826 版權宣告:本文為博主原創文章,轉載請附上博文連結!